任意文件写入漏洞
在开发过程中,如果没有对用户输入进行足够的限制或校验,很容易引入任意文件写入漏洞。这个问题通常出现在允许用户上传文件或提交内容并将其写入服务器时。如果开发者直接使用用户提供的文件名或路径,且没有做安全检查,攻击者就能利用这个漏洞指定文件的保存位置和内容。
以下是常见的存在任意文件写入漏洞的 Web 语言示例代码:
php
<?php
if (isset($_POST['filename']) && isset($_POST['content'])) {
$filename = $_POST['filename'];
$content = $_POST['content'];
file_put_contents($filename, $content);
echo "File written!";
} else {
echo "No file data provided.";
}
?>
python flask
@app.route('/upload', methods=['POST'])
def upload_file():
filename = request.form.get('filename')
content = request.form.get('content')
with open(filename, 'w') as file:
file.write(content)
return 'File written successfully!'
node.js
app.post('/upload', (req, res) => {
const { filename, content } = req.body;
fs.writeFile(filename, content, (err) => {
if (err) {
return res.status(500).send('File write failed.');
}
res.send('File written successfully!');
});
});
这些示例都是由于没有验证用户提供的 filename
和 content
就直接写入文件而导致的任意文件写入漏洞,使得用户可以任意控制文件位置和文件内容。
实际环境中的任意写入漏洞的成因和实现可能更为复杂,需要相当一系列前置操作才能够实现和达成,但其效果基本与上述代码的实现效果一致。
可写环境下任意写到RCE
在没有启用系统级强化安全或直接使用 root 权限运行 web 服务时,将会导致该漏洞可以不受权限限制,在文件系统的几乎任何位置创建或覆盖文件。这将导致攻击者可以轻松利用各种手段将任意写转化为RCE,常见的手段包括:
-
写入服务器运行的 web 服务的 webshell 或恶意脚本
-
覆盖服务器运行的 web 服务渲染所用的模板文件
-
写入关键的系统文件(
/etc/passwd
、.bashrc
) -
修改系统定时任务(cronjob)
-
写入 ssh 密钥文件(
.ssh/.authorized_keys
) -
...
探索只读环境下的写入目标
在启用了系统级强化安全(只读文件系统)或严格控制了运行 web 服务的用户的权限的情况下,任意文件写入漏洞能够写入的文件非常有限,往往限制在指定的 uploads 等文件夹下,由于这些文件夹下的文件通常不会被解析处理,所以在没有其他漏洞配合的情况下难以实现从任意写到RCE的转变。
在 Linux 系统中,procfs(进程文件系统)是一个虚拟文件系统,挂载在 /proc 目录下,它并不实际存储数据,而是动态地提供关于系统和进程的实时信息。
procfs 不依赖于磁盘存储,procfs 中 /proc/[pid]/fd
目录下列出的文件描述符,并不是实际的文件,而是代表了进程打开的各种资源的符号链接。这些资源可能包括匿名管道、网络套接字等,它们本身是内存中的数据结构,不存储在硬盘上。因此即使存在只读文件系统限制,这些特定的文件描述符仍然可以进行写操作,因为它们操作的是内存中的数据。
同时,由于在运行程序时程序自身需要以运行者身份权限实时动态写入这些特定的文件描述符才能保证程序的正常运行,所以严格意义上也无法控制对这些特定的文件描述符的写入权限。
实现只读环境下的任意写到RCE
libuv 是一个跨平台的异步I/O库,提供高效的事件循环和非阻塞I/O操作。因其跨平台和高性能的特性,它被广泛应用于多个项目和语言中,包括Node.js的核心异步I/O引擎,Python的pyuv库、Julia语言、基于Lua的Luvit框架、C++的异步服务器、以及Rust中的一些异步运行时。此外,libuv还被用于构建高并发网络服务、系统工具和后台任务处理程序等。
原理分析
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);
/* ... */
libuv 的事件处理器 uv__signal_event
会从管道中读取特定类型的消息结构 uv__signal_msg_t
,该结构体的定义如下:
typedef struct {
uv_signal_t* handle;
int signum;
} uv__signal_msg_t;
结构体中包含两个成员:
-
一个指向信号处理句柄(类型为
uv_signal_s
)的指针handle
-
一个信号标识符
signum
其中 uv_signal_s
结构体定义如下:
struct uv_signal_s {
UV_HANDLE_FIELDS
uv_signal_cb signal_cb;
int signum;
UV_SIGNAL_PRIVATE_FIELDS
};
该结构体中的 signal_cb
成员是一个函数指针,指向一个回调函数的地址
当事件循环读取消息时,如果两个结构体中的 signum
匹配,则会调用 handle->signal_cb
,即调用存储在句柄中的回调函数地址:
static void uv__signal_event(uv_loop_t* loop,
uv__io_t* w,
unsigned int events) {
/* ... */
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);
}
/* ... */
通过在地址和数据固定的内存段中寻找合适的可以解析为 uv_signal_s
结构体的数据,我们可以控制其中 signal_cb
指向的地址。接着,构造特定的 uv__signal_msg_t
使其 handle 指向该 uv_signal_s
,并将该 uv__signal_msg_t
通过任意文件写入漏洞写入 procfs 暴露的管道文件描述符。最后,libuv 的事件处理器 uv__signal_event
将会处理这条消息,并尝试调用 signal_cb
指向的地址,从而实现只读环境下的任意写到RCE。
利用条件
-
可获得目标程序的 ELF 可执行文件
-
在目标程序中可以通过构造 ROP 链等方法,在调用某入口地址后实现RCE
-
目标程序未开启 PIE 保护且在所有可读的内存段中存在可以被利用的数据段(可以被解析为
uv_signal_s
结构体且其中的signal_cb
值为条件2中的入口地址) -
目标程序的任意文件写入漏洞触发函数可以写入任意二进制数据,或者可以通过某种方式绕过对写入数据格式的限制
实现过程
-
寻找RCE方法和入口地址
在目标程序中通过构造 ROP 链等方法,使得在调用某入口地址后可以实现RCE,并确定该方法的入口地址entry_address
。 -
分析可读内存段寻找可被利用的数据段
-
扫描目标程序的所有可读内存段(不必一定是可执行段),寻找可以被解析为
uv_signal_s
结构体且其中的signal_cb
值为入口地址entry_address
的数据段(在较大的二进制文件中很容易找到)。由于目标程序未开启 PIE 保护,该数据段的地址在每次运行时应当是固定的,所以将其地址作为uv_signal_s
的地址。 -
该扫描过程应在内存中完成而不仅仅只是基于ELF文件进行扫描,这样扫描就可以包括仅在程序运行时在.bss段中创建的数据结构等内存数据。
-
触发事件循环
构造uv__signal_msg_t
,将其 handle 指针设置为寻找到的满足条件的uv_signal_s
的地址,并设置signum
值与其中的signum
相匹配。在利用任意文件写入漏洞将其写入 procfs 暴露的管道文件描述符后,libuv 的事件处理器uv__signal_event
会处理这条消息,并尝试调用signal_cb
指向的地址,进而实现RCE。
更广泛的攻击面
即使目标环境中不存在 libuv,在只读环境下对于 procfs 文件系统的管道文件描述符的写入方法也应当适用。每当应用程序使用管道作为通信机制时,攻击者都有可能可以利用任意文件写入漏洞来攻击通过 procfs 公开的管道文件描述符从而将任意写转变为RCE。
转自:https://xz.aliyun.com/t/15841?u_atoken=a979dabeec1be62113eff4fff669c025&u_asig=1a0c399717289617614363090e0030
原文始发于微信公众号(船山信安):利用 procfs 实现只读环境下的任意写到RCE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论