本文由笔者首发于奇安信攻防社区:https://forum.butian.net/share/3000
一年多前,看到小米SRC
公众号推文搞了个赏金活动,于是挖了挖当时比较新的一款AX9000
路由器,挖到了两个命令注入漏洞,不过没什么本事,挖的都是授权后的,危害一般。小米给的赏金还是很可观的,但是补丁发布的速度不知为何比较慢(交了这么多厂商,还是Zyxel
和华硕的响应速度最快),所以一直也没能分配CVE
编号,我也遵守小米的规定在漏洞披露前未公开相关漏洞细节。
直到最近和其他朋友聊起这个漏洞,才想起来已经过去了一年多,应该是能公开了,于是又去找了小米SRC
的运营小姐姐。经过一些流程的审批,得知这两个漏洞的确是已经推送完补丁可以披露了。不过有趣的是,小米申请的2023
的CVE
编号只剩一个了,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.ubifs
用binwalk
再解开,即可得到里面的SquashFS
文件系统,也就是核心部分。
小米的前端也是用的Lua
编写的,但是其中的Lua
文件不是源码,而是编译后的二进制文件,所以我们需要对其进行反编译。目前,对Lua
反编译的常用工具有unluac和luadec。但是小米对Lua
的解释器做了魔改,就不能直接用这两个工具进行反编译了,所幸已有师傅对此做了研究,并给出了专门针对小米固件的反编译工具unluac_miwifi和luadec_miwifi。至于如何对被魔改的解释器或编译器所编译出来的Lua
字节码进行逆向,网上也有不少文章,这里不再展开。
我这里用的是unluac_miwifi
,最终可以编译出一个unluac.jar
,但一次只能对一个Lua
文件进行反编译,所以我们需要写一个批量处理的简单脚本:
小米AX9000
路由器固件是AArch64el
架构的,由于网上似乎没有公开的AArch64
的内核与文件系统,系统级仿真可参考下面这篇文章的步骤extract
出来vmlinuz
和initrd.img
:https://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
这个节点来说,设置sysauth = "admin"
即确保只有admin
账户可以访问这个node
(小米路由器后台的默认账户就是admin
),设置sysauth_authenticator = "jsonauth"
即当token
不存在或错误时,通过authenticator.jsonauth
函数进行登录验证。对于具体的入口来说,定义entry{}
内的第五个参数flag
位为0x01
(或&0x01=1
)代表不需要鉴权,这里/api/xqdatacenter/request
这个入口没有设置flag
位,因此需要鉴权。flag
位有多种option
可选,通过按位与的结果确定所限制的不同权限,感兴趣的师傅可自行分析。
在函数tunnelRequest
中,会对传入payload
字段内的JSON
数据用binaryBase64Enc
函数进行Base64
编码处理,然后拼接入THRIFT_TUNNEL_TO_DATACENTER
所指代的命令中并执行。
关键在于此处用的是 formvalue_unsafe
函数 获取payload
字段内容,未过滤危险字符,而formvalue
函数中是有用hackCheck
过滤危险字符的。这里可能是开发者考虑到Json
格式的数据当中可能会用到某些字符所以不能直接过滤,但也没有进一步去做针对于Json
的危险字符过滤,给了我们可趁之机。
在/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
。
在/usr/sbin/thriftunnel
二进制文件中,*(a2 + 16)
是传入的第二个参数,即Base64
编码后的payload
字段内的Json
数据,其作为第一个参数被传入sub_1B9B0
函数中,而sub_1B9B0
函数的第二个参数v11
此时是空串。
进入sub_1B9B0
函数后,可以发现首先将与a1
(Base64
编码的payload
字段)相关的数据作为参数传入了sub_1F1F8
函数处理,并最终将其返回结果通过string::assign()
赋值给了a2
(即上一级的v11
变量)。
sub_1F1F8
函数看上去是做了一些编码转换的操作,可以猜测到这里就是做了Base64
的解码工作。我们很容易根据其中抛出的异常信息确认我们的猜测,这里的确就是将payload
字段内的Json
数据进行了Base64
解码。
我们再返回到主函数,进而当*(a2 + 8)
即传入的第一个参数option
为0
时,会执行到sub_1BAE0
函数,根据上文分析,其参数v11
就是解码后的Json
字符串。
在sub_1BAE0
函数中,创建了socket
,结合传入的参数(上级的v11
变量)是Json
字符串,很容易判断出此处会将payload
字段的Json
数据发送给本地127.0.0.1
的9090
端口(这里保护了端口的安全性,没有对外开放,我们想要找到未授权口而悬着的心也终于死了)。
/usr/sbin/datacenter
程序一直挂在进程中,监听着9090
端口,故我们的数据被传到了datacenter
程序进一步处理。
在datacenter
的constructAPIMappingTable()
函数里分别执行了三个类的sConstructMappingTable()
函数。
其中,都是通过STL map
建立起了api
编号(下文解释)和对应的处理函数handler
间的映射关系。具体来看,有一些api
是直接在datacenter
中被处理的,有些是被进一步转发到了/usr/sbin/indexservice
(9088
端口)处理,另外一些则是被转发到了/usr/sbin/plugincenter
(9091
端口)中进一步处理。
我们在这里直接定位到该漏洞对应的api
,在datacenter::PluginApiCollection::sConstructMappingTable
中,当api
为629
的时候,对应的handler
是callPluginCenter
,其实从函数名就能看出来作用了,就是转发给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()
的操作,寻找了map
中key
为v8
(即api
值)的迭代器,偏移+32
就是第一个键值元素(api
值),偏移+40
则是第二个实值元素(handler
的函数指针)。显然,此处就是根据传入的api
字段值调用对应的handler
的过程。到这里,上述建立的Mapping Table
中的映射关系也更加明朗了。
上文说过,当api
为629
时,传入的payload
字段的数据会被转发给plugincenter
程序处理。所以最后来到了/usr/sbin/plugincenter
程序中,找到datacenter::PluginApiMappingExtendCollection::sConstructMappingTable
函数,仍然是通过map
建立了api
编号和对应handler
函数的映射关系。可以看到,当api
编号为629
的时候,会执行到parseGetIdForVendor
函数进行处理。
在parseGetIdForVendor
函数中,会将传入的Json
数据内的appid
字段作为参数传递到PluginApi::getIdForVendor
函数中。
在PluginApi::getIdForVendor
函数中,可以很明显地发现:即使appid
字段合法性检查不通过,也会被拼接入命令中并执行。显然,这里是一个开发上的疏忽,在判断!IsValidAppId
的条件分支内,在输出报错信息后,应当在最后加上return ;
返回,不能继续执行下去。
因此,这里存在一个命令注入漏洞,该漏洞调用链至此分析完毕。
这里需要自行更改一下相关IP
和Token
值,此处注入了反弹shell
的命令,端口8888
。
此篇文章仅作抛砖引玉,在datacenter
,plugincenter
以及indexservice
内不同api
的handler
函数可能就有几百个(当然这里可以结合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 并公开
彩蛋: 好吧,水文章的时候需要看着固件水,于是写到这里又水沝淼㵘了一个。嘶,太水了,有辱斯文,有辱斯文,金盆洗手,到此为止了(逃
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)
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)
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
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
=
""
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-5-29 17:07
被winmt编辑
,原因: 补充内容