GL-iNet 路由器 CVE-2024-39226 漏洞分析

前言

发现了一款以OpenWrt操作系统为基础的路由器漏洞,简单分析一下了解该系统该如何挖掘漏洞

8月5日网上披露了 [CVE-2024-399226](https://github.com/gl-inet/CVE-issues/blob/main/4.0.0/s2s interface shell injection.md) [1],影响多款 GL-iNet 路由器。

产品介绍

GL.iNet 是一家专注于智能路由器和网络设备开发的科技公司。成立于 2009 年,总部位于中国,该公司的产品以 OpenWrt 操作系统为基础,提供高度的可定制性和灵活性。公司致力于为家庭、企业以及工业物联网环境提供可靠的网络解决方案。GL.iNet 的设备以其开源特性、强大的功能和优秀的用户体验而受到开发者、网络安全专家和高级用户的青睐。

OpenWrt 是一个基于 Linux 的开源嵌入式操作系统,专为网络设备(如路由器、网关和接入点)设计。与传统的路由器固件不同,OpenWrt 不是单一的、不可变的固件,而是一个完整且可扩展的操作系统,允许自定义以适应任何应用程序。

OpenResty 是一个基于 Nginx的高性能 Web 平台,它将 Lua 脚本引擎嵌入到 Nginx 中,使开发者可以通过 Lua 脚本编写高度可定制的 Web 服务,用来处理复杂的 web 逻辑和 API 请求。OpenResty 通常用于高并发、低延迟的 Web 应用程序开发,特别是在需要处理复杂逻辑或与外部服务交互时。

这种组合使得 GL.iNet 路由器不仅仅是一个网络设备,还可以作为一个小型的 Web 服务器或应用平台。

环境搭建

固件提取

GL.iNet 官网提供历史固件下载[2]。

1
固件版本:GL-AX1800 Flint 4.5.16

sysupgrade-glinet_ax1800 文件夹下存在 root 文件。

1
2
$ file root 
root: Squashfs filesystem, little endian, version 4.0, 44613986 bytes, 4754 inodes, blocksize: 262144 bytes, created: Thu Mar 21 13:28:00 2024

使用 binwalk,从 root 中提取 Squashfs 文件系统。

1
$ binwalk -Me root

查看 bin/busybox 得知是 32位arm 架构。

1
2
$ file squashfs-root/bin/busybox 
squashfs-root/bin/busybox: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-arm.so.1, stripped

QEMU模拟

使用 qemu-system-arm 从系统角度进行模拟,此时需要一个 arm 架构的内核镜像和文件系统,可以在这个网站下载[3]。

1
2
3
vmlinuz-3.2.0-4-vexpress  linux内核镜像文件
initrd.img-3.2.0-4-vexpress RAM磁盘映像文件
debian_wheezy_armhf_standard.qcow2 虚拟磁盘映像文件

启动虚拟环境。

1
$ sudo qemu-system-arm -M vexpress-a9 -cpu cortex-a15 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append "root=/dev/mmcblk0p2" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic

//默认可以不指定 cpu 模型,我在模拟过程中遇到报错所以指定了 cpu。

1
Illegal instruction

启动后用户名和密码都是 root 即可登录模拟的系统。

接下来在宿主机创建一个网卡,使 qemu 内能和宿主机通信。

宿主机安装依赖。

1
$ sudo apt-get install bridge-utils uml-utilities

将如下代码保存为 net.sh 并运行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1

# Reset iptables
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -P FORWARD ACCEPT

# Set up NAT
sudo iptables -t nat -A POSTROUTING -o ens33 -j MASQUERADE

# Accept traffic on tap0
sudo iptables -I FORWARD -i tap0 -j ACCEPT
sudo iptables -I FORWARD -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT

# Create and configure tap0
sudo ip tuntap add dev tap0 mode tap
sudo ifconfig tap0 192.168.100.254 netmask 255.255.255.0 up

然后配置 qemu 虚拟系统的路由,在 qemu 虚拟系统中运行 net.sh 并运行。

1
2
3
#!/bin/sh
ifconfig eth0 192.168.100.2 netmask 255.255.255.0
route add default gw 192.168.100.254

//虚拟系统可能没有 vimnano ,使用 echo 一行一行写。

这样宿主机和模拟环境互通,使用 scpsquashfs-root 文件夹上传到 qemu 系统中的 /root 路径下。

1
scp -r squashfs-root/ root@192.168.100.2:/root

然后挂载 procdev ,最后 chroot 即可。

1
2
3
4
5
6
7
8
9
10
root@debian-armhf:~# mount -t proc /proc ./squashfs-root/proc
root@debian-armhf:~# mount -o bind /dev ./squashfs-root/dev
root@debian-armhf:~# chroot ./squashfs-root/ sh


BusyBox v1.33.2 (2024-03-21 13:28:00 UTC) built-in shell (ash)

/ # ls
bin etc lib overlay rom sbin tmp var
dev init mnt proc root sys usr www

启动Nginx

启动 web 服务,前文已经介绍过 GL.iNet 路由器利用 OpenResty 来增强其 web 管理界面和 API 的功能。而 OpenResty 是基于 Nginxweb 平台,内置 Lua 脚本支持,所以首先启动 Nginx 服务。

尝试运行 /etc/init.dnginx 脚本(/etc/init.d 目录通常包含系统启动和管理各种服务的脚本,如果需要启动某个服务,通常可以在该目录中找到相应的脚本)。

查看 /etc/init.d/nginx 如何手动启动 nginx

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
/ # cat /etc/init.d/nginx 
#!/bin/sh /etc/rc.common
# Copyright (C) 2015 OpenWrt.org

START=80

USE_PROCD=1

start_service() {
[ -f /etc/init.d/uhttpd ] && {
/etc/init.d/uhttpd enabled && {
/etc/init.d/uhttpd stop
/etc/init.d/uhttpd disable
}
}

[ -d /var/log/nginx ] || mkdir -p /var/log/nginx
[ -d /var/lib/nginx ] || mkdir -p /var/lib/nginx

procd_open_instance
procd_set_param command /usr/sbin/nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
procd_set_param file /etc/nginx/nginx.conf
procd_set_param respawn
procd_close_instance
}

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/c0c659c1-f8c4-4cca-8915-0584d5657199.png)

创建缺少的文件夹再次启动 nginx

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/1e141b7f-d195-4e6b-83eb-4da3d9a78012.png)

看样子 nginx 好像起来了,访问 web 却是 404

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/232e6680-9af2-42e2-a379-e660ef6864cf.png)

这个时候已经没什么头绪了,find 一下所有 nginx 相关文件试试。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/c203597d-e81d-465e-bdb9-85d393a6e338.png)

每个文件都看看,发现 /etc/uci-defaults/80_nginx-oui 脚本。

/etc/uci-defaults/80_nginx-oui 脚本的主要作用是配置和调整Nginx的相关文件,确保Web服务能够正常运行。

尝试运行 /etc/uci-defaults/80_nginx-oui 看看是否能修复 404 问题。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/3d8f2a51-9cc5-4a67-b995-aec4a79a9796.png)

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/3fc8b359-c53c-4c69-a07c-f8abecbca32f.png)

漏洞复现

搭建环境成功,接下来尝试漏洞复现,先看一下披露的 PoC

1
curl -H 'glinet: 1' 127.0.0.1/rpc -d '{"method":"call", "params":["", "s2s", "enable_echo_server", {"port": "7 $(touch /root/test)"}]}'

PoC 来看好像只能通过 127.0.0.1 利用,先使用 192.168.100.2 试试。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/7aa9e233-ae33-432f-95f1-a93b0cb24d3a.png)

拒绝访问,再使用 127.0.0.1 (之前的会话因为启动 nginx ,需要 ssh 再创建一个会话)。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/e7997916-f403-4c54-b071-6cef5b6e413b.png)

这次报错为内部错误。继续找问题。根据 PoC 可知请求的路径是rpc ,在 /etc/nginx 下的 nginx 配置文件中查找 rpc 相关信息。

/etc/nginx/conf.d/gl.conf 找到请求 rpc 路径的处理方法。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/91301f40-cf7e-4391-b1f2-f77e82e10e5c.png)

跟进到 /usr/share/gl-ngx/oui-rpc.lua

代码来看 /usr/share/gl-ngx/oui-rpc.lua 是处理 HTTP POST 请求 jSON-RPC 调用的。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/ecde0593-31ef-4546-9f9b-5ffc889ee5f2.png)

以上代码实现模块导入、请求方式验证和读取请求体。

要注意 ubus 服务是 OpenWrt 系统中一个进程间通信框架,需要启动。

HTTP 请求仅允许 POST ,拒绝其他方式访问。

/usr/share/gl-ngx/oui-rpc.lua 定义了多个处理函数,每个函数对应不同的 rpc 方法(因为 PoC 通过 call 方法调用 s2s.enable_echo_server 进行攻击,所以只截取 rpc_method_call 方法代码)。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/444d2d21-ca8d-4d52-a9a6-aff746e0883e.png)

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/bc8f0595-1316-4fdd-95f0-9b1e7eb81105.png)

rpc_method_call 进行参数校验、会话检查和 Ubus 调用:

  1. 确保 params 中至少三个元素且元素类型正确。
  2. 检查 sid 是否有效,并通过 rpc.access 验证访问权限。
  3. 如果上述判断均通过,使用 rpc.call 执行指定的 Ubus 对象和方法。

继续跟进 /usr/lib/lua/oui/rpc.lua 查看 rpc.accessrpc.call 实现。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/a530ac44-df4a-4b7b-bbe6-a635c95def2d.png)

access 通过 is_local 判断是否本地请求。对于本地请求和 glinet 标头的请求,总是允许访问(确定了只能本地利用)。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/263fdef9-01ff-453c-8abd-3ac988c64582.png)

M.call 函数是核心的 rpc 调用处理器,执行以下步骤:

  1. 检查请求的对象是否已加载,如果未加载,则尝试从 /usr/lib/oui-httpd/rpc/ 目录下加载脚本文件。
  2. 如果脚本文件存在且加载成功,将对象的方法注册到 objects 表中。
  3. 如果无法从 /usr/lib/oui-httpd/rpc/ 目录下加载脚本文件或者找不到对象或方法,则调用 glc_call 执行。

查看 /usr/lib/oui-httpd/rpc/ 目录下是二进制 s2s.so 文件,无法直接加载,则通过 glc_call 调用 /cgi-bin/glc 执行 C 程序实现的 RPC 方法。

继续跟进 /www/cgi-bin/glc 文件。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/179624f5-f829-448a-a941-9b65c4bd2e04.png)

大致实现逻辑与 /usr/share/gl-ngx/oui-rpc.lua 类似,请求方式验证和读取请求体后动态加载并调用函数,区别在于 /www/cgi-bin/glc 使用 dlopen 动态加载对应的共享库(.so 文件)。

如此看来 PoC 满足/usr/share/gl-ngx/oui-rpc.lua/usr/lib/lua/oui/rpc.lua 两段代码逻辑。

1
curl -H 'glinet: 1' 127.0.0.1/rpc -d '{"method":"call", "params":["", "s2s", "enable_echo_server", {"port": "7 $(touch /root/test)"}]}'

请求发送一个 POST 请求到 rpc 路径,携带 JSON 数据:

1
2
3
4
{
"method": "call",
"params": ["", "s2s", "enable_echo_server", {"port": "7 $(touch /root/test)"}]
}

权限校验参数检查均通过但是报内部错误,打印 nginx 日志看看。

修改 /etc/nginx/nginx.conf 并重启 nginx

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/d04d6f52-df0f-4720-86bb-010246e20797.png)

再用 PoC 测试一次查看日志。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/6a4159ff-319f-40a1-a991-a60e6172b08f.png)

报错信息显示 ubus-proxyfcgiwrap 未启动或未正常配置,尝试启动 ubus fcgiwrap

1
2
ubus: /sbin/ubusd
fcgiwrap: /etc/init.d/fcgiwrap

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/1dca4eaa-0407-4419-9d14-eb1d65046ccf.png)

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/5e0b6cad-54ff-487b-97ed-9bd1fdea3d7e.png)

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/42b68ad5-295a-4578-b31c-8de44bf38a75.png)

漏洞分析

漏洞只能本地利用未免有些太鸡肋了,继续进行漏洞分析,再尝试寻找远程利用的方法。

通过 PoC 可知漏洞通过 s2s API 传递恶意 shell 命令,分析一下 /usr/lib/oui-httpd/rpc/s2s.so

漏洞出现在 s2s.enable_echo_server 检查并启动 echo_server 过程中:

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/796eee77-3a7e-4bfd-9a12-337a4cf67514.png)

虽然代码中检查了 port 参数是否为有效的数字,但没有严格限制其内容,仅验证了其是否为正数且小于 65535。然而,在字符串形式下,它仍然允许嵌入特殊字符,如 $(),这些字符可以被 shell 解释器解析为命令。

v16(v27, 128, "%s -p %s -f", "/usr/bin/echo_server", v9); 中,port 参数 (v9) 被直接传递给 snprintf 函数,生成的命令字符串随后通过 system(v27); 执行。

由于 v9 可以包含类似 7 $(touch /root/test) 的字符串,shell 会执行其中的命令 touch /root/test,导致命令注入。

漏洞成因分析起来还是比较容易的,最后一个问题,如何通过远程实现漏洞利用。公开的 PoC 是通过 rpc 路径调用 call 方法触发 s2s.enable_echo_server 漏洞。然而,由于会在 rpc.access 阶段进行权限校验,因此需要找到一个不需要权限校验的路径来执行 call 方法。

回过头再看一眼 /usr/lib/lua/oui/rpc.luaglc_call 方法。

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/8d300ad3-f9d6-4913-ad10-a3fa04d3210f.png)

如果直接请求 /cgi-bin/glc 路径,将会调用 glc_call 函数。glc_call 函数会向另一个内部路径(/cgi-bin/glc)发起一个内部 HTTP POST 请求,并传递方法名称、参数等信息。执行 call 方法并且跳过之前的权限校验,修改 PoC 尝试远程利用。

1
curl http://192.168.100.2/cgi-bin/glc -d '{"object":"s2s","method":"enable_echo_server","args":{"port":"7 $(touch /root/test2024)"}}'

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/b4e126d9-1e4c-4018-8961-465d4732b3da.png)

![img](picture/GL-iNet 路由器 CVE-2024-39226 漏洞分析/160be414-974f-4c09-a950-7439c899f62d.png)