24年8月网上披露了 CVE-2024-399226
[1] ,影响多款 GL-iNet
路由器,随后开始漏洞应急。起初对 GL-iNet
路由器不了解导致踩了很多坑浪费了不少时间,因此做完应急后对这次漏洞分析和固件仿真进行记录。
GL.iNet
是一家专注于开发智能路由器和网络设备的公司。该公司的产品通常基于 OpenWrt
操作系统,广泛应用于家庭、企业和工业物联网场景。GL.iNet
路由器以其开源、可定制性和强大的功能而著称,特别适合开发者、网络安全研究人员和高级用户。
OpenWrt
是一个基于 Linux 的开源嵌入式操作系统,专为网络设备(如路由器、网关和接入点)设计。与传统的路由器固件不同,OpenWrt
不是单一的、不可变的固件,而是一个完整且可扩展的操作系统,允许自定义以适应任何应用程序。
OpenResty
是一个基于 Nginx
的高性能 Web
平台,它将 Lua
脚本引擎嵌入到 Nginx
中,使开发者可以通过 Lua
脚本编写高度可定制的 Web
服务,用来处理复杂的 web
逻辑和 API
请求。OpenResty
通常用于高并发、低延迟的 Web
应用程序开发,特别是在需要处理复杂逻辑或与外部服务交互时。
这种组合使得 GL.iNet
路由器不仅仅是一个网络设备,还可以作为一个小型的 Web
服务器或应用平台。
GL.iNet
官网提供历史固件下载[2]
sysupgrade-glinet_ax1800
文件夹下存在 root
文件
使用 binwalk
从 root
中提取 Squashfs
文件系统
查看 bin/busybox
得知是 32位arm
架构
使用 qemu-system-arm
从系统角度进行模拟,此时需要一个 arm
架构的内核镜像和文件系统,可以在这个网站下载[3]
启动虚拟环境
//默认可以不指定 cpu 模型,我在模拟过程中遇到报错所以指定了 cpu
启动后用户名和密码都是 root
即可登录模拟的系统。
接下来在宿主机创建一个网卡,使 qemu
内能和宿主机通信。
宿主机安装依赖
将如下代码保存为 net.sh
并运⾏即可
然后配置 qemu
虚拟系统的路由,在 qemu
虚拟系统中运⾏ net.sh
并运⾏
//虚拟系统可能没有 vim
或 nano
,使⽤ echo
⼀⾏⼀⾏写。
这样宿主机和模拟环境互通,使⽤ scp
将 squashfs-root
⽂件夹上传到 qemu
系统中的 /root
路径下。
然后挂载 proc
、 dev
,最后 chroot
即可
启动 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
从 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
调用:
继续跟进 /usr/lib/lua/oui/rpc.lua
查看 rpc.access
和 rpc.call
实现access
通过 is_local
判断是否本地请求。对于本地请求和 glinet
标头的请求,总是允许访问(确定了只能本地利用)M.call
函数是核心的 rpc
调用处理器,执行以下步骤:
查看 /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
两段代码逻辑
请求发送一个 POST
请求到 rpc
路径,携带 JSON
数据:
权限校验参数检查均通过但是报内部错误,打印 nginx
日志看看
修改 /etc/nginx/nginx.conf
并重启 nginx
再用 POC
测试一次查看日志 报错信息显示 ubus-proxy
和 fcgiwrap
未启动或未正常配置,尝试启动 ubus
fcgiwrap
成功复现嘻嘻
漏洞只能本地利用未免有些太鸡肋了,继续进行漏洞分析,再尝试寻找远程利用的方法。
通过 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
尝试远程利用。
漏洞应急最烦环境弄不好,这次就因为一开始不了解 GL-iNet
路由器导致纯在瞎折腾浪费时间,最后在大佬的指点下搭建好环境。记录过程中尽量复刻了当时工作的操作顺序,逻辑上有很多地方其实有更简单的解决方法不用绕这么多弯,诸君见笑。
[1] 漏洞详情
https://github.com/gl-inet/CVE-issues/blob/main/4.0.0/s2s%20interface%20shell%20injection.md
[2] 固件下载