针对Tenda AC15路由器固件重托管和远程命令执行(CVE-2020-10987)漏洞利用复现的编写指南。
这篇说明详细展示了我如何仿真AC15 (V15.03.05.19)固件的Web服务器,使用QEMU,使其可以从主机浏览器访问,并利用存在漏洞的/goform/setUsbUnload处理程序(CVE-2020-10987)在仿真的根文件系统中执行命令。
squashfs文件系统并开始逆向分析首先我们需要获取固件镜像。我未能找到AC15 V15.03.05.19的固件镜像下载链接,但我找到了一个GitHub仓库,其中包含了已提取的squashfs文件系统。
如果我们有正确的镜像文件,我们可以使用binwalk像这样提取它,```
user@computer $ binwalk -e AC15_V15.03.05.19.bin
64 0x40 TRX firmware header, little endian, image size: 6778880 bytes, CRC32: 0x80AD82D6, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1A488C, rootfs offset: 0x0 92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4177792 bytes 1722572 0x1A48CC Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5052332 bytes, 848 inodes, blocksize: 131072 bytes, created: 2017-04-19 16:18:08
user@computer $ cd _AC15_V15.03.05.19.bin.extracted
由于我们没有正确的镜像文件,但有一个已经提取了文件系统的仓库,我们可以直接克隆该仓库。```
git clone https://github.com/lapinpt/Tenda-AC15-Firmware-V15.03.05.19-9061
我创建了一个名为 VR 的目录,并将此仓库克隆到其中。我的 rootfs 位于 $HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs
很好,现在我们有了 AC15 V15.03.05.19 固件。接下来进行一些逆向分析,看看情况如何。
我将使用 Ghidra 11.4.2 进行逆向分析。
让我们导航到目标二进制文件 rootfs/bin/httpd,并将其加载到 Ghidra 中。
如果我们转到 formsetUsbUnload 函数,可以看到,```C
uVar1 = FUN_0002bd4c(param_1,"deviceName",&DAT_000f4bdc);
doSystemCmd("cfm post netctrl %d?op=%d,string_info=%s",0x33,3,uVar1);
FUN_0002c6cc(param_1,"HTTP/1.0 200 OK\r\n\r\n");
FUN_0002c6cc(param_1,"{"errCode":0}");
FUN_0002cc14(param_1,200);
return;
这是漏洞所在。`deviceName`参数被直接传递给`doSystemCmd`,从而允许我们发送任意命令。
既然我们要使用qemu重放此固件,而非原始路由器硬件,一些程序会尝试访问不存在的设备,导致我们的启动过程崩溃。
由于我们的目标是利用Web服务器(`httpd`),我只专注于重放该二进制文件,而不是整个启动过程(`/rootfs/etc_ro/init.d/rcS`)。_事后看来,我不确定这是否是正确做法_
因此,在查看`rcS`时,我想找出`rcS`可能做的、而`httpd`可能需要的内容。在文件末尾附近,我们可以看到:```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS 在其余堆栈启动之前,在后台启动 cfmd。
经过更多研究并查看易受攻击的函数如何发送命令后,我得出的结论是:
httpd 构建一个 cfm postcfm 客户端通过 UNIX 域套接字(例如 /var/cfm_socket)与 cfmd 通信在 cfmd 的 InitServer 例程中,我们可以看到```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
它创建一个 UIX 套接字并进行监听。
总体工作方式是,处理程序从每个客户端读取一个固定的 0x7e0 字节帧(`RecvMsg`/`SendMsg`)。前 4 个字节是命令代码;然后是一个 512 字节的键缓冲区和 1500 字节的值缓冲区(你可以在处理程序中看到栈对象的大小)。它根据操作码进行切换并回复一个 ACK 代码:
- `2` -> Get: `GetCfmValue(key, value)` 然后回复代码 `3`
- `0` -> Set: `SetCfmValue(key, value)` 然后回复代码 `1`
- `0x11` -> Unset: `UnSetCfmValue(key)` 然后回复代码 `0x12`
- `10` -> Commit: `SaveCfm2Flash()` 然后回复 `0x10`(成功)或 `0xB`(错误)
因此,为了模拟 `cfmd`,我创建了一个简短的脚本 `cfm_stub````C
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define SOCK_PATH "/var/cfm_socket"
// minimal UNIX-domain server that httpd expects.
// Replies with an IP string when it sees the key it asks for.
int main(void) {
int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {0};
if (s < 0) { perror("socket"); return 1; }
unlink(SOCK_PATH);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path)-1);
if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
if (listen(s, 5) < 0) { perror("listen"); return 1; }
for (;;) {
int c = accept(s, NULL, NULL);
if (c < 0) { if (errno==EINTR) continue; perror("accept"); break; }
char buf[1024]; ssize_t n = read(c, buf, sizeof(buf));
if (n > 0) {
// In some builds httpd asks for "lan.webiplansslen" etc.
// Any non-empty reply that looks like an IP keeps init happy.
const char *reply = "192.168.0.1";
write(c, reply, strlen(reply));
}
close(c);
}
close(s);
return 0;
}
我将在下一部分之后展示如何编译它。
下一个辅助文件是 hooks.so。有几个在 httpd 和 cfm 中使用的函数尝试与不存在的硬件交互。
以下是我们的程序启动时假设的内容:
/dev/nvram) 并返回合理的默认值。以下函数成为问题,因此我们必须使用 LD_PRELOAD=/hooks.so 进行修补。
get_flash_type() -> 如果返回 4,代码将采用基于文件的路径 (cfm_file_init),否则它将尝试与 MTD 通信(而我们没有 MTD)。get_cfm_blk_size_from_cache()(或其变体 j_get_cfm_blk_size_from_cache)用于咨询配置块大小。bcm_nvram_get) 的调用不得失败,否则堆栈会假设“NVRAM 已损坏”并进入恢复/重启逻辑。load_l7setting_file() 和 restore_power() 预期会成功,但它们会触及不存在的硬件/文件。这是我们的 hooks.c。感谢 azeria-labs 提供原始代码。```C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
int j_get_cfm_blk_size_from_cache(const int i) { puts("j_get_cfm_blk_size_from_cache called....\n"); return 0x20000; // 128 KiB block — what the file path expects }
int get_flash_type() { puts("get_flash_type called....\n"); return 4; // force file-backed CFM init, not MTD }
int load_l7setting_file() { puts("load_l7setting_file called....\n"); return 1; // pretend Layer-7 settings loaded OK }
int restore_power(int a, int b) { puts("restore_power called....\n"); return 0; // success (don’t touch RF/power hardware) }
char *bcm_nvram_get(char *key) { char *value = NULL;
if (strcmp(key, "et0macaddr") == 0) { value = strdup("DE:AD:BE:EF:CA:FE"); // any valid MAC works } if (strcmp(key, "sb/1/macaddr") == 0) { value = strdup("DE:AD:BE:EF:CA:FD"); } if (strcmp(key, "default_nvram") == 0) { value = strdup("default_nvram"); // signals “nvram is OK” }
printf("bcm_nvram_get(%s) == %s\n", key, value); return value; }
现在,让我们编译这些文件并将它们放入我们的固件中。
要进行交叉编译,我们可以使用 Bootlin 的预构建 uClibc 工具链。```bash
wget https://toolchains.bootlin.com/downloads/releases/toolchains/armv5-eabi/tarballs/armv5-eabi--uclibc--stable-2020.08-1.tar.bz2
tar xjf armv5-eabi--uclibc--stable-2020.08-1.tar.bz2
export PATH="$PWD/armv5-eabi--uclibc--stable-2020.08-1/bin:$PATH"
ls armv5-eabi--uclibc--stable-2020.08-1/bin | grep gcc
您应该看到类似以下内容:```bash arm-buildroot-linux-uclibcgnueabi-gcc arm-buildroot-linux-uclibcgnueabi-gcc-9.3.0 arm-buildroot-linux-uclibcgnueabi-gcc-9.3.0.br_real arm-buildroot-linux-uclibcgnueabi-gcc-ar arm-buildroot-linux-uclibcgnueabi-gcc.br_real arm-buildroot-linux-uclibcgnueabi-gcc-nm arm-buildroot-linux-uclibcgnueabi-gcc-ranlib arm-linux-gcc arm-linux-gcc-9.3.0 arm-linux-gcc-9.3.0.br_real arm-linux-gcc-ar arm-linux-gcc.br_real arm-linux-gcc-nm arm-linux-gcc-ranlib
现在我们可以用```bash
arm-buildroot-linux-uclibcgnueabi-gcc -shared -fPIC -Os -ldl -Wl,-soname,hooks.so -o hooks.so hooks.c
arm-buildroot-linux-uclibcgnueabi-gcc -Os -s -o cfm_stub cfm_stub.c
然后最后将它们安装到固件中。``` $FIRM = "$HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs" install -m 0644 ./hooks.so "$FIRM/hooks.so" install -D -m 0755 ./cfm_stub "$FIRM/usr/sbin/cfm_stub"
## 构建 ARM 客户系统
让我们使用 qemu 完整系统设置我们的 arm 客户机。
我在 `~/qsys` 创建了一个目录来存放这个客户系统。
现在在这个目录中,让我们通过以下步骤来设置我们的客户机:```bash
sudo apt-get install -y qemu-system-arm qemu-utils debootstrap qemu-user-static binfmt-support
mkdir -p ~/qsys/rootfs-armhf
sudo debootstrap --arch=armhf --foreign bookworm ~/qsys/rootfs-armhf http://deb.debian.org/debian
sudo cp /usr/bin/qemu-arm-static ~/qsys/rootfs-armhf/usr/bin/
sudo chroot ~/qsys/rootfs-armhf /debootstrap/debootstrap --second-stage
cat | sudo tee ~/qsys/rootfs-armhf/etc/apt/sources.list >/dev/null <<'EOF'
deb http://deb.debian.org/debian bookworm main
EOF
sudo chroot ~/qsys/rootfs-armhf apt-get update
sudo chroot ~/qsys/rootfs-armhf apt-get install -y \
net-tools iproute2 iputils-ping python3 busybox-syslogd openssh-server \
ifupdown curl ca-certificates