本文是关于影响了 Cyberoam SSL VPN(也可称为 CyberoamOS)的远程命令执行漏洞的一些基本说明。
这个 Cyberoam 漏洞被标记为CVE-2019-17059
,它是一个高危漏洞,攻击者可利用它在未经授权的情况下直接控制 Cyberoam 设备。最重要的是,攻击者的访问权限是 root,这代表给攻击者可以完全控制 Cyberoam 设备。
由于在大多数网络环境中,Cyberoam 设备都被用作防火墙或 SSL VPN,一旦攻击者完全控制设备,就可对目标内网进行大面积攻击。而且在一般情况下,Cyberoam 设备通常在安全“白名单”中,这就帮助攻击者更好地绕过其他安全防护。
根据 Shodan 的数据,全世界有超过 96000 个位于互联网的 Cyberoam 设备。这些设备大多位于企业、大学和一些世界知名银行地网络中,一旦沦陷,可能会造成大量经济损失。
与 Sophos 安全团队合作是一件非常愉快的事情,因为他们在接到报告后很快就意识到了严重性,并立刻发布了补丁。
CyberoamOS 远程命令执行
CyberoamOS 是一种基于 linux 的改进版操作系统,主要用于 Cyberoam 设备。该操作系统具有基于 web 的配置接口和一个 SSL VPN 入口。
网页界面主要分为两部分:
- 用 Java 编写的前端
- 使用 C 和 Perl 编写的后端
我们在此不会深入研究前端或后端代码的内部机制,而是简要讨论如何触发这个漏洞。
配置接口和 SSL VPN 入口都有一个处理主要操作的 servlet。这些操作是通过参数 mode 定义的。
其中大多数操作是需要身份验证的,但有一些操作可以在不需要身份验证的情况下进行(比如登录操作)。
我们发现的漏洞位于电子邮件防病毒/反垃圾邮件
模块。此模块的请求代码是 458。
需要注意的是,操作码会对应到 Cyberoam 设备数据库(指内部数据库 Postgres)中的数据。因此通过查找 458,我们可以找到对应操作码的名称。
下面是数据库初始化 SQL 脚本中的一行,展示了 458 对应的操作名称:
insert into tblcrevent(opcode,desc ription,mode,requesttype)
values('RELEASEQUARANTINEMAILFROMMAIL','RELEASE QUARANTINE MAIL FROM MAIL','458',2);
而执行操作的函数存储在/_conf/csc/cscconf/
目录中。我们不会透露存在缺陷的函数的全部代码,但是我们将提供一些代码片段来展示漏洞的位置以及它是如何触发的。
处理操作码 458 的 Java 前端代码
if ((jsonob ject.getString("hdnSender").equals("") ||
validateEmail(jsonob ject.getString("hdnSender"))) &&
validateEmail(jsonob ject.getString("hdnRecipient")) &&
isSafeFilePath(jsonob ject.getString("hdnFilePath")) && b) {
httpServletResponse.setContentType("text/html");
CyberoamLogger.debug("Antivirus/AntiSpam", "CSC Constant value " +
CSCConstants.isCCC);
正如上面的代码所展示的,它检查了几个参数的有效性。如果确认有效,则会发生以下情况:
final EventBean eventByMode = EventBean.getEventByMode(363);
...redacted.
final int sendWizardEvent = cscClient.sendWizardEvent(eventByMode, hashMap, sqlReader);
此时我们有一个新的事件代码(363),它将被发送到后端。而漏洞就存在后端处理这个事件的代码中。
该操作码对应名称为sendmail
,为了避免直接暴露出漏洞,我们只展示部分代码。
send_mail
的处理程序。
...redacted...
<code>$param = $request->{release};</code>
param = DLOPEN(ba se64_decode,param)
LOG applog " Decode values :: $param \n"
<code>%requestData = split(/[&=]/, $param);
$mailServerHost = $requestData{hdnDestDomain};
$mailFrom = $requestData{hdnSender};
$mailTo = $requestData{hdnRecipient};
$file = $QUARANTINE_PATH."/".$requestData{hdnFilePath};
$mailfile=$requestData{hdnFilePath};
$validate_email="false";
my $email_regex='^([\.]?[_\-\!\#\{\}\$\%\^\&\*\+\=\|\?\'\\\\\\/a-zA-Z0-9])*@([a-zA-Z0-9]([-]?[a-zA-Z0-9]+)*\.)+([a-zA-Z0-9]{0,6})$';
if($requestData{hdnRecipient} =~ /$email_regex/ && ((defined $requestData{hdnSender} && $requestData{hdnSender} eq '') || $requestData{hdnSender} =~ /$email_regex/) && index($requestData{hdnFilePath},'../') == -1){
$validate_email="true";
}
....redacted....
正如我们从上面代码看到的,伪 perl 代码向我们展示了后端如何接收输入($requestData),以及它是如何尝试验证所接受的一些参数的。
验证后,如果我们的参数有效,则执行以下代码:
%mailreq=("mailaction"=>"$MAIL_FORWARD","subject"=>"$strSubject","toEmail"=>"$mailTo","attachmentfile"=>"$file","smtpserverhost"=>"$mailServerHost","fromaddress"=>"$mailFrom");
</code>
out = OPCODE mail_sender json %mailreq
以上代码将把变量mailreq
设置为接受的值,并调用mail_sender
函数。我们将看到这其中涉及的操作,以及 RCE 到底在哪里触发:
<code>
#mailaction 0=mail_with_var,1=mail_forward,2=mail_attachment
$mailaction=$request->{mailaction};
$subject=$request->{subject};
$mailbody='';
$attachmentfile=$request->{attachmentfile};
$toEmail=$request->{toEmail};
</code>
#mail body
IF("defined $request->{mailbody} && '' ne $request->{mailbody}"){
<code>$mailbody=$request->{mailbody};</code>
}
#SMTP server host
IF("defined $request->{smtpserverhost} && '' ne $request->{smtpserverhost}"){
<code>$smtpserverhost=$request->{smtpserverhost};</code>
}ELSE{
result = QUERY "select servicevalue from tblclientservices where servicekey='MailServer'"
IF("defined $result->{output}->{servicevalue}[0] && '' ne $result->{output}->{servicevalue}[0]"){
<code>$smtpserverhost=$result->{output}->{servicevalue}[0];</code>
}ELSE{
<code>$smtpserverhost="127.0.0.1";</code>
}
}
#SMTP server port
IF("defined $request->{smtpserverport} && '' ne $request->{smtpserverport}"){
<code>$smtpserverport=$request->{smtpserverport};</code>
}ELSE{
result = QUERY "select servicevalue from tblclientservices where servicekey='MailServerPort'"
IF("defined $result->{output}->{servicevalue}[0] && '' ne $result->{output}->{servicevalue}[0]"){
<code>$smtpserverport=$result->{output}->{servicevalue}[0];</code>
}ELSE{
<code>$smtpserverport="25";</code>
}
}
#SMTP auth flag
<code>$smtpauthflag="0";</code>
IF("defined $request->{smtpauthflag} && '' ne $request->{smtpauthflag}"){
<code>$smtpauthflag=$request->{smtpauthflag};</code>
}ELSE{
result = QUERY "select servicevalue from tblclientservices where servicekey='SMTPAuthenticationFlag'"
IF("defined $result->{output}->{servicevalue}[0] && '' ne $result->{output}->{servicevalue}[0]"){
<code>$smtpauthflag=$result->{output}->{servicevalue}[0];</code>
}
}
IF("$smtpauthflag == 1"){
IF("defined $request->{mailusername} && '' ne $request->{mailusername}"){
<code>
$mailusername=$request->{mailusername};
$mailpassword=$request->{mailpassword};
</code>
}ELSE{
result = QUERY "select servicevalue from tblclientservices where servicekey = 'MailServerUsername'"
<code>$mailusername = $result->{output}->{servicevalue}[0];</code>
result = QUERY "select servicevalue from tblclientservices where servicekey = 'MailServerPassword'"
<code>$mailpassword = $result->{output}->{servicevalue}[0];</code>
}
}ELSE{
<code>
$mailusername = "";
$mailpassword = "";
</code>
}
IF("defined $request->{fromaddress} && '' ne $request->{fromaddress}"){
<code>$fromaddress=$request->{fromaddress};</code>
}ELSE{
result = QUERY "select servicevalue from tblclientservices where servicekey = 'FromAddress'"
<code>$fromaddress = $result->{output}->{servicevalue}[0];</code>
}
#Security Mode
IF("defined $request->{smtpsecurity} && '' ne $request->{smtpsecurity}"){
<code>$smtpsecurity=$request->{smtpsecurity};</code>
}ELSE{
result = QUERY "select servicevalue from tblclientservices where servicekey = 'smtpsecurity'"
<code>$smtpsecurity = $result->{output}->{servicevalue}[0];</code>
}
<code>$smtpsecuritymode=0;</code>
IF("$smtpsecurity eq 'STARTTLS'"){
<code>$smtpsecuritymode=1;</code>
}ELSE IF("$smtpsecurity eq 'SSL/TLS'"){
<code>$smtpsecuritymode=2;</code>
}
#SMTP Certificate
<code>
$smtpcertificate = '';
$certpassword='';
</code>
IF("$smtpsecuritymode!=0"){
IF("defined $request->{smtpcertificate} && '' ne $request->{smtpcertificate}"){
result = QUERY "select certname,password from tblvpncertificate where certid=$request->{smtpcertificate}"
}ELSE{
result = QUERY "select certname,password from tblvpncertificate where certid=(select servicevalue::int from tblclientservices where servicekey = 'smtpcertificate')"
}
<code>
$smtpcertificate = $result->{output}->{certname}[0];
$certpassword=$result->{output}->{password}[0];
</code>
}
#From Address with Name
IF("defined $request->{fromaddresswithname} && '' ne $request->{fromaddresswithname}"){
<code>$fromaddresswithname=$request->{fromaddresswithname};</code>
}ELSE{
<code>$fromaddresswithname = $OEMNAME . " <" . $fromaddress . ">";</code>
}
上面的代码在开始运行时会做一件所有代码都会执行的操作,初始化变量(如果没有指定一些变量,则直接取自设备)。
在分配变量之后,将执行以下代码。
out = EXECSH "/bin/cschelper mail_send '$fromaddress' '$fromaddresswithname' '$toEmail' '$toEmail' '$subject' '$mailbody' '$smtpserverhost' '$smtpserverport' '$mailusername' '$mailpassword' '$mailaction' '$smtpsecuritymode' '$smtpcertificate' '$certpassword' '1' '$attachmentfile'"
这里的使用是EXECSH
,它会调用/bin/sh -c “ARGUMENTS”
。通过控制所需执行的“变量”,我们就可以在未经身份验证的情况下轻松实现远程命令执行。