浅析LuCI系统的漏洞挖掘

浅析LuCI系统的漏洞挖掘

原创 信创实验室 山石网科安全技术研究院 2022-06-10 16:39

01

摘要

Luci
系统
是基于lua
语言编写的一套开源系统,主要是介绍对其审计的一些技巧,先从准备阶段谈谈工具的使用,再讲一下从代码审计中的一些思路,希望能读完这篇文章的读
者能有所收获。

02

准备阶段

除非黑盒测试,代码审计是漏洞挖掘必不可少的过程,这个代码不仅限于我们下面要讲到的
lua
语言,还有
php

java
甚至是
python
,以及平时
CTF
中经常令二进制手头疼的伪
C
代码,通过代码审计我们可以了解整个程序开发的逻辑和接口调用的流程等等,然后从中找到程序的缺陷,轻则造成拒绝服务,重则直接
RCE


2.1工具使用


磨刀不误砍柴工,有个好的审计工具对于代码审计是很重要的,工具用的好,往往可以使我们得心应手。我这里推荐的工具
vscode
,相信这个工具对于在读的各位并不陌生,
Visual Studio Code
(简称
VS Code
)是一个由微软开发,同时支持
Windows 

Linux 

 macOS 
等操作系统的免费代码编辑器,其强大之处主要在于该工具众多强大的插件。这里主要介绍两个,分别是
vscode-lua-format

lua

2.1.1   vscode-lua-format

我们发现,有些固件从中提取出来的文件系统中,
lua
文件里面的
lua
代码全都是乱码很难看,这无疑会给我们代码审计工作带来巨大的困难。这个插件可以美化代码,使得代码看起来更清晰,更高大上了!

2.1.2    lua

如果称代码格式化的插件为基石,那么这款插件就可以称为辅助。该插件可以让你查看各个函数之间的调用关系,更快地理清程序的基本逻辑。

2.2   提取文件系统

首先就是想办法拿到固件,有些官网上可以下载,或者可以买个设备回来提取
flash
。拿到固件以后可以直接
binwalk
提取文件系统,如果是遇到
ubi
文件系统的,用
ubireader_extract_images
工具即可提取。

03

代码审计

3.1   寻找攻击面

一般主要有两种思路,一种是从正向的角度来寻找脆弱点,就是先找网站各个功能的的接口,然后再根据接口的名称找到对应的逻辑代码对其进行审计。这种方法好处就是覆盖得比较全面,但是缺点也很明显,就是一个网站的接口可能会很庞大,这样审计起来就会很麻烦,还有就是无法找到隐藏的接口。

还有一种是从反向的角度来寻找脆弱点,就是首先找到代码中的危险函数,然后再全局查找是哪个接口来调用了它,以此来找到对应的代码来对其进行审计。这种方法唯一的缺点就是覆盖不够全面,有时候可能会遗漏某些有逻辑漏洞的接口,不过最大的优点就是对症下药,直接找到脆弱点,所以我们这里主要就是介绍这种方法。

3.2  了解接口调用的逻辑

如果要找到对应接口的代码漏洞,首先要了解接口调用的代码逻辑,然后去审计对应的代码,在我们理清逻辑之后,就可以找出其隐藏的一些接口,就是后台是有这个接口的代码的,但是开发人员由于某些原因在前端删掉了对应的接口代码,就存在这样的隐藏接口。这就是代码审计的强大之处,是黑盒测试无法媲美的。

下面就以某厂商的一款路由器为例:

//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
数据。

3.3  寻找危险函数

我们理清完接口的调用逻辑之后,现在就可以反向寻找脆弱点。先来找他的危险函数,跟审计伪
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      全局搜索

利用全局搜索,我们可以找到这些危险函数在哪里调用的,进而来具体审计代码。

3.4 寻找文件上传接口

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_TEMPLATE_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函数的外面,利用这个可能会找到命令注入、任意文件上传等漏洞。

3.5 寻找二进制程序的漏洞

我们在审计代码的时候,除了他代码本身的漏洞,有时候还能找出其中调用的二进制程序中的漏洞,例如:

 
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来对这个程序进行分析

这时候我们就可以跟平时一样审计改程序有没有命令注入和栈溢出等二进制的漏洞。

3.6 寻找前台漏洞

后台的接口众多,所以相对比较好挖,不过后台漏洞的情况下大部分意义不大,如果我们想要扩大漏洞的危害,自然是要去寻找不用去登录认证的漏洞,这时候就要去审计前台中的漏洞,奈何一般路由器类的前端只有一个登录的接口,我们可以去找到并审计一下这个接口:

//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"}}
 
 

04

总结

综上,我们以某厂商的路由器为例介绍了luci
系统的漏洞挖掘技巧,从代码审计角度讲了如何去挖接口的一些漏洞,如有讲得不对的地方请多多指正。