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个函数loginsingleLoginmergecheckNet 其中,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.checkIpparams.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字段都变成了加密或者不加密,真或者假,好像变得不可控了
调用了cmddevSta.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
notion image
难道这个程序有问题吗,大概率说明 IDA 没有把 client_call 解析成字符串,而是解析成了代码,我们用010打开看看能不能找到这个字符串
notion image
发现在010中能找到这完整的字符串,起始地址在0xff0 ,我们在ida看看这个位置
notion image
确实这里直接解析成了代码,选中之后按a
notion image
确实存在这个字符串,我们看程序在哪有引用这个字符串,
notion image
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地址都有些什么
notion image
刚好即使字符串client_call ,函数指针sub_A00 ,这里就说明程序中的sub_A00 函数其实就是我们要找的client_call函数
 
为了能顺利分析这个C函数,我们要先了解Lua栈是什么
Lua 栈是 Lua 虚拟机用来管理函数调用和数据传递的一个重要结构。它是一个后进先出(LIFO)的数据结构,专门用于在 C 和 Lua 之间传递数据。每个 Lua 状态机(lua_State)都有自己的栈,用于存储函数参数、返回值和临时变量。
  1. 压栈操作:
      • 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 压入栈中。
  1. 弹栈操作:
      • lua_tonumber(lua_State* L, int index): 将栈上指定索引处的值转换为数字。
      • lua_tostring(lua_State* L, int index): 将栈上指定索引处的值转换为字符串。
      • lua_toboolean(lua_State* L, int index): 将栈上指定索引处的值转换为布尔值。
  1. 栈操作:
      • lua_settop(lua_State* L, int index): 设置栈顶索引。
      • lua_gettop(lua_State* L): 获取栈顶索引。
      • lua_remove(lua_State* L, int index): 移除栈上指定索引处的值。
  1. 表操作:
      • 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
 
notion image
 
我们进入这个文件中查看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 的类型

  1. 流式套接字(Stream Socket,SOCK_STREAM)
      • 使用 TCP(传输控制协议)进行通信。
      • 提供可靠的、面向连接的字节流服务。
      • 适用于需要保证数据传输顺序和完整性的应用,例如 HTTP、FTP 等。
  1. 数据报套接字(Datagram Socket,SOCK_DGRAM)
      • 使用 UDP(用户数据报协议)进行通信。
      • 提供不可靠的、无连接的消息传递服务。
      • 适用于对实时性要求较高、但对数据传输可靠性要求较低的应用,例如 DNS 查询、视频流等。
 
这里使用的是一个uf_socket_msg_write,而winmt师傅猜测因为这里用write,v41是socket的标识符,v40又指向的是我们可控字段的指针,因此这里一定在其他进程中有一个uf_socket_msg_read与它互相接收信息,很明显并不是下面那个,所以我们全局搜索一下
notion image
 
这里有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 的位置中,(调试可得)
notion image
 
这里注意因为mips中的特点,执行 uf_socket_msg_read 函数的第2个参数就是s0+4 ,执行完这个函数之后,就能看到收到的数据被放到s0+4
notion image
 
在接收到数据 解析字段执行具体操作 的两个函数分别为 parse_content add_pkg_cmd2_task (均位于 main 函数)
notion image

parse_content函数

所以我们直接看parse_content函数内部 ,这里我们结合上面传入的数据进行分析
根据上面的分析可知,具体进行数据解析的位置应该是 parse_obj2_cmd 函数,该函数具体分析如下
 
写道这里的时候感觉有点太长了,重新开了一篇文章,就是这个cve的下接着分析
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CVE-2023-20073复现CNVD-2013-11625复现
Loading...