I. Summary
The Luci system is an open-source system written in lua language, mainly introducing some auditing techniques for it. Let's talk about the tool usage in the preparation phase first, and then discuss some ideas from code auditing. I hope that the readers who finish reading this article can gain something from it.
II. Preparation Phase
Code auditing is an indispensable process for vulnerability挖掘, unless black-box testing. This code is not limited to the lua language we will talk about below, but also includes php, java, and even python, as well as the pseudo-C code that often gives binary hands a headache in daily CTF. Through code auditing, we can understand the logic of the entire program development and the process of interface calls, etc., and then find the defects of the program. These defects can cause Denial of Service at a light level, and direct Remote Code Execution (RCE) at a serious level.
Tool usage
The saying goes, 'Sharpen the blade before cutting wood; a good auditing tool is crucial for code auditing. Using the tool well can often make us handle things with ease. The tool I recommend here is VS Code, which I believe is not unfamiliar to everyone reading this. Visual Studio Code (abbreviated as VS Code) is a free code editor developed by Microsoft that supports various operating systems such as Windows, Linux, and macOS. Its strength mainly lies in the numerous powerful plugins it offers. Here, I mainly introduce two, namely vscode-lua-format and lua.
2.1.1 vscode-lua-format
We found that some firmware extracted from the file system has lua code in the lua files that are all garbled and ugly, which undoubtedly brings great difficulties to our code audit work. This plugin can beautify the code, making it look clearer and more impressive!
Analysis of Vulnerability Mining in LuCI System
2.1.2 lua
If the code formatting plugin is called the cornerstone, then this plugin can be called the auxiliary. This plugin allows you to view the calling relationships between various functions, making it easier to understand the basic logic of the program.
Extract the file system
Firstly, try to obtain the firmware, which can be downloaded from some official websites or can be purchased to extract flash from the device. After obtaining the firmware, you can directly extract the file system with binwalk, or if you encounter an ubi file system, you can use the ubireader_extract_images tool to extract it.
3. Code Audit
Find the attack surface
There are generally two main ideas: one is to find vulnerabilities from the positive angle, which is to first find the interfaces of various functions of the website, and then according to the interface names, find the corresponding logic code for audit. The advantage of this method is that it covers more comprehensively, but the drawback is also obvious, as the interfaces of a website may be very large, making it麻烦 to audit, and it is also impossible to find hidden interfaces.
There is also another approach from the reverse angle to find vulnerabilities, which is to first find dangerous functions in the code, and then globally search for which interface called it, thereby finding the corresponding code to audit. The only drawback of this method is that it is not comprehensive enough, and sometimes it may miss some interfaces with logical vulnerabilities. However, the biggest advantage is that it targets the problem directly, finding the vulnerable points. Therefore, we mainly introduce this method here.
Understand the logic of interface calls
To find code vulnerabilities in the corresponding interface, it is first necessary to understand the code logic of the interface call, then to audit the corresponding code. After we clarify the logic, we can find some hidden interfaces. These are interfaces that exist in the background with corresponding code, but developers have deleted the interface code on the front-end for some reasons. Such hidden interfaces exist. This is the powerful aspect of code audit, which cannot be matched by black-box testing.
For example, let's take a router from a certain manufacturer.
//post package 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"}}} //Corresponding code 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 |
From the above, we can see that the value of the method parameter in the JSON data of the post package can be add, set, or del, which correspond to the three functions in the code. Next, ipgroup corresponds to the first parameter of the register_keyword_set_data function, the value of table corresponds to the second parameter, and the third parameter is the function to be executed for registration, and the first parameter passed to the function corresponds to the following string of JSON data.
Search for dangerous functions
After we have clarified the interface call logic, we can now reverse search for vulnerable points. First, let's find his dangerous functions. Unlike auditing pseudo C code, he does not need to consider stack vulnerabilities.
3.3.1 Search for Command Execution Functions
This is the most common method we use to find command injection vulnerabilities, which is to find the command execution functions. In Lua, command execution functions include os.execute, io.popen, etc. The execute function is equivalent to the system() function in C language. The function has a default parameter command, which is to parse the command and then call the system through the parsed result. Popen starts a program prog in an additional process and returns the file handle for prog. In simple terms, this function can call a command (program) and return a file descriptor related to the program, which is generally the output result of the called function.
In the luci framework, in addition to these library functions, there are other wrapping functions in the sys.lua file such as call(), fork_cal()l, exec() etc.
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 Global Search
By using global search, we can find where these dangerous functions are called, and then specifically audit the code
Search for the file upload interface
In openwrt, there is a luci.http.setfilehandler function used for file upload. Let's look at an example of it
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 The setfilehandler function (function(i, t, e)) parameter i is a structure that contains parameters such as filename and file size 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") |
The setfilehandler function calls a callback function. If the formvalue function is not called, the setfilehandler function will not be executed, and at least one formvalue function is outside the setfilehandler function. This may be used to find vulnerabilities such as command injection and arbitrary file upload.
Searching for vulnerabilities in binary programs
When auditing the code, in addition to the vulnerabilities in the code itself, we can sometimes find vulnerabilities in the binary programs called by it, such as:
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") |
This code calls isp_route.sh, and its parameter isp_route_restore_database is something we can control. Let's take a look at the content of the sh script
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 |
This script calls a binary program called isp_route_db, and at this point, we can continue to use ida to analyze this program
At this point, we can audit the program as usual to see if there are binary vulnerabilities such as command injection and stack overflow.
Looking for front-end vulnerabilities
There are many interfaces on the back-end, so they are relatively easy to exploit. However, in most cases, vulnerabilities on the back-end are not very significant. If we want to expand the harm of the vulnerability, naturally, we need to look for vulnerabilities that do not require login authentication. At this point, we need to audit vulnerabilities in the front-end. Unfortunately, the front-end of general routers usually only has one login interface. We can find and audit this interface:
//post package {"method":"do","login":{"username":"xxxxx","password":"xxxxx"}} //Corresponding code 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 |
We found that in the process of searching for the login interface, we also found other front-end interfaces
//post package {"method":"do","query_auth_log":{"xxx":"xxxxx","xxx":"xxxxx"}} {"method":"do","get_domain_array":{"xxx":"xxxxx","xxx":"xxxxx"}} |
4. Summary
In summary, we take a router from a certain manufacturer as an example to introduce the vulnerability挖掘 techniques of the luci system, and explain from the perspective of code audit how to dig some interface vulnerabilities, and please correct me if there is anything wrong in the explanation.
Four, integrate deep code scanning to automatically perform code quality checks
Development of Burp plugin for sensitive information detection based on deepseek local large model
Generative AI Red Team Testing: How to Effectively Evaluate Large Language Models
Continuation of the white-box part of DevSecOps construction
Build an information security visualization platform by yourself (Part Two) Missle Map
A brief discussion on the implementation plan for DevSecOps.
DLL injection vs. Shellcode injection

评论已关闭