先拜读原文。
https://www.sonarsource.com/blog/why-code-security-matters-even-in-hardened-environments/
虽然适用场景比较小,但整个漏洞利用过程还是比较有意思的。是一个教科书式的web转pwn的漏洞。
利用前提条件:
1,linux系统上存在nodejs搭建的web。
2,有文件上传功能,且文件内容和文件路径均可控。
很多人看到这里,你这不扯呢么,都文件路径可控了,我直接任务计划/ssh key了,还废这事。
然而很多情况下web中间件都是低权限,甚至像php这种,实战中都有可能碰到web目录无法写webshell。唯一能写的./images目录,给你来个不解析。
3,低权限,无法任务计划/ssh key等
4,使用了官方编译的node,而不是apt install的
5,攻击者知道node版本
实战中肯定很难碰上这种苛刻条件,出在CTF上倒不错。首先思考一下,低权限下哪些目录可读可写。显然除了/tmp之外,还有/proc/self/fd/xx。
fd目录下的是文件描述符,而它们链接的就是一个一个的管道(pipe),用来和其他进程通信。
漏洞的关键就在于nodejs的异步IO库libuv,会对管道进行循环处理,其中会调用一个结构体的回调函数指针,具体代码见原文。
uv__signal_event->uv__signal_msg_t->uv_signal_s->signal_cb
简单来说,只需要向/proc/self/fd/15中上传一个结构体地址uv_signal_t和一个signum,uv_signal_t中包含一个回调函数指针signal_cb和signum。uv_signal_t和signum会被打包到栈上,被当成uv__signal_msg_t,当两个signum相等时,signal_cb会被调用。
也就是说,我们只需要找到一个PIVOT_GADGET,它的第一部分是one_gadget,第二部分是任意signum,然后向/proc/self/fd/15上传p64(PIVOT_GADGET)+p64(signum),就能跳转到one_gadget上了。
当然one_gadget肯定不现实,因为栈可以控制嘛,所以one_gadget换成多个pop/ret。文件上传就变成,p64(PIVOT_GADGET)+p64(signum)+p64(pop_rdi_ret)+p64(bin_sh_addr)+p64(system_addr)。
是不是变成了一个很简单的pwn题?
但不要忘记这一切的条件是我们知道node的text区地址(libc显然就别想了),也就是说需要/usr/bin/node关闭PIE保护。这都2024年了,真的还会有现代linux程序没有保护全开吗?
答案是真的有,虽然直接apt install nodejs的开了PIE,但nodejs官方提供的版本居然是没开的。这里直接给出原作者测试的版本。
https://nodejs.org/dist/v22.9.0/node-v22.9.0-linux-x64.tar.xz
对比两者。
然后就可以拿原作者的环境测试了。
https://github.com/JorianWoltjer/nodejs-file-write-rce
注意换源。
npm get registry
npm set registry https://registry.npmmirror.com/
npm install
node-v22.9.0-linux-x64/bin/node main.js
简单查询pid并且向/proc/self/fd/15写入多个A引发程序崩溃
ps aux | grep node
echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA >> /proc/3196/fd/15
gdb进去看一眼原因。可以发现,AAAA成功写到栈上,并且取r12+0x68地址上值时报错,也就是A*8必须为PIVOT_GADGET。
那么使用作者的poc,并在报错处下断点。
可以看到我们写在栈上的正是POC中的。
PIVOT_GADGET = 0x4354c41 # -> 0x012d0000
SIGNUM = 0x4032d00 # must be equal to dword after PIVOT_GADGET
经过汇编mov esi, dword ptr [r12 + 0x68],esi=0x4032d00
下一行cmp比较dword ptr [rbp + r15 - 0x228],其实就是栈上第二行0x4032d00,和esi相等才不跳走。这也正是之前描述的signum校验。
然后再n几次就来到call qword ptr [r12 + 0x60]了,可以发现正是uv_signal_s结构体的要求,那么signal_cb=0x12d0000
0x12d0000的代码非常简单,就是3个pop一个ret,其目的只是为了消栈。
消完了栈,自然而然走到pop rdi;ret的gadget上来。
后面的就可以直接看poc,看作者用什么方式RCE的。
1,找到一块可读可写的地址RW_SECTION,依次向其+100,+200,+300写入/bin/sh -c xxxxx命令。
2,将RW_SECTION+100,RW_SECTION+200,RW_SECTION+300的地址,再写回RW_SECTION,RW_SECTION+8,RW_SECTION+16,那么RW_SECTION就是一个指针数组。
3,rax=0x3b,rdi=/bin/sh,rsi={bin/sh, -c, xxxx},rdx=0,最后syscall,显然系统调用执行execve。
还是非常简单非常亲民的POC了。但是每个版本的node都需要独立去找gadget,其他的还好说,PIVOT_GADGET找起来很有难度。它需要先将全部pop_ret链大于等于3次以上地址找出来,再去找哪些地址上写了这些地址,非常麻烦。
介于这个漏洞很可能在CTF上碰到,CTF赛棍可以提前准备脚本了。