1 前言
8月5日网上披露了 CVE-2024-399226 [1],影响多款 GL-iNet
路由器,随后开始漏洞应急。起初对 GL-iNet
路由器不了解导致踩了很多坑、浪费了不少时间,因此在做完应急后对这次漏洞分析和固件仿真进行记录。
2 产品介绍
GL.iNet
是一家专注于智能路由器和网络设备开发的科技公司。成立于 2009 年,总部位于中国,该公司的产品以 OpenWrt
操作系统为基础,提供高度的可定制性和灵活性。公司致力于为家庭、企业以及工业物联网环境提供可靠的网络解决方案。GL.iNet
的设备以其开源特性、强大的功能和优秀的用户体验而受到开发者、网络安全专家和高级用户的青睐。
OpenWrt
是一个基于 Linux 的开源嵌入式操作系统,专为网络设备(如路由器、网关和接入点)设计。与传统的路由器固件不同,OpenWrt
不是单一的、不可变的固件,而是一个完整且可扩展的操作系统,允许自定义以适应任何应用程序。
OpenResty
是一个基于 Nginx
的高性能 Web
平台,它将 Lua
脚本引擎嵌入到 Nginx
中,使开发者可以通过 Lua
脚本编写高度可定制的 Web
服务,用来处理复杂的 web
逻辑和 API
请求。OpenResty
通常用于高并发、低延迟的 Web
应用程序开发,特别是在需要处理复杂逻辑或与外部服务交互时。
这种组合使得 GL.iNet
路由器不仅仅是一个网络设备,还可以作为一个小型的 Web
服务器或应用平台。
3 环境模拟
3.1 固件提取
GL.iNet
官网提供历史固件下载[2]。
固件版本:GL-AX1800 Flint 4.5.16
sysupgrade-glinet_ax1800
文件夹下存在 root
文件。
$ 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
文件系统。
$ binwalk -Me root
查看 bin/busybox
得知是 32位arm
架构。
$ 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
3.2 QEMU模拟
使用 qemu-system-arm
从系统角度进行模拟,此时需要一个 arm
架构的内核镜像和文件系统,可以在这个网站下载[3]。
vmlinuz-3.2.0-4-vexpress linux内核镜像文件
initrd.img-3.2.0-4-vexpress RAM磁盘映像文件
debian_wheezy_armhf_standard.qcow2 虚拟磁盘映像文件
启动虚拟环境。
$ 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。
Illegal instruction
启动后用户名和密码都是 root
即可登录模拟的系统。
接下来在宿主机创建一个网卡,使 qemu
内能和宿主机通信。
宿主机安装依赖。
$ sudo apt-get install bridge-utils uml-utilities
将如下代码保存为 net.sh
并运行即可。
#!/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
并运行。
#!/bin/sh
ifconfig eth0 192.168.100.2 netmask 255.255.255.0
route add default gw 192.168.100.254
//虚拟系统可能没有 vim
或 nano
,使用 echo
一行一行写。
这样宿主机和模拟环境互通,使用 scp
将 squashfs-root
文件夹上传到 qemu
系统中的 /root
路径下。
scp -r squashfs-root/ [email protected]:/root
然后挂载 proc
、 dev
,最后 chroot
即可。
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
4 漏洞复现
启动 web
服务,前文已经介绍过 GL.iNet
路由器利用 OpenResty
来增强其 web
管理界面和 API
的功能。而 OpenResty
是基于 Nginx
的 web
平台,内置 Lua
脚本支持,所以首先启动 Nginx
服务。
尝试运行 /etc/init.d
下 nginx
脚本(/etc/init.d
目录通常包含系统启动和管理各种服务的脚本,如果需要启动某个服务,通常可以在该目录中找到相应的脚本)。
查看 /etc/init.d/nginx
如何手动启动 nginx
。
创建缺少的文件夹再次启动 nginx
。
看样子 nginx
好像起来了,访问 web
却是 404
。
这个时候已经没什么头绪了,find
一下所有 nginx
相关文件试试。
每个文件都看看,发现 /etc/uci-defaults/80_nginx-oui
脚本。
/etc/uci-defaults/80_nginx-oui
脚本的主要作用是配置和调整Nginx的相关文件,确保Web服务能够正常运行。
尝试运行 /etc/uci-defaults/80_nginx-oui
看看是否能修复 404
问题。
成功修复,接下来尝试漏洞复现,先看一下披露的 PoC
。
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
试试。
拒绝访问,再使用 127.0.0.1
(之前的会话因为启动 nginx
,需要 ssh
再创建一个会话)。
这次报错为内部错误。继续找问题。根据 PoC
可知请求的路径是rpc
,在 /etc/nginx
下的 nginx
配置文件中查找 rpc
相关信息。
在 /etc/nginx/conf.d/gl.conf
找到请求 rpc
路径的处理方法。
跟进到 /usr/share/gl-ngx/oui-rpc.lua
。
代码来看 /usr/share/gl-ngx/oui-rpc.lua
是处理 HTTP POST
请求 jSON-RPC
调用的。
以上代码实现模块导入、请求方式验证和读取请求体。
要注意 ubus
服务是 OpenWrt
系统中一个进程间通信框架,需要启动。
HTTP
请求仅允许 POST
,拒绝其他方式访问。
/usr/share/gl-ngx/oui-rpc.lua
定义了多个处理函数,每个函数对应不同的 rpc
方法(因为 PoC
通过 call
方法调用 s2s.enable_echo_server
进行攻击,所以只截取 rpc_method_call
方法代码)。
rpc_method_call
进行参数校验、会话检查和 Ubus
调用:
-
确保
params
中至少三个元素且元素类型正确。 -
检查
sid
是否有效,并通过rpc.access
验证访问权限。 -
如果上述判断均通过,使用
rpc.call
执行指定的Ubus
对象和方法。
继续跟进 /usr/lib/lua/oui/rpc.lua
查看 rpc.access
和 rpc.call
实现。
access
通过 is_local
判断是否本地请求。对于本地请求和 glinet
标头的请求,总是允许访问(确定了只能本地利用)。
M.call
函数是核心的 rpc
调用处理器,执行以下步骤:
-
检查请求的对象是否已加载,如果未加载,则尝试从
/usr/lib/oui-httpd/rpc/
目录下加载脚本文件。 -
如果脚本文件存在且加载成功,将对象的方法注册到
objects
表中。 -
如果无法从
/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
文件。
大致实现逻辑与 /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
两段代码逻辑。
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
数据:
{
"method": "call",
"params": ["", "s2s", "enable_echo_server", {"port": "7 $(touch /root/test)"}]
}
权限校验参数检查均通过但是报内部错误,打印 nginx
日志看看。
修改 /etc/nginx/nginx.conf
并重启 nginx
。
再用 PoC
测试一次查看日志。
报错信息显示 ubus-proxy
和 fcgiwrap
未启动或未正常配置,尝试启动 ubus
fcgiwrap
。
ubus: /sbin/ubusd
fcgiwrap: /etc/init.d/fcgiwrap
终于复现成功了。
5 漏洞分析
漏洞只能本地利用未免有些太鸡肋了,继续进行漏洞分析,再尝试寻找远程利用的方法。
通过 PoC
可知漏洞通过 s2s
API
传递恶意 shell
命令,分析一下 /usr/lib/oui-httpd/rpc/s2s.so
。
漏洞出现在 s2s.enable_echo_server
检查并启动 echo_server
过程中:
虽然代码中检查了 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.lua
的 glc_call
方法。
如果直接请求 /cgi-bin/glc
路径,将会调用 glc_call
函数。glc_call
函数会向另一个内部路径(/cgi-bin/glc
)发起一个内部 HTTP POST 请求,并传递方法名称、参数等信息。执行 call
方法并且跳过之前的权限校验,修改 PoC
尝试远程利用。
curl http://192.168.100.2/cgi-bin/glc -d '{"object":"s2s","method":"enable_echo_server","args":{"port":"7 $(touch /root/test2024)"}}'
6 总结
漏洞应急最烦环境弄不好,这次就因为一开始不了解 GL-iNet
路由器导致纯在瞎折腾浪费时间,最后在大佬的指点下搭建好环境。记录过程中尽量复刻了当时工作的操作顺序,逻辑上有很多地方其实有更简单的解决方法不用绕这么多弯,诸君见笑。
7 相关链接
https://dl.gl-inet.cn/
https://people.debian.org/~aurel32/qemu/
作者名片
原文始发于微信公众号(知道创宇404实验室):原创 Paper | GL-iNet 路由器 CVE-2024-39226 漏洞分析