D-Link登录信息泄露漏洞复现
漏洞编号CVE-2018-7034,影响D-Link和TrendNet的一些老设备。
固件包(见附件):TEW751DR_FW103B03.bin
这里分析的是TrendNet TEW751的固件,此外D-Link的一些老设备,如DIR645,DIR815等也都受该漏洞影响。
固件分析
首先解包一下固件:

根据相关报告描述,存在漏洞的文件是 /htdocs/web/getcfg.php:

这里通过 $AUTHORIZED_GROUP 变量判断用户权限
而 $GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ","); 中,$GETCFG_SVC 是通过 POST 传入的 $_POST["SERVICES"] 所以这里文件的加载是我们可控的,也就是我们可以控制 /htdocs/webinc/getcfg/ 这个路径下的文件加载,这显然存在一个我们可以利用的地方,所以我们需要进入到 else分支 中,也就是说我们的 $AUTHORIZED_GROUP 变量应该大于等于0。
我们进入这个路径 /htdocs/webinc/getcfg/ 看看有什么文件是我们可以利用的
关注到 /htdocs/webinc/getcfg/DEVICE.ACCOUNT.xml.php 这个文件:

这段代码的作用是遍历系统路径 /device/account/entry 下的所有账户条目,并且使用echo输出,并且echo输出的条目包括用户名和密码,所以我们可通过这个加载文件泄露账号
但是,我们首先还是得先知道如何绕过全局变量 $AUTHORIZED_GROUP >= 0 的检查
我们在解包的固件目录里中使用命令 grep -ra 'AUTHORIZED_GROUP' 来寻找都有哪些文件含有控制 $AUTHORIZED_GROUP 变量的功能

主角登场!
漏洞分析
我们去逆向分析一下 cgibin 文件
由于这里的 webserver 运行的是php脚本,那么这个二进制文件中重点的就是处理 php 语言的部分,也就是 phpcgi,我们进入 phpcgi_main

进入 phpcgi_main 之后我们分析一下下面这几个函数

sobj_new() 的作用是创建一个结构体,用于存放之后程序解析出来的各个字段:

sobj_add_string(v7, *(a2 + 4)); 的作用就是将 *(a2 + 4) 存储的内容放入到 v7 这个结构体中,其实就是分配一个空间存放解析的URL字符串,然后,下面的for循环就是在读取我们的环境变量并拼接起来
下面编写shell脚本来使用qemu进行动态调试
编写start.sh脚本
1 | |
编写gdb启动脚本
1 | |
然后执行如下命令进入gdb调试:
1 | |
我们在 sobj_add_string 函数处打上一个断点随后跟进到此处,这样我们就可以看到这里的几个参数,而它的第二个参数也就是 *(a2 + 4) 显示为 phpcgi 这个字符串,也就是说在开头的 sobj_add_string(v7, *(a2 + 4)); 它将 phpcgi 这个字符串写入了 v7 这个结构体中:

然后我们在 phpcgi_main 打个断点,继续往下走,往下走就对应进入ida中的这个for循环:

我们进入for循环,看看变量 v6 的变化(下面是进入for循环但是还没有开始执行for循环的任何指令)
根据图中信息 v6 的地址应该为 0x438008 (此时v6是这个函数中的第一个参数,对应mips汇编就是a0这个寄存器)

下面我们直接跳过 for ( i = a3; *i; ++i ) 来看结果

从上面这张图中我们可以看到参数是以键值对的形式存储并且以换行符 \n 分割
而这个结构体是用于存储我们传入的数据包中的内容,如果我们构造并传入一个 AUTHORIZED_GROUP=1,这样就可以在给用我们的值去顶替掉原来的 AUTHORIZED_GROUP 赋值程序所赋的值。
我们再继续往下看:
其中 v9 获取了 REQUEST_METHOD 的值,随后判断是否使用 HEAD 或 GET 类型传参,如果是,则将 sub_405CF8 函数地址赋值给 v11,如果使用的是 POST 传参那么就会将 sub_405AC0 函数地址赋值给 v11,而显然我们使用的是 POST 传参方式,也就是说我们的 v11 是 sub_405AC0 函数地址。
1 | |
继续往下走会进入到 cgibin_parse_request 函数中,这里获取了 CONTENT_TYPE 和 CONTENT_LENGTH 两个环境变量,所以我们的 start.sh 脚本中也需要设置对应的 -E CONTENT_TYPE 和 -E CONTENT_LENGTH 这里需要注意 CONTENT_LENGTH 得和我们输入的长度一致,否则可能发生错误

再进入到 parse_uri ,可以看到开头获取了 REQUEST_URI 如果我们的 start.sh 脚本中没有设置 -E REQUEST_URL
那么后续就会导致获取失败,从而导致程序异常退出
回到 cgibin_parse_request 函数中,我们继续往下看,可以看到下面有一个 strncasecmp 要求让 content_type 和 v14 的相同才可以进入其中,否则则会执行 return -1:

我们在 strncasecmp 处下断点看到这里的 v14 是 application/,我们设置 application/ 可以顺利通过这个判断,进入到下面的 return 部分
为了验证这一点,我们在 strncasecmp 函数处下断点,观察程序比较的内容:
$a0指向"application/x-www-form-urlencoded"(我们通过环境变量设置的值)$a1指向"application/"(程序硬编码的比较字符串)$a2=12(比较前12个字符)
从调试输出可见,$a1 的值正是我们提到的 v14,即 “application/“,与我们之前的静态分析完全一致。比较通过后,程序会继续执行,进入后续的 POST 数据解析流程。

继续跟进到 sub_403A0C 函数中,发现其中有一个 read 函数,我们 POST 的请求就是使用这个 read 来读入的:

再继续进入到 sub_403864 函数:
1 | |
这个函数负责解析 URL 查询字符串(如 “name=john&age=25”),
将解析出的键值对通过回调函数返回给调用者。
通过 GDB 调试可以观察到:
- 函数在解析过程中会进入 else 分支处理键名
- 解析完成后,会调用回调函数 v9 来处理键值对
- 回调函数的入口地址是 0x405ac0
GDB 调试验证 sub_403864
- 在 0x40392c 下断
1 | |
- 查看 t9 的值
1 | |
执行 jalr t9 前,t9 指向 0x405ac0
- 单步执行
1 | |
单步后 PC = 0x405ac0,证实跳转目标
- 结论
0x40392c (jalr t9) → t9 = 0x405ac0 → 跳转到 0x405ac0
现在我们来到 0x405ac0,重点是里面的 else 分支
1 | |
这里会按照 POST 请求对输入的内容进行解析,就是找到一个 = 再将前后分离并且拼接,然后将解析好的内容拼接到上面的字符串,然后我们就可以观察到如下情况:

也就是说程序原本打算写入的是 AUTHORIZED_GROUP=-1,但是我们在调试过程中没有做任何操作仅仅是程序自己去处理,而逻辑设计上的缺陷导致它实际的值变成了 1,从而实现权限绕过。
漏洞利用演示
我们可以使用 curl -d "SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1" "http://[ip:port]/getcfg.php" 去验证。
也可以在BurpSuite中构造如下数据包:
1 | |
在 shodon 找个同型号的路由器试一试:


在浏览器访问 ip:port,访问成功会出现下面界面:

用上面的exp去打:

然后就可以成功登入了:

References
- https://xidp0.github.io/2025/06/13/D-Link%20%E7%99%BB%E5%BD%95%E4%BF%A1%E6%81%AF%E6%B3%84%E9%9C%B2(%E6%9D%83%E9%99%90%E7%BB%95%E8%BF%87)%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A(CVE-2018-7034)/
- https://www.cnblogs.com/cosyQAQ/p/19065872
- https://xz.aliyun.com/news/18232
- https://www.iotsec-zone.com/article/384
- https://www.freebuf.com/articles/wireless/165716.html