浅析LuCI系统的漏洞挖掘
一. 摘要
Luci系统是基于lua语言编写的一套开源系统,主要是介绍对其审计的一些技巧,先从准备阶段谈谈工具的使用,再讲一下从代码审计中的一些思路,希望能读完这篇文章的读者能有所收获。
二. 准备阶段
除非黑盒测试,代码审计是漏洞挖掘必不可少的过程,这个代码不仅限于我们下面要讲到的lua语言,还有php、java甚至是python,以及平时CTF中经常令二进制手头疼的伪C代码,通过代码审计我们可以了解整个程序开发的逻辑和接口调用的流程等等,然后从中找到程序的缺陷,轻则造成拒绝服务,重则直接RCE。
工具使用
磨刀不误砍柴工,有个好的审计工具对于代码审计是很重要的,工具用的好,往往可以使我们得心应手。我这里推荐的工具vscode,相信这个工具对于在读的各位并不陌生,Visual Studio Code(简称VS Code)是一个由微软开发,同时支持Windows 、Linux 和 macOS 等操作系统的免费代码编辑器,其强大之处主要在于该工具众多强大的插件。这里主要介绍两个,分别是vscode-lua-format和lua。
2.1.1 vscode-lua-format
我们发现,有些固件从中提取出来的文件系统中,lua文件里面的lua代码全都是乱码很难看,这无疑会给我们代码审计工作带来巨大的困难。这个插件可以美化代码,使得代码看起来更清晰,更高大上了!
浅析LuCI系统的漏洞挖掘
2.1.2 lua
如果称代码格式化的插件为基石,那么这款插件就可以称为辅助。该插件可以让你查看各个函数之间的调用关系,更快地理清程序的基本逻辑。
提取文件系统
首先就是想办法拿到固件,有些官网上可以下载,或者可以买个设备回来提取flash。拿到固件以后可以直接binwalk提取文件系统,如果是遇到ubi文件系统的,用ubireader_extract_images工具即可提取
三. 代码审计
寻找攻击面
一般主要有两种思路,一种是从正向的角度来寻找脆弱点,就是先找网站各个功能的的接口,然后再根据接口的名称找到对应的逻辑代码对其进行审计。这种方法好处就是覆盖得比较全面,但是缺点也很明显,就是一个网站的接口可能会很庞大,这样审计起来就会很麻烦,还有就是无法找到隐藏的接口。
还有一种是从反向的角度来寻找脆弱点,就是首先找到代码中的危险函数,然后再全局查找是哪个接口来调用了它,以此来找到对应的代码来对其进行审计。这种方法唯一的缺点就是覆盖不够全面,有时候可能会遗漏某些有逻辑漏洞的接口,不过最大的优点就是对症下药,直接找到脆弱点,所以我们这里主要就是介绍这种方法。
了解接口调用的逻辑
如果要找到对应接口的代码漏洞,首先要了解接口调用的代码逻辑,然后去审计对应的代码,在我们理清逻辑之后,就可以找出其隐藏的一些接口,就是后台是有这个接口的代码的,但是开发人员由于某些原因在前端删掉了对应的接口代码,就存在这样的隐藏接口。这就是代码审计的强大之处,是黑盒测试无法媲美的。
下面就以某厂商的一款路由器为例:
//post包
POST /stok=89eb18a6314226c045d82175589ff4a1/ds HTTP/1.1 Host: 192.168.0.1 Content-Length: 132 Origin: http://192.168.0.1 Connection: close Referer: http://192.168.0.1/
{"method":"add","ipgroup":{"table":"rule_ipgroup","para":{"flag":"user","name":"aa","rule_scope":["---"],"comment":"aa","ref":"0"}}}
//对应代码 function index() register_module("ipgroup", "ipgroup") local e = require("luci.model.uci") local e = e.cursor() register_keyword_set_data("ipgroup", "rule_ipgroup", "rule_ipgroup_set_data") register_keyword_add_data("ipgroup", "rule_ipgroup", "rule_ipgroup_add_data") register_keyword_del_data("ipgroup", "rule_ipgroup", "rule_ipgroup_del_data") function rule_ipgroup_add_data(t, l, l, l) local l = p.cursor() local r = e.ipgroup_table_count_get_by_key_value("flag", "user") local l = l:get_profile("ipgroup", "group_max") or s if t.flag == "user" and r >= l then return n.ETABLEFULL end local l = e.ipgroup_table_entry_get_is_exist("name", t.name) if l ~= false then return n.EENTRYEXIST end t.secname = t.name t.flag = t.flag or "user" t.comment = t.comment or "" if t.comment ~= nil then t.comment = string.gsub(t.comment, "'", "''") end if t.rule_scope == nil then t.rule_scope = {} end if t.rule_ipgroup == nil then t.rule_ipgroup = {} end e.ipgroup_table_entry_insert(t) local _ = e.ipgroup_table_entry_get_is_exist("name", t.name, "id") local r = e.ipscope_table_entry_get_id_table_by_name(t.rule_scope) local l = {} for e = 1, #r do l[e] = r[e].id end if #l > 0 then e.relation_table_entry_multi_insert(_[1].id, l) end local r = e.ipgroup_table_entry_get_id_table_by_name(t.rule_ipgroup) local l = {} for e = 1, #r do l[e] = r[e].id end if #l > 0 then e.group_relation_table_entry_multi_insert(_[1].id, l) end ipgroup_after_proc("add") return n.ENONE, {["ipgroup"] = {["name"] = t.name}} end |
由上面可以知道,post包中json数据的method参数的值可以是add、set、del,分别对应代码中的三个函数,接着ipgroup对应register_keyword_set_data函数的第一个参数,table的值对应第二个参数,第三个参数就是对应注册要执行的函数,函数传进的第一个参数就是对应后面一串json数据。
寻找危险函数
我们理清完接口的调用逻辑之后,现在就可以反向寻找脆弱点。先来找他的危险函数,跟审计伪C代码不一样的是,他不用考虑栈方面的漏洞。
3.3.1 寻找命令执行函数
这是我们寻找命令注入漏洞最常用的方法,就是找命令执行的函数,在lua语言中,命令执行的函数有os.execute、io.popen等等。execute函数相当于C语言中的system(),函数有一个缺省的参数command,这个函数就是解析command再来通过的系统来调用解析的结果。popen在额外的进程中启动程序prog,并返回用于prog的文件句柄。通俗的来说就是使用这个函数可以调用一个命令(程序),并且返回一个和这个程序相关的文件描述符,一般是这个被调用函数的输出结果。
在luci框架中,除了这些库函数,在sys.lua文件还有其他对其进行包装的函数如call()、fork_cal()l、exec()等
function call(...) return u.execute(...) / 256 end function fork_exec(...) local e = n.fork() if e == 0 then n.chdir("/") n.chdir("/") local e = n.open("/dev/null", "w+") if e then n.dup(e, n.stderr) n.dup(e, n.stdout) n.dup(e, n.stdin) if e:fileno() > 2 then e:close() end end return n.exec("/bin/sh", "-c", ...) else return end end function fork_call(...) local t = n.fork() if t == 0 then local e = n.open("/dev/null", "w+") if e then n.dup(e, n.stderr) n.dup(e, n.stdout) n.dup(e, n.stdin) if e:fileno() > 2 then e:close() end end return n.exec("/bin/sh", "-c", ...) elseif t > 0 then local t, e, n = n.waitpid(t) if e == "exited" then return n else return nil end end end function exec(e) local e = r.popen(e) local n = e:read("*a") e:close() return n end |
3.3.2 全局搜索
利用全局搜索,我们可以找到这些危险函数在哪里调用的,进而来具体审计代码
寻找文件上传接口
openwrt下有个 luci.http.setfilehandler作为文件上传函数,来看其中一个例子
function upload_pic() local t = {} local l, i, _ local c = 0 local d = 200 * 1024 local r = 0 local a = "/www/web-static/resources/authserver/tmpfile" local h = PORTAL_TEMPL.ATE_DIR luci.http.setfilehandler(function(i, t, e) 参数i是个结构体,里面有文件名,文件大小等参数 if not l then l = io.open(a, "w") c = 0 end if t then c = c + #t if c <= d then l:write(t) else if 0 == r then l:close() o.fork_call("rm -f " .. a) l = io.open(a, "w") r = 1 end end end if e then l:close() end end) luci.http.formvalue("filename") |
setfilehandler函数是调用一个回调函数,如果没有调用formvalue函数,setfilehandler函数将不会执行,而且至少有一个formvalue函数在setfilehandler函数的外面,利用这个可能会找到命令注入、任意文件上传等漏洞。
寻找二进制程序的漏洞
我们在审计代码的时候,除了他代码本身的漏洞,有时候还能找出其中调用的二进制程序中的漏洞,例如:
function upload_db() local _ = 131072 local t = 0 local i local t = nil local o = nil local l = 0 local n = e.ENONE luci.http.setfilehandler(function(r, a, s) if not i then if r then if r.name == "isp_database" then t = p o = ". /lib/isp_route/isp_route.sh && isp_route_restore_database" elseif r.name == "user_database" then t = d o = ". /lib/isp_route/isp_route.sh && isp_restore_user_database" end end if t == nil then n = e.EISPDBNULLFILENAME return end i = io.open(t, "w") l = 0 end if a then l = l + #a if l <= _ then i:write(a) else n = e.EISPDBTOOLARGE return end end if s then i:close() end end) luci.http.formvalue("trigger-parser") |
这个代码中调用了isp_route.sh,而且他的参数isp_route_restore_database是我们可以控制的,再来看下这个sh脚本的内容
isp_route_restore_database() { if [ ! -f $ISP_DATABASE_TMP ];then return 1; fi
/usr/sbin/isp_route_db system -c
if [ $? != "0" ]; then logger -t isp -p warn "The isp system database's format is invalid." rm $ISP_DATABASE_TMP -f return 1; fi
#move the tmp isp database to /etc/nouci_config/dbs mv $ISP_DATABASE_TMP $ISP_DATABASE_DIR -f
state=`uci get isp_route.global.state 2>/dev/null` [ "$state" == "on" ] && /etc/init.d/isp_route stop
/usr/sbin/isp_route_db system -b
if [ "$state" == "on" ]; then /etc/init.d/isp_route start else for i in $isp_tables; do ipset destroy $i 2>/dev/null done fi }
isp_restore_user_database() { if [ ! -f $ISP_USER_DB_TMP ];then return 1; fi
#check the valid of user's database /usr/sbin/isp_route_db user -c
if [ $? != "0" ]; then logger -t isp -p warn "The isp user database's format is invalid." rm $ISP_USER_DB_TMP -f return 1; fi
#move the tmp isp database to /etc/nouci_config/dbs mv $ISP_USER_DB_TMP $ISP_USER_DB_DIR -f
state=`uci get isp_route.global.state 2>/dev/null` [ "$state" == "on" ] && /etc/init.d/isp_route stop
/usr/sbin/isp_route_db user -b
if [ "$state" == "on" ]; then /etc/init.d/isp_route start else ipset destroy ISP_USER_DEFINE 2>/dev/null fi } |
这个脚本调用了一个叫isp_route_db的二进制程序,我们这时候就可以继续用ida来对这个程序进行分析
这时候我们就可以跟平时一样审计改程序有没有命令注入和栈溢出等二进制的漏洞。
寻找前台漏洞
后台的接口众多,所以相对比较好挖,不过后台漏洞的情况下大部分意义不大,如果我们想要扩大漏洞的危害,自然是要去寻找不用去登录认证的漏洞,这时候就要去审计前台中的漏洞,奈何一般路由器类的前端只有一个登录的接口,我们可以去找到并审计一下这个接口:
//post包 {"method":"do","login":{"username":"xxxxx","password":"xxxxx"}} //对应代码 if n["query_auth_log"] then if n.method ~= "do" then r[e.NAME] = e.EINVARG write_json(r) return false end return action_get_unauth_log(n["query_auth_log"]) end if n["get_domain_array"] then if n.method ~= "do" then r[e.NAME] = e.EINVARG write_json(r) return false end return action_get_domain_array(n["get_domain_array"]) end local a, c = l.is_locked() if a then r[e.NAME] = e.EUNAUTH r.data = get_unauth_data(c) write_json(r) return false end if "" ~= i then luci.http.redirect("/") return false end if n.login then if n.method ~= "do" then r[e.NAME] = e.EINVARG write_json(r) return false end return action_login(n.login) end if (n["administration"] and n["administration"]["set_pwd_before_login"]) or n["set_password"] then if n.method ~= "do" then r[e.NAME] = e.EINVARG write_json(r) return false end if n["set_password"] then local e = n["set_password"]["username"] local t = n["set_password"]["password"] n["set_password"] = nil n["administration"] = {} n["administration"]["set_pwd_before_login"] = {} n["administration"]["set_pwd_before_login"]["username"] = e n["administration"]["set_pwd_before_login"]["password"] = t end return t.ds(n) end |
我们发现在寻找登录接口的过程中,还发现了其他的前端接口
//post包 {"method":"do","query_auth_log":{"xxx":"xxxxx","xxx":"xxxxx"}} {"method":"do","get_domain_array":{"xxx":"xxxxx","xxx":"xxxxx"}}
|
四. 总结
综上,我们以某厂商的路由器为例介绍了luci系统的漏洞挖掘技巧,从代码审计角度讲了如何去挖接口的一些漏洞,如有讲得不对的地方请多多指正。
扫描关注公众号