type
status
date
slug
summary
tags
category
icon
password
程序分析
lua流程分析
这个固件是用的luci框架编写的,其中
/etc/config/luci通常是luci框架的配置文件,/usr/lib/lua/luci 通常是 LuCI 框架的核心文件所在的目录
Luci采用的是MVC的Web框架,即Model、View、Controller。
这里我们最先做的事找到无鉴权的API接口
显然,此类固件的
cgi部分是用Lua所写的。我们既然想要挖未授权的漏洞,那么首先就要找到无鉴权的API接口,定位到/usr/lib/lua/luci/controller/eweb/api.lua文件。可以看到,只有对
/cgi-bin/luci/api/auth发送请求的时候,不需要权限验证:在 LuCI 的控制器中注册一个路由条目(entry),让
URL 路径/api/auth对应执行rpc_auth函数。同时设置sysauth = false表示这个接口不需要用户登录认证(即“免登录访问”,不需要认证,可以匿名访问)。这意味着当用户访问
/api/auth 路径时,将调用 rpc_auth 函数。在 luci 框架中 sysauth 属性控制是否需要系统级的用户认证才能访问该路由,这里的 sysauth 属性为 false ,表示无需进行系统认证即可访问。
这个就是我们要找的无鉴权的api接口,之后我们看这个接口的具体作用
这个接口调用
rpc_auth函数这里首先引入4个模块,这几个模块其实都是对应的不同文件,
jsonrpc:用于处理 JSON-RPC 请求。
http:用于处理 HTTP 请求和响应。
ltn12:用于处理数据流。
_tbl:假设是一个包含无认证功能的模块(noauth),用于处理实际的 JSON-RPC 方法。
然后获取
HTTP_CONTENT_LENGTH 的长度是否大于 1000 字节,如果不大于的话会将准备 HTTP 响应的类型设置为 application/json之后重点看
这里将
_tbl, http.source()), http.write 这个3个参用/usr/lib/lua/luci/utils/jsonrpc.lua(由jsonrpc和上面得内容得出)中的handle及其相关函数,可以得知这里通过JSON数据的method字段定位并调用noauth.lua中对应的函数,同时将Json数据的params字段内容作为参数传入。(这里面似乎并没有问题)noauth.lua分析
我们看看
_tbl 中有什么,根据local _tbl = require "luci.modules.noauth” 可以的得知这个参数的在文件/luci/modules/noauth.lua 中,我们来这个里面看看都有些什么函数。4个函数
login ,singleLogin ,merge ,checkNet 其中,singleLogin函数无可控制的参数,不用看;解释一下像这个代码的含义,本地导入一个
tool模块,其具体内容在luci.utils.tool 里面,后面调用的到这个模块的某个函数就可以到luci/utils/tool.lua 中寻找具体内容module(): Lua 5.1的模块定义函数(已在Lua 5.2+废弃)
"luci.modules.noauth": 定义模块的完整路径名luci: LuCI框架的命名空间modules: 模块子目录noauth: 关键——这个名字表明模块中的函数可以在未经身份验证的情况下被调用
package.seeall: 使模块能够访问全局环境- 这是一个便利特性但不安全
- 可能导致命名冲突和意外的全局变量访问
回到这个4个函数,先看
checkNet 函数,这个函数我们只能控制params.host 段,并拼接入了命令字符串执行,但程序用了tool.checkIp 对params.host 的内容就行检测,无法绕过,故这个函数没用。checkIP函数,(在luci/utils/tool.lua中)再看看
login 函数,这里一看可以控制的字符十分的多,params.password params.time,params.encry params.limit但是通过
params.password and tool.includeXxs(params.password) 对
params.password 就行过滤,winmt师傅说少过滤了一个\n或许未来有命令注入的可能,类似;这个效果之后程序将
params 中的数据解析进checkStat 中,如何对其进行检测会发现
encry字段和limit字段都变成了加密或者不加密,真或者假,好像变得不可控了调用了
cmd的devSta.get它是首先会将
params传入doParams解析,之后用fetch那么来分析一下
doParams函数而这里会把
\n字符编码为\u000a,导致最后的漏洞点被补上,导致这个login函数也不能使用我们就来看最后一个
merge函数可以看到没有对
params有任何的处理,然后直接调用了cmd 模块中的devSta.set函数,其中直接将data 段数据赋值为 params 说明后面的data 段数据为我们可控的这里
devSta.set函数就是devSta[opt[i]] 中opt[i]为set这里先用
doParams函数对params 中的数据进行解析将其放入对应的位置中,doParams函数最后的return data, back, ip, password, _shell 最后调用
fetch函数处理解析后的数据,这里我们可以看到这个函数内部会调用
fn(...) ,根据最上面的参数,这个fn就是model.fetch 所以执行到这里就会执行在
local model = require "dev_sta" 中可以看出这个函数用的是dev_sta.lua 文件中的fetch函数,这个函数中间的部分就是对一些字段赋予了真假值后,最终将参数都传递给了
/usr/lib/lua/libuflua.so中的client_call函数,我们直接进入这个二进制程序看看。在用ida打开之后会发现一个问题,就是在这个程序中并没有
client_call函数,只有一个uf_client_call
难道这个程序有问题吗,大概率说明
IDA 没有把 client_call 解析成字符串,而是解析成了代码,我们用010打开看看能不能找到这个字符串
发现在010中能找到这完整的字符串,起始地址在
0xff0 ,我们在ida看看这个位置
确实这里直接解析成了代码,选中之后按
a
确实存在这个字符串,我们看程序在哪有引用这个字符串,

在
luaopen_libuflua 函数中有明确显示这个字符串我们看这里的
luaL_register函数,函数原型
参数说明
lua_State *L- Lua 虚拟机状态指针
- 代表当前 Lua 执行环境
- 所有 Lua C API 函数都需要这个参数
2.
const char *libname- 库(模块)的名称
- 可以是
NULL或具体的字符串
- 决定了函数注册的方式:
- 非 NULL:创建全局表并注册函数
- NULL:在栈顶的表中注册函数
3.
const luaL_Reg *l- 函数注册表数组
- 定义了要注册的所有函数
luaL_Reg 结构体
这里我们看看
off_1101C地址都有些什么
刚好即使字符串
client_call ,函数指针sub_A00 ,这里就说明程序中的sub_A00 函数其实就是我们要找的client_call函数为了能顺利分析这个C函数,我们要先了解Lua栈是什么
Lua 栈是 Lua 虚拟机用来管理函数调用和数据传递的一个重要结构。它是一个后进先出(LIFO)的数据结构,专门用于在 C 和 Lua 之间传递数据。每个 Lua 状态机(lua_State)都有自己的栈,用于存储函数参数、返回值和临时变量。
- 压栈操作:
lua_pushnumber(lua_State* L, lua_Number n): 将一个数字压入栈中。
lua_pushstring(lua_State* L, const char* s): 将一个字符串压入栈中。
lua_pushboolean(lua_State* L, int b): 将一个布尔值压入栈中。
lua_pushnil(lua_State* L): 将一个 nil 压入栈中。
- 弹栈操作:
lua_tonumber(lua_State* L, int index): 将栈上指定索引处的值转换为数字。
lua_tostring(lua_State* L, int index): 将栈上指定索引处的值转换为字符串。
lua_toboolean(lua_State* L, int index): 将栈上指定索引处的值转换为布尔值。
- 栈操作:
lua_settop(lua_State* L, int index): 设置栈顶索引。
lua_gettop(lua_State* L): 获取栈顶索引。
lua_remove(lua_State* L, int index): 移除栈上指定索引处的值。
- 表操作:
lua_createtable(lua_State* L, int narr, int nrec): 创建一个新的表并压入栈中。
lua_settable(lua_State* L, int index): 将栈顶的值弹出并存储在表中。
lua_gettable(lua_State* L, int index): 获取表中的值并压入栈中。
但是可以直接理解为调用了
v4 = uf_client_call(v3, v13, 0);在此之前的操作就是在对栈上数据的修改,与漏洞无关,我们来看最后这个uf_client_call函数,因为本函数明显没有显示的漏洞点,因此就先跟进然后再返回来找。uf_client_call函数不在本文件,里我们全局搜索一下,用 grep 在整个文件系统搜索字符串 uf_client_call ,结合 /usr/lib/lua/libuflua.so 文件中引用的外部库进行分析,最终判断出 uf_client_call 函数定义在 /usr/lib/libunifyframe.so
我们进入这个文件中查看
uf_client_call 函数好多代码0_0,这里我们直接结合可控字段出发分析
首先先大致分析一下每个参数是什么,
ctype=2,cmd=’set’,module=”networkId_merge”,param=可控字段,只到back后面都是null想详细分析一下,不过感觉分析不好,直接套用大师傅们的解释吧
这个函数一直到
这里,上面的目的都是首先判断了
method 的类型,然后解析出报文中各字段的值,并将其键值对添加到一个 JSON 对象中,接着将最终处理好的 JSON 对象转换为 JSON 格式的字符串,通过 uf_socket_msg_write 用 socket 套接字进行数据传输,发送形如
补充一下基础的只是 what is socket?
Socket(套接字)是网络编程中用于描述计算机之间通信的端点。它提供了一种机制,使得应用程序可以通过网络传输数据。Socket 是操作系统提供的一种编程接口,用于网络通信。它可以在同一台计算机上的不同进程之间通信,也可以在不同计算机之间通信。Socket 的类型
- 流式套接字(Stream Socket,SOCK_STREAM):
- 使用 TCP(传输控制协议)进行通信。
- 提供可靠的、面向连接的字节流服务。
- 适用于需要保证数据传输顺序和完整性的应用,例如 HTTP、FTP 等。
- 数据报套接字(Datagram Socket,SOCK_DGRAM):
- 使用 UDP(用户数据报协议)进行通信。
- 提供不可靠的、无连接的消息传递服务。
- 适用于对实时性要求较高、但对数据传输可靠性要求较低的应用,例如 DNS 查询、视频流等。
这里使用的是一个
uf_socket_msg_write,而winmt师傅猜测因为这里用write,v41是socket的标识符,v40又指向的是我们可控字段的指针,因此这里一定在其他进程中有一个uf_socket_msg_read与它互相接收信息,很明显并不是下面那个,所以我们全局搜索一下
这里有3个文件,我们要在1,2中确定哪个是要用到的。
很容易锁定
/usr/sbin/unifyframe-sgi.elf文件。又发现在初始化脚本/etc/init.d/unifyframe-sgi中,启动了unifyframe-sgi.elf,即说明unifyframe-sgi.elf一直挂在进程中。因此,我们可以确定unifyframe-sgi.elf就是接收libunifyframe.so所发数据的文件(这里采用了Ubus总线进行进程间通信)。我们就来看在这个文件中都有什么
unifyframe-sgi.elf文件分析
main函数,
(这里实在太多了,像这种我们可以直接来到之前我们说的用来接收信息的
uf_socket_msg_read函数)这里我们看到,用
v51 = uf_socket_msg_read(*_fbss_4, v31 + 1);接收数据,并放入v31 + 1 的位置中,(调试可得)
这里注意因为mips中的特点,执行
uf_socket_msg_read 函数的第2个参数就是s0+4 ,执行完这个函数之后,就能看到收到的数据被放到s0+4中
在接收到数据 解析字段、执行具体操作 的两个函数分别为
parse_content add_pkg_cmd2_task (均位于 main 函数)
parse_content函数
所以我们直接看
parse_content函数内部 ,这里我们结合上面传入的数据进行分析根据上面的分析可知,具体进行数据解析的位置应该是
parse_obj2_cmd 函数,该函数具体分析如下
写道这里的时候感觉有点太长了,重新开了一篇文章,就是这个cve的下接着分析
- 作者:wgiegie
- 链接:https://tangly1024.com/article/2993ecc9-5160-807e-97ee-e606d18305dc
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。