百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

为什么代码安全很重要——即使在强化环境中

csdh11 2025-01-15 12:07 30 浏览


基础设施强化使应用程序更能抵御攻击。这些措施提高了攻击者的门槛,使攻击变得更加困难。然而,它们不应被视为灵丹妙药,因为坚定的攻击者仍然可以利用源代码中的漏洞。

在这篇博文中,我们将通过展示一种技术来强调基本代码安全的重要性,攻击者可以利用该技术将 Node.js 应用程序中的文件写入漏洞转化为远程代码执行 - 即使目标的文件系统是以只读方式安装的。该技术通过利用暴露的管道文件描述符来获得代码执行,从而阻止在这种强化环境中应用的限制。

文件写入漏洞

在我们主要针对 Web 的漏洞研究中,我们遇到了各种不同类型的漏洞,例如跨站点脚本、SQL 注入、不安全反序列化、服务器端请求伪造等等。这些漏洞类型的影响和利用难易程度各不相同,但对于其中一些漏洞,一旦发现该类型的漏洞,几乎可以肯定整个应用程序都会受到影响。

这些严重漏洞类型之一是任意文件写入漏洞。攻击者仍然需要弄清楚在哪里写入什么,但通常有很多方法可以将其转变为代码执行,从而完全破坏应用程序的服务器:

  • 将 PHP、JSP、ASPX 或类似文件写入 Web 根目录。
  • 覆盖由服务器端模板引擎处理的模板文件。
  • 写入配置文件(例如,uWSG.ini 文件或Jetty.xml 文件)。
  • 添加 Python站点特定的配置挂钩。
  • 使用通用方法,通过编写 SSH 密钥、添加 cronjob 或覆盖用户的 .bashrc 文件。

这些示例表明,攻击者通常会找到一种简单的方法将任意文件写入漏洞转化为代码执行。为了降低此类漏洞的严重程度,应用程序的底层基础架构通常会得到强化 - 这使得攻击者更难利用它,但并非不可能。

强化环境中的文件写入

我们最近在 Node.js 应用程序中发现了一个任意文件写入漏洞,事实证明该漏洞不太容易被利用。该漏洞本身更为复杂,但可以分解为以下易受攻击的代码片段:

app.post('/upload', (req, res) => {
   const { filename, content } = req.body;
   fs.writeFile(filename, content, () => {
       res.json({ message: 'File uploaded!' });
   });
});

该函数fs.writeFile用于写入文件,并且两个参数 -filename和content- 都是完全可由用户控制的。因此,这是一个任意文件写入漏洞。

在确定此漏洞的影响时,我们注意到运行该应用程序的用户仅限于特定上传文件夹的写入权限。文件系统上的其他所有内容都是只读的。虽然这对漏洞的利用来说似乎是死路一条,但它引导我们提出了以下研究问题:
即使目标的文件系统以只读方式安装,任意文件写入漏洞是否可能转变为代码执行?

只读文件写入

在 Linux 等基于 Unix 的系统上,一切都是文件。与 ext4 等将数据存储在物理硬盘驱动器上的传统文件系统不同,还有其他文件系统用于不同的目的。其中之一是procfs 虚拟文件系统,它通常安装在内核中/proc,并充当内核内部工作的窗口。procfs 不存储实际文件,而是提供对正在运行的进程、系统内存、硬件配置等实时信息的访问。

procfs 提供的一条特别有趣的信息是正在运行的进程的打开文件描述符,可以通过 进行检查/proc/<pid>/fd/。进程打开的文件不仅可以是传统文件,还可以是设备文件、套接字和管道。例如,以下命令可用于列出 Node.js 进程的打开文件描述符:

user@host:~$ ls -al /proc/`pidof node`/fd
total 0
dr-x------ 2 user user 22 Oct 8 13:37 .
dr-xr-xr-x 9 user user  0 Oct 8 13:37 ..
lrwx------ 1 user user 64 Oct 8 13:37 0 -> /dev/pts/1
lrwx------ 1 user user 64 Oct 8 13:37 1 -> /dev/pts/1
lrwx------ 1 user user 64 Oct 8 13:37 2 -> /dev/pts/1
lrwx------ 1 user user 64 Oct 8 13:37 3 -> 'anon_inode:[eventpoll]'
lr-x------ 1 user user 64 Oct 8 13:37 4 -> 'pipe:[9173261]'
l-wx------ 1 user user 64 Oct 8 13:37 5 -> 'pipe:[9173261]'
lr-x------ 1 user user 64 Oct 8 13:37 6 -> 'pipe:[9173262]'
l-wx------ 1 user user 64 Oct 8 13:37 7 -> 'pipe:[9173262]'
lrwx------ 1 user user 64 Oct 8 13:37 8 -> 'anon_inode:[eventfd]'
lrwx------ 1 user user 64 Oct 8 13:37 9 -> 'anon_inode:[eventpoll]'
...

从上面的输出中我们可以看出,这还包括匿名管道(例如pipe:[9173261])。与在文件系统上以命名文件形式公开的命名管道不同,由于缺少引用,通常无法写入匿名管道。但是,procfs 文件系统允许我们通过 中的条目引用管道/proc/<pid>/fd/。与 procfs 下的其他文件相比,此文件写入不需要 root 权限,并且可以由运行 Node.js 应用程序的低权限用户执行:

user@host:~$ echo hello > /proc/`pidof node`/fd/5

如果 procfs 以只读方式挂载(例如在 Docker 容器中),甚至可以写入管道,因为管道由名为 的单独文件系统处理pipefs,该文件系统由内核内部使用。

这为可以写入任意文件的攻击者揭示了新的攻击面,因为他们可以将数据提供给从匿名管道读取的事件处理程序。

Node.js 和管道

Node.js 建立在 V8 JavaScript 引擎上,它是单线程的。但是,Node.js 提供了异步和非阻塞事件循环。为此,它使用了一个名为libuv 的库。该库使用匿名管道来发送信号和处理事件,这些事件通过 procfs 公开,如我们在上面的输出中看到的那样。

当 Node.js 应用程序容易受到文件写入漏洞的影响时,没有什么可以阻止攻击者写入这些管道,因为它们可以由运行该应用程序的同一用户写入。但是写入管道的数据会发生什么?

在审计相关的 libuv 源代码时,一个名为的处理程序uv__signal_event引起了我们的注意。它假定从管道读取的数据是以下类型的消息uv__signal_msg_t:

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);
      // [...]

该uv__signal_msg_t数据结构仅包含两个成员,一个handle指针和一个整数signum:

typedef struct {
  uv_signal_t* handle;
  int signum;
} uv__signal_msg_t;

uv_signal_t指针的类型是数据结构handle的 typedef uv_signal_s,它包含一个特别有趣的成员signal_cb:

struct uv_signal_s {
  UV_HANDLE_FIELDS
  uv_signal_cb signal_cb;
  int signum;
  // [...]

此signal_cb成员是一个函数指针,它应该包含一个回调函数的地址,如果signum两个数据结构的值匹配,则稍后在事件处理程序中调用该回调函数:

      // [...]
      handle = msg->handle;

      if (msg->signum == handle->signum) {
        assert(!(handle->flags & UV_HANDLE_CLOSING));
        handle->signal_cb(handle, handle->signum);
      }

下图显示了事件处理程序所需的数据结构:

对于攻击者来说,这是一个非常有利的情况:他们可以将任何数据写入管道,并且有一条快速路径可以调用函数指针。事实上,我们并不是唯一和第一批注意到这一点的研究人员。8 月 8 日,HackerOne 披露了Seunghyun Lee的这份精彩报告,他在报告中描述了一种不同的场景,他能够利用 Node.js 程序中的打开文件描述符来绕过任何基于模块和进程的权限——基本上就是沙盒逃逸。

即使在他在此处描述的场景中(我们没有想到),这也不算是安全漏洞,并且该报告已作为信息性报告关闭。这意味着我们在以下部分中描述的技术仍然适用于最新版本的 Node.js,并且这在不久的将来可能不会改变。

建筑结构

攻击者利用具有文件写入漏洞的事件处理程序的一般策略可能如下所示:

  • 将虚假的uv_signal_s数据结构写入管道。
  • 将函数指针设置signal_cb为他们想要调用的任意地址。
  • 将虚假的uv__signal_msg_t数据结构写入管道。
  • 将指针设置handle为uv_signal_s指向之前写入的数据结构。
  • 将两个数据结构的值设置signum为相同的值。
  • 获得任意代码执行。

假设攻击者只能写入文件,那么所有这些都需要通过一次性写入来实现,而无法事先读取任何内存。

事件处理程序的缓冲区非常大,这使得攻击者可以轻松地将两个数据结构写入管道。然而,有一个障碍:由于写入管道的所有数据都存储在堆栈中,因此数据结构的地址是未知的:

因此,攻击者无法让handle指针引用伪造的 uv_signal_s数据结构。这引出了一个问题:攻击者可以引用的数据到底有没有?

堆栈、堆和所有库的地址都通过 ASLR 随机化。但是,Node.js 二进制文件本身的段却不是。令我们惊讶的是,Node.js 的官方 Linux 版本未启用 PIE(位置无关的可执行文件):

user@host:~$ checksec /opt/node-v22.9.0-linux-x64/bin/node 
[*] '/opt/node-v22.9.0-linux-x64/bin/node'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

这样做的原因显然是出于性能方面的考虑,因为 PIE 的间接寻址会增加一点开销。对于攻击者来说,这意味着他们可以引用 Node.js 段中的数据,因为这个地址是已知的:

下一个问题是:攻击者如何uv_signal_s在 Node.js 段中存储虚假的数据结构?寻找让 Node.js 将攻击者控制的数据存储在静态位置(例如从 HTTP 请求读取的数据)的方法是一种方法,但这似乎相当具有挑战性。

一种更简单的方法是使用已有的内容。通过检查 Node.js 内存段,攻击者可能能够uv_signal_s在现有数据中识别出适合伪造结构的数据。

攻击者梦想的数据结构看起来类似于此:

此数据结构以命令字符串 ( ) 开头,后跟正确偏移处"touch /tmp/pwned"的 的地址,以便与函数指针重叠。攻击者只需使值与伪造的数据结构匹配,即可调用回调函数,从而有效地调用。systemsignal_cbsignumuv_signal_ssystem("touch /tmp/pwned")

这种方法要求地址system存在于 Node.js 段中。全局偏移表 (GOT) 通常是候选。但是,Node.js 不使用该system函数,因此其地址不存在于 GOT 中。即使它存在,生成的伪uv_signal_s数据结构的开头也可能是 GOT 中的另一个条目,而不是有用的命令字符串。因此,另一种方法似乎更可行:经典的 ROP 链。

搜索数据结构小工具

每个 ROP 链的开始都是搜索有用的 ROP 小工具。搜索 ROP 小工具的工具通常会解析磁盘上的 ELF 文件,然后确定所有可执行部分。该.text部分通常是最大的可执行部分,因为它存储了程序本身的指令:

现在,该工具会遍历此部分中的字节并查找一条ret指令,例如,因为这是 ROP 小工具的合适最后一条指令。然后,该工具从代表该ret指令的字节开始逐字节返回,以确定所有可能有用的 ROP 小工具:

但在本例中,这并不是攻击者所需要的。他们不需要 ROP 小工具,而需要一个引用虚假uv_signal_s数据结构的地址,该地址通过其signal_cb函数指针引用 ROP 小工具。因此,存在一种间接方式:ROP 小工具(指令序列的地址)需要存储在引用的数据本身中:

为了识别此类合适的数据结构,攻击者需要搜索 Node.js 镜像,类似于经典的 ROP 小工具查找工具。但不同之处在于,攻击者不仅对可执行部分(如.text部分)感兴趣。伪造数据结构所在的内存不必是可执行的。攻击者需要指向小工具的指针。因此,他们可以考虑所有至少可读的段。此外,此搜索可以在内存中完成,而不仅仅是解析磁盘上的 ELF 文件。这样,攻击者还可以找到仅在运行时在.bss部分中创建的数据结构。这可能会导致误报或特定于环境的结构,但增加了他们获得有用发现的机会,这些发现可以手动验证。

这种内存中搜索虚假数据结构的基本实现实际上非常简单:

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 兼容小工具并不困难。但是,这种限制进一步限制了uv_signal_s现有数据中可能适合伪造的数据结构。基于此,需要在脚本中添加额外的检查,以验证伪造数据结构的基地址是否为有效的 UTF-8:

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)
       # [...]

即使进行了这项额外检查,脚本仍然会产生合适的虚假数据结构,引用如下所示的枢轴小工具:

...
0x4354ca1 -> 0x12d0000: pop rsi; pop r15; pop rbp; ret  
...

相关数据结构在内存中的样子如下:

这个伪造数据结构 ( ) 的基地址0x4354c41是有效的 UTF-8,因此可以正确填充数据结构handle中的指针uv__signal_msg_t。但是,还有另一个与 UTF-8 相关的问题。这次的值是signum:

signum 值的最后一个字节是0xf0,这不是有效的 UTF-8。如果攻击者试图通过文件写入漏洞写入此字节,它将被替换字符替换,并且signum值检查失败。如果我们0xf0在UTF-8 可视化工具中输入,我们可以看到这个字节引入了一个 4 字节的 UTF-8 序列:

因此,UTF-8 解析器需要在此字节后有 3 个连续字节。由于uv__signal_msg_t数据结构包含一个 8 字节指针和一个 4 字节整数,因此编译器会添加 4 个额外的填充字节,以使结构对齐到 16 个字节。这些字节可用于添加 3 个连续字节,从而制作有效的 UTF-8 序列:

例如,上面的软盘是一个有效的 4 字节 UTF-8 序列,以 开头0xf0。通过添加这些连续字节,攻击者可以满足整个有效载荷为有效 UTF-8 的要求,并使两个signum值匹配:

扫清这最后的障碍后,攻击者就能够实现远程代码执行。

经验与结论

基于 Unix 的系统上的“一切皆文件”理念在利用文件写入漏洞时会打开不常见的攻击面。在这篇博文中,我们展示了一种可用于将 Node.js 应用程序中的文件写入漏洞转变为远程代码执行的技术。由于事件处理程序代码来自libuv,因此该技术也可以应用于使用 libuv 的其他软件,例如julia。

即使没有 Node.js 和 libuv,通用方法也适用。每当应用程序使用管道作为通信机制时,攻击者可能会利用文件写入漏洞来攻击通过 procfs 公开的管道文件描述符。正如此示例所示,这可能不会被考虑在常见的威胁模型中,但可以让远程攻击者能够执行任意代码。

从防御角度来看,此示例强调了基础设施强化只能被视为额外的防御层,无法取代基本的代码安全性。即使已经采用了强化措施,坚定的攻击者仍可以利用源代码中的漏洞。这充分说明了为什么Clean Code所暗示的代码安全性如此重要,以及为什么应该在漏洞的源头(源代码)上修复漏洞。

相关推荐

当iPhone X遇上 Mate 10 Pro 怎么挑?

产品:Mate10Pro(全网通)华为手机1iPhoneX一出就遇上了对手Hello!大家好我是石头这里是zol。首先来看看我手里的这个,不是装x的意思,今天好多人欢天喜地的拿到了属于自己或者属...

谁是办公高手? 六款商务平板全推荐

1超低功耗联想Thinkpad10领衔ThinkPad的名字在笔记本上依然是高曝光率,从IBM到联想,它一直以来是商务、办公领域的不二人选,然而在平板电脑大行其道的今天,ThinkPad当然也不单...

如何仅用几行代码将微信4.0公测版转换成玲珑格式?

微信4.0(玲珑版)已上架如意玲珑应用商店!!!...

音乐研发必备:理解 MIDI 协议与标准 MIDI 文件格式

1.MIDI简介...

HEIF格式可以节约50%的空间!质量会有影响吗?

关于手机1亿像素到底有没有必要,争议肯定是存在的,但无法回避的一点是随着像素的成倍增加,一张照片十几MB成了常事,这必将给本就不太宽裕的存储容量造成压力,这种压力又会转嫁到用户身上,让他们不得不花更多...

「干货分享」30个前端知识技能提升的资源网站

今天给小伙伴们分享30个前端相关学习资源网站及一些在线小工具,希望能帮助到大家。CSS相关1、css精灵牛|在线图片CSS生成工具。...

用Facebook引流Shopify独立站,如何优化Facebook商户页面?

什么是FacebookBusinessPage?FacebookBusinessPage是官方的称号,其实你可以简单理解为facebookbusinesspage就是微信公众账户,而我们...

玩转手机摄影 nubia Z9 mini新品首测

1Z9系列新品nubiaZ9mini发布会开始之前的邀请函总能透露出很多内容,或者说我们总会特意解读出很多内容。nubia此前发出的3月26日智能手机新品发布会的邀请函很显然再一次着重强调了其产品...

影院看片怕座位不好?那买个盒子在家看

着消费者对高清播放器的需求的不断增加,高清播放器的功能也在不断得到提升。以前看电影的时候相信很多人更喜欢去电影院里观看,但是有时候去电影院的人太多买不到好位子也会让不少人感到扫兴。现在随着技术的革新,...

今天你买了么?不入后悔的8款平板推荐

随着高考大战的落幕,对于广大学生朋友们来说正真的暑假终于来了!这个假期想要外出旅行或是居家畅玩,没有一款便携式全能娱乐利器的平板电脑怎么能叫放假呢?正直京东618购物季即将到来的日子,相信你也不会错过...

Cursor使用指南:释放AI编程的无限潜能

Cursor简介...

谁说轻薄本缺乏生产力?没体验过别乱说

笔记本电脑轻薄化一直是各大厂商们不断努力的方向,不过早前的轻薄笔记本电脑会受制于技术方面的限制,无法将高能耗的CPU以及显卡塞入轻薄的机身内。为了能给这些芯片散热、供电,制造商们不得不对这些芯片的功耗...

通告ImageMagick再爆核心漏洞 站长小心

目前所有版本的GraphicsMagick和ImageMagick都支持打开文件,当文件名的第一个字符为“|”,则文件名会被传递给shell程序执行,导致(可能远程)代码执行。文件打开操作位于源代码文...

《玻璃之花与坏掉的世界》再曝新宣传片

雨文冰璃发表于昨天22:56【新闻】《玻璃之花与坏掉的世界》再曝新宣传片http://comic.qq.com/a/20151122/016784.htmPONYCANYON和A-1Pictu...

7 招教你轻松搭建以图搜图系统

作者|小龙责编|胡巍巍当您听到“以图搜图”时,是否首先想到了百度、Google等搜索引擎的以图搜图功能呢?事实上,您完全可以搭建一个属于自己的以图搜图系统:自己建立图片库;自己选择一张图片到...