漏洞概要
受影响版本
- 固件版本 ≤ 1.0.168
漏洞类型
- 授权后命令注入漏洞
风险等级
- CVSS 3.1评分:8.8(High)
- 影响范围:机密性/完整性/可用性全破坏
漏洞机理
小米AX9000
路由器在1.0.168
版本及之前存在二进制漏洞(命令注入),该漏洞由于未对非法的appid
做出有效限制而引起。已授权登录的攻击者在成功利用此漏洞后,可在远程目标设备上执行任意命令,并获得设备的最高控制权,造成权限提升。
环境搭建
固件下载地址:固件下载
小米AX9000
路由器固件是AArch64el
架构的,我这里因为使用的mac的m芯片,就是arm64的,加上提前安装了ubuntu、arm的虚拟机,我这里 就直接在虚拟机中模拟起手,
解压后,直接文件夹内进行挂载,
mount --bind /proc proc
mount --bind /dev dev
chroot . /bin/sh
启动httpd服务
这里有三个http程序,uhttpd,mihttpd,sysapihttpd。查看/etc/sysapihttpd/sysapihttpd.conf
发现就是nginx,并且监听了80端口,
根据openwrt
的内核初始化流程,按理说应该先启动/etc/preinit
,其中会执行/sbin/init
进行初始化,但是在这套固件仿真的时候,这样会导致qemu
重启,所以我们首先先执行/sbin/init
中最重要的/sbin/procd &
,启动进程管理器即可。
这里我们直接启动sysapihttpd
即可,/etc/init.d/sysapihttpd start
Failed to connect to ubus
这里有报错这里是用到了ubus
总线通信,我们需要启动/sbin/ubusd &
再去start http程序,启动成功,且netstat
查看web
端口也正常对外开放,这里的nvram我们暂时用不到,就先不管,
访问一下,成功模拟,
因为我这里前面已经设置过密码了,所有让我们直接密码登录,
由于该漏洞是需要登录授权的,所以我们得看一下后端的密码校验是怎么写的,这里需要分析一下
身份校验的过程在/usr/lib/lua/luci/dispatcher.lua
的jsonauth
函数中,其中调用了checkUser
函数根据从POST
报文中获取的username
,password
和nonce
(现时)字段进行身份验证。
在/usr/lib/lua/xiaoqiang/util/XQSecureUtil.lua
的checkUser
函数中,首先获取了系统uci
配置项中存储的密码,这里的XQPreference.get
函数在本文的上一节中已经给出,可分析出此处的配置项为account.common.(用户名)
。接着,需要POST
报文中传入的现时字段nonce
与系统中uci
存储的password
的值拼接后进行sha1
哈希的结果等于POST
报文中传入的密码字段。
到这里我们清楚了后端的鉴权方式,我们去前端js里看一下是怎么构建登录的用户名和密码的,
这里可以看到固定的用户名就是admin,
而密码字段是通过oldPwd()
函数加密后的结果。这里的oldPwd()
函数将用户提交的密码明文与一个固定的key
值(a2ffa5c9be07488bbb04a3a47d3c5f6a
)拼接后,进行sha1
哈希,再将结果继续与现时nonce
拼接后,再sha1
哈希一次,作为POST
请求报文中的密码字段
结合上述分析,我们需要将account.common.admin
这个uci
配置项设置为sha1(登录密码+key)
,比如说登录密码设置为fizzl
,那么这个值就是sha1(fizzla2ffa5c9be07488bbb04a3a47d3c5f6a)=b718c7808f4a1caac7dfab110f12865807cbf40b
。
uci set account.common.admin=b718c7808f4a1caac7dfab110f12865807cbf40b
uci commit
登录成功,token也有了,(后面exp里面会用到)
漏洞复现
在反编译的/usr/lib/lua/luci/controller/api/xqdatacenter.lua
中,可以看到 URL /api/xqdatacenter/request
相关的handler
函数是tunnelRequest
函数,且这里只能通过admin用户请求访问,我们默认的账户就是admin,
默认是加密混淆过的lua,我们需要用到luadec_miwifi
接着分析tunnelRequest
函数,看看进行了什么操作,
加载模块:
使用 require
导入了 xiaoqiang.util.XQCryptoUtil 和 luci.util 模块。
获取表单数据:
从表单中获取名为 payload
的数据。
加密数据:
使用 binaryBase64Enc
函数对获取的 payload 数据进行 Base64
编码。
生成通信数据:
获取 THRIFT_TUNNEL_TO_DATACENTER
,并用编码后的数据 L1 进行某种运算(可能是对通信数据的处理)。
执行命令:
使用 luci.util.exec
执行带有加密数据的系统命令。
关键在于此处用的是 formvalue_unsafe
函数 获取payload
字段内容,未过滤危险字符,
在/usr/lib/lua/xiaoqiang/common/XQConfigs.lua
中,可以找到THRIFT_TUNNEL_TO_DATACENTER
的相关定义:
这里 我们跟进用法发现THRIFT_TUNNEL_TO_DATACENTER
所指代的命令为thrifttunnel 0 '%s'
。因此,最终所执行的完整命令是thrifttunnel 0 'base64编码的payload字段'
,即payload
字段中被Base64
编码后的Json
数据会被传入thrifttunnel
程序中,且option
为0
。
接着我们去找一下thrifttunnel
这个文件,最终在/usr/sbin/thriftunnel
二进制文件中找到了,拖进IDA分析一下,
这里的a2就是我们前面base64编码的payload字段,其作为第一个参数被传入sub_1B9B0
函数中,这里的v12此时是空字符串,
进入sub_1B9B0
函数后,可以发现首先将与a1
(Base64
编码的payload
字段)相关的数据作为参数传入了sub_1F1F8
函数处理,并最终将其返回结果通过string::assign()
赋值给了a2
(即上一级的v12
变量)。
sub_1F1F8
进去后根据大概逻辑,其实就是对我们最开始的值,进行了解码操作,
到这里我们再回到主函数,*(a2 + 8)即传入的第一个参数option
为0
时,(就是在lua脚本中的参数)会进入sub_1BAE0
函数,这里的v12可以值得就是我们上一步进行解码后的pyload字段,回传给了v12,
这里我们继续跟进sub_1BAE0
函数看看做了什么操作,在sub_1BAE0
函数中,大概是做了网络套接字进行交互,这里可以看到其实就是创建了socket
,然后将数据发送到localhost的9090端口,那么v2其实就是我们上一步传进来的解码后的pyload字段,
这里由于我们的数据被传到了datacenter
程序进一步处理,我们继续去看datacenter文件,这里的datacenter程序一直监听着9090端口,
constructAPIMappingTable
函数的作用:
依次调用三个类的静态方法来构建不同类型的 API 映射表(存储 API、下载 API 和插件 API),函数返回 datacenter::PluginApiCollection::sConstructMappingTable(a1)
的结果,其实就是constructAPIMappingTable()
函数里分别执行了三个类的sConstructMappingTable()
函数
其中,都是通过STL map
建立起了api
编号(下文解释)和对应的处理函数handler
间的映射关系。具体来看,有一些api
是直接在datacenter
中被处理的,有些是被进一步转发到了/usr/sbin/indexservice
(9088
端口)处理,另外一些则是被转发到了/usr/sbin/plugincenter
(9091
端口)中进一步处理。
我们这里进入到函数中可以看到,当api
为629的时候对应的方法就是callPluginCenter
,那么就是给到这个函数了吧,我们接着去跟进一下,
callPluginCenter
是一个高层的封装函数,理数据的准备Thrift 客户端的通信功能以及处理返回数据。ThriftClient::sCallPluginCenter
是与插件中心进行实际通信的低层函数,负责建立连接、发送请求、接收响应并返回结果。
这里就是把我们的数据转发到了本地的9091端口上,
在DataCenterHandler::request
函数中,在调用APIMapping::APIMapping
函数建立好上述的映射关系表后,紧接着调用了APIMapping::redirectRequest
函数。其中,先获取了Json
对象中的api
字段的值,存放在v8
变量中,然后经历了一个for
循环,其中有对v8
值的判断比较,最后执行了一个函数指针。
我们回到正题,前面知道了当api
为629
时,传入的payload
字段的数据会被转发给plugincenter
程序处理。所以我们继续跟进,最后找到了/usr/sbin/plugincenter
,
在main中找到datacenter::PluginApiMappingExtendCollection::sConstructMappingTable
函数,仍然是通过map
建立了api
编号和对应handler
函数的映射关系。可以看到,当api
编号为629
的时候,会执行到parseGetIdForVendor
函数进行处理。
在parseGetIdForVendor
函数中,会将传入的Json
数据内的appid
字段作为参数传递到PluginApi::getIdForVendor
函数中。
在PluginApi::getIdForVendor
函数中,虽然有使用 AppAccountManager::IsValidAppId(&v11, a1)
来验证传入的 appid
(a1
)是否合法,但是,如果验证失败(IsValidAppId
返回 false
)则会执行一系列处理:即使应用 ID 无效,代码仍会继续执行以下步骤:构造一个命令行字符串并通过 CommonUtils::sCallSystem
执行,那么很明显,这里存在命令注入。
Poc:
import requests
server_ip = "192.168.1.193"
token = "04e524882aa6f6a1f60b2c72c52ebcec"
exp = ";ps > 1.txt ;"
res = requests.post("http://{}/cgi-bin/luci/;stok={}/api/xqdatacenter/request".format(server_ip, token), data={'payload':'{"api":629, "appid":"' + exp + '"}'})
print(res.text)
漏洞链
大概流程:
- 通过鉴权后的请求(需要admin权限的token)调用/api/xqdatacenter/request。
- payload中的JSON数据被Base64编码后作为参数传递给thrifttunnel命令。
- thrifttunnel解码数据并转发到datacenter的9090端口。
- datacenter根据api字段的值629将请求转发到plugincenter的9091端口。
- plugincenter中的parseGetIdForVendor函数处理appid参数时,未正确验证并在检查失败后继续执行命令,导致注入。