从零开始复现CVE-2023-34644
说实话复现这个漏洞光调试我就调了一个星期,主要是逆向很难
仿真启动脚本
tar czf rootfs.tar.gz ./rootfs
scp rootfs.tar.gz [email protected]:/root/rootfs
cd rootfs
chmod -R 777 ./
mount -bind /proc proc
mount -bind /dev dev
chroot . /bin/sh
/sbin/init
mkdir /var/run/lighttpd.pid
/etc/init.d/lighttpd start
/sbin/ubusd &
mkdir /tmp/coredump
mkdir /tmp/rg_device
cp /sbin/hw/60010081/rg_device.json /tmp/rg_device/rg_device.json
/usr/sbin/unifyframe-sgi.elf
配置文件漏洞分析
简单说一下JSON中的一些字段
method
用于指定调用远程的方法
params
用于指定方法的参数
首先这是一个未授权漏洞那就从认证部分开始,所以思路就是从
api
接口寻找,这个路由器是使用lua
来完成前端服务路径是/usr/libc/lua/luci/controller/eweb
,下面有一个api.lua
这里的
_tbl
是四种方法分别是login
,singleLogin
,merge
,checkNet
路径
usr/lib/lua/luci/controller/eweb/api.lua
认证模块
-- 认证模块
function rpc_auth()
--导入模块
local jsonrpc = require "luci.utils.jsonrpc" --应对JSON-RPC 的请求和响应
local http = require "luci.http"
local ltn12 = require "luci.ltn12"
local _tbl = require "luci.modules.noauth"
if tonumber(http.getenv("HTTP_CONTENT_LENGTH") or 0) > 1000 then --长度检查
http.prepare_content("text/plain")
-- http.write({code = "1", err = "too long data"})
return "too long data"
end
--设置http的响应类型
http.prepare_content("application/json")
--调用`jsonrpc.handle`处理JSON-RPC请求
ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write)
end
handle
handle
函数的主要功能就是根据接收json
中的method
字段选择方法和params
做为参数(这也是为什么后面构造的poc
只有method
和params
这两个字段)路径
usr/lib/lua/luci/utils/jsonrpc.lua
function handle(tbl, rawsource, ...)
local decoder = luci.json.Decoder() --c创建一个json解码器
local stat, err = luci.ltn12.pump.all(rawsource, decoder:sink())
local json = decoder:get() --获得解码后的数据
local response
local success = false
if stat then --判断是否解码成功
if type(json.method) == "string" then --查看json中是否存在method字段并且是一个字符串
local method = resolve(tbl, json.method) --调用`resolve`方法
if method then
response = reply(json.jsonrpc, json.id, proxy(method, json.params or {})) --调用 reply和proxy函数 (params 字段是用来传递参数给远程过程调用(RPC)方法的部分)
else
...
end
resolve这个函数的功能就是为method中字段选择方法
function resolve(mod, method) --第一个参数是是四种方法分别是`login`,`singleLogin`,`merge`,`checkNet` 第二个参数是JSON中的method字段
local path = luci.util.split(method, ".") -- 根据.来分割字符
for j = 1, #path - 1 do
if not type(mod) == "table" then -- 这段代码是检查导入的"luci.modules.noauth" 模块是否成功
break
end
mod = rawget(mod, path[j]) --从 Lua 表格 mod 中获取键为 path[j] 的元素或字段的值,并将该值存储在变量 mod 中
if not mod then
break
end
end
mod = type(mod) == "table" and rawget(mod, path[#path]) or nil
if type(mod) == "function" then
return mod
end
end
reply函数根据传入的参数创建一个符合JSON-RPC规范的响应对象
function reply(jsonrpc, id, res, err)
require "luci.json"
id = id or luci.json.null
-- 1.0 compatibility
if jsonrpc ~= "2.0" then
jsonrpc = nil
res = res or luci.json.null
err = err or luci.json.null
end
-- if type(res) == "string" then
-- res = luci.json.decode(res) or res
-- end
return {id = id, data = res, error = err, jsonrpc = jsonrpc, code = 0}
end
proxy
function proxy(method, ...)
local tool = require "luci.utils.tool"
local res = {luci.util.copcall(method, ...)}--在这里又调用了copcall
...
end
copcall
function copcall(f, ...)
return coxpcall(f, copcall_id, ...)
end
coxpcall
function coxpcall(f, err, ...)
local res, co = oldpcall(coroutine.create, f) --在这里利用coroutine.create创建一个新的进程f,也就是我们选择的method
...
四种metho
下面就是重点了,分析四种metho
了,根据上面的分析这四种方法在luci.modules.noauth
中
singleLogin
没有看见可以控制的参数
-- 单点登录
function singleLogin()
local sauth = luci.sauth
local fs = require "nixio.fs"
local config = require("luci.config")
config.sauth = config.sauth or {}
local sessionpath = config.sauth.sessionpath
if sauth.sane() then
local id
for id in fs.dir(sessionpath) do
sauth.kill(id)
end
end
end
login
调用
includeXxs
过滤危险字符调用
checkPasswd
,checkPasswd
里面有调用了cmd.devSta.get
,cmd.devSta.get
又调用了doParams
会对于未检查到的特殊字符进行进一步过滤故这个也没有可以利用的漏洞
function login(params)
local disp = require("luci.dispatcher")
local common = require("luci.modules.common")
local tool = require("luci.utils.tool")
if params.password and tool.includeXxs(params.password) then --检查输入的密码是否为空,并调用includeXxs函数(就是检查密码中是否含有[`&$;|]这些特殊字符)
tool.eweblog("INVALID DATA", "LOGIN FAILED")
return
end
local authOk
local ua = os.getenv("HTTP_USER_AGENT") or "unknown brower (ua is nil)"
tool.eweblog(ua, "LOGIN UA")
local checkStat = { -- 创造一个结构题,但我们只能控制password字段
password = params.password,
username = "admin", -- params.username,
encry = params.encry,
limit = params.limit
}
local authres, reason = tool.checkPasswd(checkStat)--调用checkPasswd
...
checkNet
检查了
host
的合法性,并拼接了一下字符
ction checkNet(params)
if params.host then
local tool = require("luci.utils.tool")
if string.len(params.host) > 50 or not tool.checkIp(params.host) then --过滤"^[\.%d:%a]+$"
return {connect = false, msg = "host illegal"}
end
local json = require "luci.json"
local _curl =
'curl -s -k -X POST \'http://%s/cgi-bin/luci/api/auth\' -H content-type:application/json -d \'{"method":"checkNet"}\'' %
params.host --拼接字符
...
merge
调用
devSta.set
并将params
当成data
传入,没有看到有什么过滤,下面就是查找命令执行
function merge(params)
local cmd = require "luci.modules.cmd"
return cmd.devSta.set({device = "pc", module = "networkId_merge", data = params, async = true})
end
devSta.set
devSta[opt[i]] = function(params)
local model = require "dev_sta"
params.method = opt[i]
params.cfg_cmd = "dev_sta"
local data, back, ip, password, shell = doParams(params)--过滤的只是最简单的
return fetch(model.fetch, shell, params, opt[i], params.module, data, back, ip, password)
end
fetch
local function fetch(fn, shell, params, ...)
require "luci.json"
local tool = require "luci.utils.tool"
local _start = os.time()
local _res = fn(...) --调用fn函数,也就是model.fetch
...
model.fetch
最终将参数都传递给了
/usr/lib/lua/libuflua.so
中的client_call
函数
function fetch(cmd, module, param, back, ip, password, force, not_change_configId, multi)
local uf_call = require "libuflua"
local ctype
...
local stat = uf_call.client_call(ctype, cmd, module, param, back, ip, password, force, not_change_configId, multi)
return stat
end
下面就是找一下client_call
的定义
首先就是在libuflua.so
找一下client_call
的具体名字是uf_client_call
先将传入的
data
等字段转为Json
格式的数据,作为param
字段的内容。然后将Json
数据通过uf_socket_msg_write
用socket
套接字(分析可知,此处采用的是本地通信的方式)进行数据传输。
if ( !a3 ) // 判断是否有调用了方法
{
...
}
v4 = json_object_new_object();//创建一个JSON对象用于存放键对值
...
switch ( *(_DWORD *)a1 )//这里的a1是ctype在dev_sta.lua文件中赋值为2
{
...
case 2:
v7 = ((int (*)(void))strlen)() + 8;
v8 = calloc(v7, 1);
v9 = 433;
if ( !v8 )
goto LABEL_20;
v10 = v8;
v11 = v7;
v12 = "devSta.%s";
goto LABEL_22;
...
}
...
LABEL_22: // 开始为各种字段设置相应字段
...
v18 = json_object_new_string(v8); //创建一个新的JSON字符串对象
free(v8);
if ( !v18 )
{
...
}
json_object_object_add(v4, "method", v18);//向JSON对象中存放一个键对值,其中键是字符串 "method",值是v18(v18一般是一个结构体的地址)
v19 = json_object_new_object();
if ( !v19 )
{
...
}
v20 = json_object_new_string(*(_DWORD *)(a1 + 8));
if ( !v20 )
{
...
}
json_object_object_add(v19, "module", v20);
v21 = *(_DWORD *)(a1 + 20);
if ( !v21 )
goto LABEL_34;
v22 = json_object_new_string(v21);
if ( !v22 )
goto LABEL_40;
json_object_object_add(v19, "remoteIp", v22);
LABEL_34:
v23 = *(_DWORD *)(a1 + 24);
if ( v23 )
{
...
json_object_object_add(v19, "remotePwd", v24);
}
if ( *(_DWORD *)(a1 + 36) )
{
v25 = json_object_new_int();
...
json_object_object_add(v19, "buf", v25);
}
if ( *(_DWORD *)a1 )
{
...
}
else
{
...
}
v26 = *(unsigned __int8 *)(a1 + 45);
LABEL_56:
if ( v26 )
{
v31 = json_object_new_boolean(1);
if ( v31 )
json_object_object_add(v19, "from_url", v31);
}
if ( *(_BYTE *)(a1 + 47) )
{
v32 = json_object_new_boolean(1);
if ( v32 )
json_object_object_add(v19, "from_file", v32);
}
if ( *(_BYTE *)(a1 + 48) )
{
v33 = json_object_new_boolean(1);
if ( v33 )
json_object_object_add(v19, "multi", v33);
}
if ( *(_BYTE *)(a1 + 46) )
{
v34 = json_object_new_boolean(1);
if ( v34 )
json_object_object_add(v19, "not_commit", v34);
}
...
v36 = *(_BYTE **)(a1 + 12);
if ( !v36 || !*v36 )
goto LABEL_75;
v37 = json_object_new_string(v36);
if ( !v37 )
goto LABEL_78;
json_object_object_add(v19, "data", v37);
LABEL_75:
v38 = *(_BYTE **)(a1 + 16);
if ( v38 && *v38 )
{
v39 = json_object_new_string(v38);
if ( !v39 )
{
...
}
json_object_object_add(v19, "device", v39);
}
json_object_object_add(v4, "params", v19);
v40 = json_object_to_json_string(v4);
if ( !v40 )
{
...
}
v41 = uf_socket_client_init(0);
if ( v41 <= 0 )
{
...
}
v46 = strlen(v40);
uf_socket_msg_write(v41, v40, v46);//通过uf_socket_msg_write使用socket进行数据传输
json_object_put(v4);
...
while ( 1 )
{
...
}
else
{
...
}
}
下面就是找一下接收的地方
二进制文件流程分析
说一下这个程序的流程,因为代码量有点大就不再一一说明,主要就是调试,只要掌握了调试的技巧其实是很快的,在调试的时候从分支三开始调试,前两个分支只可以下断点到被阻塞之后的函数,因为前面的函数都是初始化的函数执行的比较早,想看具体细节的可以看我上传的附件里面也有解密后的固件
链接:https://pan.baidu.com/s/1rGHH2FCBWMptIdgd3rF6WQ?pwd=ubm1
提取码:ubm1
分支一
从
main
首先进入ufm_init
这个函数中,接下来的流程如下
ufm_init
ufm_thd_init
sub_41AFC8
会执行sem_wait(&unk_4360A8);
阻塞当前进程,直到在分之二中执行async_cmd_push_queue
被唤醒sub_41ADF0
ufm_popen
触发漏洞
分支二
uf_cmd_task_init()
deal_remote_config_handle
uf_task_remote_pop_queue
进入这个函数会阻塞当前进程(sem_wait(&unk_435E90);
)需要触发在分之二中的sub_40b0b0
去唤醒然后才会执行下面的函数uf_cmd_call
ufm_handle
sub_40FD5C
sub_40CEAC
:v72 += snprintf(&v66[v70], v68, ``" '%s'"``, v71);
//这里存在了命令注入,data字段的值为我们可控,造成了任意命令拼接到原本的字符串上(第243行)ufm_commit_add
(第264行)async_cmd_push_queue
: 会执行sem_post(&unk_4360A8);
就是唤醒分支一的sub_41AFC8
进程
分支三
在
main
会执行下面三个函数
uf_socket_msg_read
:接收json
字符串
parse_content
:在这个函数中会执行两个函数1、parse_obj2_cmd
作用就是解析字段,将接收的json
字符串解析为json
数组,2、pkg_add_cmd
它的核心作用就是在a1
这个数据结构中记录了v16
的指针,使得后续操作通过a1
访问到刚刚解析出来的各个字段
add_pkg_cmd2_task
在这个函数中会有下面一个调用链
sub_40B304
94行sub_40B0B0
32行 在这个函数中会执行sem_post(&unk_435E90);
唤醒分支二的uf_task_remote_pop_queue
json的几种格式
- 1 表示 JSON 对象(
json_type_object
)- 2 表示 JSON 数组(
json_type_array
)- 3 表示 JSON 字符串(
json_type_string
)- 4 表示 JSON 整数(
json_type_int
)- 5 表示 JSON 双精度浮点数(
json_type_double
)- 6 表示 JSON 布尔值(
json_type_boolean
)- 7 表示 JSON 空值(
json_type_null
)
uf_socket_msg_read
接受的数据
调试
这个就是顺便提一下,必须是在系统级下进行调试,还是老样子
传进入一个 [gdbserver
](gdb-static-cross/prebuilt/gdbserver-7.7.1-mipsel-ii-v1 at master · stayliv3/gdb-static-cross · GitHub)
gdbserver 0.0.0.0:9999 --attach PID
在外边启动
gdb-multiarch
输入target remote 192.168.195.153:9999
参考
[原创]站在巨人肩膀上复现CVE-2023-34644-智能设备-看雪-安全社区|安全招聘|kanxue.com
[原创] 记一次全设备通杀未授权RCE的挖掘经历-智能设备-看雪-安全社区|安全招聘|kanxue.com
标签:...,34644,--,object,json,2023,CVE,local,luci From: https://www.cnblogs.com/trunk/p/17732179.html