小米AX9000路由器CVE-2023-26315漏洞复现

小米
2025-02-21 17:50
38100

漏洞概要

受影响版本

  • 固件版本 ≤ 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

image-20250220121612583.png

启动httpd服务

这里有三个http程序,uhttpd,mihttpd,sysapihttpd。查看/etc/sysapihttpd/sysapihttpd.conf发现就是nginx,并且监听了80端口,

根据openwrt的内核初始化流程,按理说应该先启动/etc/preinit,其中会执行/sbin/init进行初始化,但是在这套固件仿真的时候,这样会导致qemu重启,所以我们首先先执行/sbin/init中最重要的/sbin/procd &,启动进程管理器即可。

image-20250220131918523.png
这里我们直接启动sysapihttpd即可,/etc/init.d/sysapihttpd start

image-20250220132305393.png
Failed to connect to ubus这里有报错这里是用到了ubus总线通信,我们需要启动/sbin/ubusd &再去start http程序,启动成功,且netstat查看web端口也正常对外开放,这里的nvram我们暂时用不到,就先不管,

image-20250220132922528.png

image-20250220133005025.png
访问一下,成功模拟,

image-20250220133046340.png
因为我这里前面已经设置过密码了,所有让我们直接密码登录,

由于该漏洞是需要登录授权的,所以我们得看一下后端的密码校验是怎么写的,这里需要分析一下

身份校验的过程在/usr/lib/lua/luci/dispatcher.luajsonauth函数中,其中调用了checkUser函数根据从POST报文中获取的usernamepasswordnonce(现时)字段进行身份验证。

/usr/lib/lua/xiaoqiang/util/XQSecureUtil.luacheckUser函数中,首先获取了系统uci配置项中存储的密码,这里的XQPreference.get函数在本文的上一节中已经给出,可分析出此处的配置项为account.common.(用户名)。接着,需要POST报文中传入的现时字段nonce与系统中uci存储的password的值拼接后进行sha1哈希的结果等于POST报文中传入的密码字段。

到这里我们清楚了后端的鉴权方式,我们去前端js里看一下是怎么构建登录的用户名和密码的,

image-20250220133821209.png

image-20250220133934159.png
这里可以看到固定的用户名就是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里面会用到)

image-20250220134414937.png

漏洞复现

在反编译的/usr/lib/lua/luci/controller/api/xqdatacenter.lua中,可以看到 URL /api/xqdatacenter/request 相关的handler函数是tunnelRequest函数,且这里只能通过admin用户请求访问,我们默认的账户就是admin,

默认是加密混淆过的lua,我们需要用到luadec_miwifi

image-20250220151836498.png
接着分析tunnelRequest函数,看看进行了什么操作,

加载模块:
使用 require 导入了 xiaoqiang.util.XQCryptoUtil 和 luci.util 模块。
获取表单数据:
从表单中获取名为 payload 的数据。
加密数据:
使用 binaryBase64Enc 函数对获取的 payload 数据进行 Base64 编码。
生成通信数据:
获取 THRIFT_TUNNEL_TO_DATACENTER,并用编码后的数据 L1 进行某种运算(可能是对通信数据的处理)。
执行命令:
使用 luci.util.exec 执行带有加密数据的系统命令。
关键在于此处用的是 formvalue_unsafe函数 获取payload字段内容,未过滤危险字符,

image-20250220155349718.png
/usr/lib/lua/xiaoqiang/common/XQConfigs.lua中,可以找到THRIFT_TUNNEL_TO_DATACENTER的相关定义:

image-20250220160910782.png
这里 我们跟进用法发现THRIFT_TUNNEL_TO_DATACENTER所指代的命令为thrifttunnel 0 '%s'。因此,最终所执行的完整命令是thrifttunnel 0 'base64编码的payload字段',即payload字段中被Base64编码后的Json数据会被传入thrifttunnel程序中,且option0

接着我们去找一下thrifttunnel这个文件,最终在/usr/sbin/thriftunnel二进制文件中找到了,拖进IDA分析一下,

这里的a2就是我们前面base64编码的payload字段,其作为第一个参数被传入sub_1B9B0函数中,这里的v12此时是空字符串,

image-20250220163700707.png
进入sub_1B9B0函数后,可以发现首先将与a1Base64编码的payload字段)相关的数据作为参数传入了sub_1F1F8函数处理,并最终将其返回结果通过string::assign()赋值给了a2(即上一级的v12变量)。

image-20250220165123418.png
sub_1F1F8进去后根据大概逻辑,其实就是对我们最开始的值,进行了解码操作,

image-20250220165531739.png
到这里我们再回到主函数,*(a2 + 8)即传入的第一个参数option0时,(就是在lua脚本中的参数)会进入sub_1BAE0函数,这里的v12可以值得就是我们上一步进行解码后的pyload字段,回传给了v12,

image-20250220165907398.png
这里我们继续跟进sub_1BAE0函数看看做了什么操作,在sub_1BAE0函数中,大概是做了网络套接字进行交互,这里可以看到其实就是创建了socket,然后将数据发送到localhost的9090端口,那么v2其实就是我们上一步传进来的解码后的pyload字段,

image-20250220170810215.png
这里由于我们的数据被传到了datacenter程序进一步处理,我们继续去看datacenter文件,这里的datacenter程序一直监听着9090端口,

image-20250220172223319.png
constructAPIMappingTable 函数的作用:

依次调用三个类的静态方法来构建不同类型的 API 映射表(存储 API、下载 API 和插件 API),函数返回 datacenter::PluginApiCollection::sConstructMappingTable(a1) 的结果,其实就是constructAPIMappingTable()函数里分别执行了三个类的sConstructMappingTable()函数

image-20250220173331473.png
其中,都是通过STL map建立起了api编号(下文解释)和对应的处理函数handler间的映射关系。具体来看,有一些api是直接在datacenter中被处理的,有些是被进一步转发到了/usr/sbin/indexservice9088端口)处理,另外一些则是被转发到了/usr/sbin/plugincenter9091端口)中进一步处理。

我们这里进入到函数中可以看到,当api为629的时候对应的方法就是callPluginCenter,那么就是给到这个函数了吧,我们接着去跟进一下,

image-20250220174454033.png
callPluginCenter 是一个高层的封装函数,理数据的准备Thrift 客户端的通信功能以及处理返回数据。ThriftClient::sCallPluginCenter 是与插件中心进行实际通信的低层函数,负责建立连接、发送请求、接收响应并返回结果。

image-20250220175220272.png

image-20250220175235906.png
这里就是把我们的数据转发到了本地的9091端口上,

DataCenterHandler::request函数中,在调用APIMapping::APIMapping函数建立好上述的映射关系表后,紧接着调用了APIMapping::redirectRequest函数。其中,先获取了Json对象中的api字段的值,存放在v8变量中,然后经历了一个for循环,其中有对v8值的判断比较,最后执行了一个函数指针。

image-20250220181003477.png
我们回到正题,前面知道了当api629时,传入的payload字段的数据会被转发给plugincenter程序处理。所以我们继续跟进,最后找到了/usr/sbin/plugincenter

在main中找到datacenter::PluginApiMappingExtendCollection::sConstructMappingTable函数,仍然是通过map建立了api编号和对应handler函数的映射关系。可以看到,当api编号为629的时候,会执行到parseGetIdForVendor函数进行处理。

image-20250220181536865.png
parseGetIdForVendor函数中,会将传入的Json数据内的appid字段作为参数传递到PluginApi::getIdForVendor函数中。

image-20250220181624756.png
PluginApi::getIdForVendor函数中,虽然有使用 AppAccountManager::IsValidAppId(&v11, a1) 来验证传入的 appida1)是否合法,但是,如果验证失败(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)

image-20250219204848172.png

image-20250220183030966.png

漏洞链

大概流程:

  1. 通过鉴权后的请求(需要admin权限的token)调用/api/xqdatacenter/request。
  2. payload中的JSON数据被Base64编码后作为参数传递给thrifttunnel命令。
  3. thrifttunnel解码数据并转发到datacenter的9090端口。
  4. datacenter根据api字段的值629将请求转发到plugincenter的9091端口。
  5. plugincenter中的parseGetIdForVendor函数处理appid参数时,未正确验证并在检查失败后继续执行命令,导致注入。

参考:

https://bbs.kanxue.com/thread-282034.htm

分享到

参与评论

0 / 200

全部评论 2

xxoo的头像
熟悉的感觉,winmt师傅,超长调用链随便逆
2025-03-10 17:53
挽风six的头像
这个linux是在虚拟机上跑的吗
2025-03-06 10:47
Fizzl的头像
是的
2025-03-07 16:16
投稿
签到
联系我们
关于我们