CVE-2019-17059

Catalpa 网络安全爱好者

2019 年 10 月 1 日,CVE 官方发布了编号为 CVE-2019-17059 的漏洞:Cyberoam 防火墙任意代码执行。本文对此漏洞进行分析。

漏洞的发现者在 2019 年 10 月 7 日左右发布了关于此漏洞的具体信息 ,但文章中隐藏了很多关键细节。

A. 数据处理流程

Cyberoam 底层使用一个修改过的 Linux 系统,整体框架设计比较复杂,包含一个 JAVA 编写的前端服务,以及 Embedded perl 编写的后端服务,前后端通过一种自定义的类 HTTP 格式数据进行通信。

A.1 前端

netstat 查看端口监听情况,发现监听 80 端口的服务启动命令为

1
apache -d /_conf/httpd

来到 /_conf/httpd/conf 目录下可以找到 apache 相关配置文件 httpd.conf,其内部会引用子文件:

1
Include /cfs/web/apache/httpd.conf

/cfs/web/apache/httpd.conf,内容如下

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
Listen 80
<VirtualHost *:80>
ServerName manage.cyberoam
<Proxy *>
AddDefaultCharset utf-8
</Proxy>
RewriteEngine On
RewriteRule /WEB-INF/* - [F]
RewriteRule /properties/* - [F]
RewriteRule ^/$ /corporate/webpages/login.jsp [R=302]
ErrorDocument 500 /error/INTERNAL_SERVER_ERROR.html
ErrorDocument 503 /error/APP_SERVER_NOT_AVAILABLE.html
ErrorDocument 404 /error/FILE_NOT_EXISTS.html
ProxyPass /corporate/images !
ProxyPass /corporate/dg !
ProxyPass /corporate/APIController !
ProxyPass /corporate/iview/images !
ProxyPass /corporate/iview/css !
ProxyPass /corporate/iview/javascript !
ProxyPass /corporate/iview/FusionCharts !
ProxyPass /corporate/iview/graycss !
ProxyPass /corporate/iview/grayimages !
ProxyPass /corporate/iview/defaultcss !
ProxyPass /corporate/iview/defaultimages !
ProxyPass /corporate/iview/lite1css !
ProxyPass /corporate/iview/lite1images !
ProxyPass /corporate/iview/lite2css !
ProxyPass /corporate/iview/lite2images !
ProxyPass /corporate/iview/jqplot !
ProxyPass /corporate http://localhost:8009/corporate
ProxyPassReverse /corporate http://localhost:8009/corporate
ProxyPass /corporate/iview http://localhost:8009/corporate/iview
ProxyPassReverse /corporate/iview http://localhost:8009/corporate/iview
RewriteRule ^/portal(/)?$ - [F]
RewriteRule /corporate/webpages/tportal/* - [F]
ProxyPass /registrationserver http://cyberoamupdate.cyberoam.com/demoregserver/servlet/CyberoamSyncManager retry=0
ProxyPassReverse /registrationserver http://cyberoamupdate.cyberoam.com/demoregserver/servlet/CyberoamSyncManager
ProxyPass /notificationmsg http://cyberoamregistration.cyberoam.com/datetime/notificationmsg.jsp retry=0
ProxyPassReverse /notificationmsg http://cyberoamregistration.cyberoam.com/datetime/notificationmsg.jsp
ProxyPreserveHost On
RewriteEngine On
Rewritecond %{request_method} !^(GET|POST)$
RewriteRule .* - [F]
ExpiresActive On
ExpiresDefault "access plus 1 month"
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</VirtualHost>

这里看到监听外部 80 端口的服务配置,当用户访问 /corporate 接口时,请求会被转发到 http://localhost:8009/corporate,tomcat 监听此接口,启动命令:

1
tomcat -X mx64M -X ms12M -X ss128k -X asyncgc -X noinlining -X replication:none -X compactalways -D java.io.tmpdir=/tmp-Djava.awt.headless=true -D jetty.class.path=/bin/jetty/lib/javax.servlet.jar:/bin/jetty/lib/org.mortbay.jetty.jar:/bin/jetty/webapps/corporate/properties:/bin/jetty/webapps/corporate/jar/jta26.jar:/bin/jetty/webapps/reports/properties -X bootclasspath:/bin/jamvm/share/jamvm/classes:/bin/classpath/share/classpath -D STOP.PORT=-1 -jar /bin/jetty/start.jar /bin/jetty/etc/jetty.xml

jetty.xml 部分配置

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
<?xml version="1.0"?> 
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http:/>

<!-- =============================================================== -->
<!-- Configure the Jetty Server -->
<!-- =============================================================== -->
<Configure class="org.mortbay.jetty.Server">

<!-- =============================================================== -->
<!-- Increase Maximum Form Content Size -->
<!-- =============================================================== -->
<Call class="java.lang.System" name="setProperty">
<Arg>org.mortbay.http.HttpRequest.maxFormContentSize</Arg>
<Arg>4194304</Arg>
</Call>

<!-- =============================================================== -->
<!-- Configure the Request Listeners -->
<!-- =============================================================== -->

<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add and configure a HTTP listener to port 8080 -->
<!-- The default port can be changed using: java -Djetty.port=80 -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.SocketListener">
<Set name="Port"><SystemProperty name="jetty.port" default="8009"/></S>
<Set name="PoolName">P1</Set>
<Set name="MinThreads">20</Set>
<Set name="MaxThreads">200</Set>
<Set name="lowResources">50</Set>
<Set name="MaxIdleTimeMs">30000</Set>
<Set name="LowResourcePersistTimeMs">2000</Set>
<Set name="acceptQueueSize">0</Set>
<Set name="ConfidentialPort">8443</Set>
<Set name="IntegralPort">8443</Set>
</New>
</Arg>
</Call>

<Set name="WebApplicationConfigurationClassNames">
<Array type="java.lang.String">
<Item>org.mortbay.jetty.servlet.XMLConfiguration</Item>
<Item>org.mortbay.jetty.servlet.JettyWebConfiguration</Item>

<!--
<Item>org.mortbay.jetty.servlet.TagLibConfiguration</Item>
<Item>org.mortbay.jetty.servlet.jsr77.Configuration</Item>
-->
</Array>
</Set>

<!-- =============================================================== -->
<!-- Configure the Contexts -->
<!-- =============================================================== -->

<Set name="rootWebApp">root</Set>

<Call name="addWebApplications">
<Arg></Arg>
<Arg><SystemProperty name="jetty.home" default="."/>/webapps/</Arg>
<Arg><SystemProperty name="jetty.home" default="."/>/etc/webdefault.xml</A>
<Arg type="boolean">true</Arg><!--extract WARs-->
<Arg type="boolean">false</Arg><!-- java 2 compliant class loader -->
</Call>

<!-- =============================================================== -->
<!-- Configure the Request Log -->
<!-- =============================================================== -->
<Set name="RequestLog">
<New class="org.mortbay.http.NCSARequestLog">
<Arg><SystemProperty name="logs" default="/" />/log/tomcat.log</Arg>
<Set name="retainDays">0</Set>
<Set name="append">true</Set>
<Set name="extended">false</Set>
<Set name="buffered">false</Set>
<Set name="LogTimeZone">GMT</Set>
</New>
</Set>

</Configure>

这里找到 web 目录在 /bin/jetty/webapps/corporate/ 下,web.xml 部分配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<servlet-mapping>                                   
<servlet-name>Controller</servlet-name>
<url-pattern>/Controller</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>APIController</servlet-name>
<url-pattern>/APIController</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Controller</servlet-name>
<url-pattern>/sslvpnuserportal/manager/CRSSLAuthenticationManager</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RefreshServlet</servlet-name>
<url-pattern>/corporate/servlet/RefreshServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>TelnetServlet</servlet-name>
<url-pattern>/corporate/servlet/TelnetServlet</url-pattern>
</servlet-mapping>

从配置中可以看到前端有几个可访问的接口,其中 /Controller 比较关键,它对应的类定义如下

1
2
3
4
5
<servlet>
<servlet-name>Controller</servlet-name>
<servlet-class>cyberoam.corporate.servlets.CyberoamCommonServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

反编译这个类,部分代码:

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
// ...
try {
int1 = Integer.parseInt(httpServletRequest2.getParameter("mode"));
}
catch (Exception ex2) {
int1 = -447;
}
// ...
if (!eventByMode.getOPCode().equals("login") && !eventByMode.getOPCode().equals("iview_login") && eventByMode.getMode() != 500 && eventByMode.getMode() != 172 && eventByMode.getMode() != 404 && eventByMode.getMode() != 34 && eventByMode.getMode() != 405 && !eventByMode.getOPCode().equals("login_report") && eventByMode.getMode() != 451 && eventByMode.getMode() != 458 && !eventByMode.getOPCode().equals("ccc_login") && eventByMode.getMode() != 603 && eventByMode.getMode() != 746 && eventByMode.getMode() != 958 && eventByMode.getMode() != 605 && eventByMode.getMode() != 531) {
if (eventByMode.getMode() != 720 && CheckSession.isAdminSessionLock(httpServletRequest2)) {
httpServletResponse.getWriter().print("{\"status\":597,message:\"Session is Lock\"}");
CyberoamLogger.debug("CSC", "Lock Response is: 597");
return;
}
if (CheckSession.isSessionExpire(httpServletRequest2)) {
httpServletResponse.sendRedirect(String.valueOf(httpServletRequest2.getContextPath()) + "/webpages/SessionExpired.jsp");
return;
}
}
// ...
if (eventByMode.getRequesttype() == 1) {
transactionBean.setTransactionID(CSCClient.getTransactionID());
CyberoamAjaxHelper.process(httpServletRequest2, httpServletResponse, eventByMode, transactionBean, sqlReader);
}
else if (eventByMode.getRequesttype() == 2) {
transactionBean.setTransactionID(CSCClient.getTransactionID());
CyberoamCustomHelper.process(httpServletRequest2, httpServletResponse, eventByMode, transactionBean, sqlReader);
}

这段代码从用户请求中获取 mode 参数的值,然后进行数个判断,除特定的一些值以外,都需要进行 session 验证。

如果通过验证,则调用 CyberoamCustomHelper.process 函数,此函数逻辑复杂,简单来讲,它会进一步解析用户提交的请求,根据 mode 值的不同进行不同的处理,以无需身份验证的 458 功能为例,它会调用函数 QuarantineDownloadUtility.handleReleaseRequestFromMail,部分代码:

1
2
3
4
5
6
7
8
9
10
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
// ...
hashMap.put("release", paramHttpServletRequest.getParameter("release"));
// ...
hashMap.put("___username", str2);
hashMap.put("currentlyloggedinuserip", paramHttpServletRequest.getHeader("X-FORWARDED-FOR"));
// ...
CyberoamLogger.debug("Spam Mail", "Calling opcode --> " + eventBean.getOPCode());
i = cSCClient.sendWizardEvent(eventBean, hashMap, paramSqlReader);
// ...

之后会调用 cSCClient.sendWizardEvent

1
2
3
// ...
i = send(paramEventBean, jSONObject, paramHttpServletRequest, paramSqlReader);
// ...

这里来到关键位置,send 函数内部会构造出之前提到的类 HTTP 格式数据,并转发到 127.0.0.1 299 端口。

A.2 后端

查看端口开放信息,找到监听 299 的服务是 csc 二进制文件,启动命令为

1
csc -L 3 -c /_conf/csc/csc.conf

在 /_conf/csc/ 目录下可以找到很多 .conf 文件,每个文件中都是一种类似 perl 的伪代码,通过 IDA 反编译分析 csc 发现程序引用了大量 perl 相关函数

经调查 csc 可能修改了原始 perl,自己实现了一套新的 perl 解释器,并在其上添加了格式解析相关逻辑。

前端所有功能发送到后端之后,都会先调用 apiInterface 接口,此接口在 perl 层面实现了鉴权、参数过滤、请求分发等操作。

我们可以利用 tcpdump 截获前后端之间的通信流量

1
tcpdump -i lo -A port 299

样例请求和响应

1
2
3
4
5
opcode getCustomerInfo csc/1.2
content-type:json
content-length:118

{"APIVersion":"063.051","___component":"LOCAL","___username":"LOCAL","mode":370,"currentlyloggedinuserip":"127.0.0.1"}
1
2
3
4
csc/1.2 200 OK
content-length:74

NULL###NULL###NULL###NULL###NULL###NULL###NULL###NULL###NULL###NULL###NULL

以这个请求为例,在 csc 目录下搜索字符串 getCustomerInfo 可以找到此功能的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
OPCODE getCustomerInfo {
<code>
$modulename=$request->{modulename};
$custinfo="fail";
</code>
custinfo=DLOPEN(do_nvram_get,customer.info)

ON_FAIL{
LOG applog "getCustomerInfo opcode failed\n"
REPLY text "fail" 500
}
REPLY text $custinfo 200
}

其余功能的调用类似,都是以类 HTTP 形式发送到 299 端口,然后 csc 找到对应 perl 函数进行调用。

A.3 mode 定义

代码中有很多 mode,每个 mode 对应 perl 中的不同函数,具体定义可以在 java Modes.class 中找到,部分定义如下

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
public static final int ACCESS_TIME_POLICY_ADD = 1;

public static final int ACCESS_TIME_POLICY_EDIT = 2;

public static final int SURFING_QUOTA_POLICY_ADD = 3;

public static final int SURFING_QUOTA_POLICY_EDIT = 4;

public static final int NAT_POLICY_ADD = 5;

public static final int NAT_POLICY_EDIT = 6;

public static final int DATA_TRANSFER_POLICY_ADD = 7;

public static final int DATA_TRANSFER_POLICY_EDIT = 8;

public static final int ACCESS_TIME_POLICY_DELETE = 10;

public static final int UNICAST_ROUTE_ADD = 11;

public static final int UNICAST_ROUTE_EDIT = 12;

public static final int UNICAST_ROUTE_DELETE = 13;

public static final int MULTICAST_ROUTE_ADD = 14;

public static final int MULTICAST_ROUTE_EDIT = 15;

public static final int MULTICAST_ROUTE_DELETE = 16;

B. 漏洞分析

根据披露信息,存在漏洞的功能 mode = 458,在 mode 定义中找到功能名称叫做 RELEASE_QUARANTINE_MAIL_FROM_MAIL,同时在 CyberoamCommonServlet 类中可以看到这个功能无需身份验证即可访问。

前端中对此功能处理逻辑

  1. CyberoamCustomHelper
1
2
3
if (eventBean.getMode() == 458) {
new QuarantineDownloadUtility().handleReleaseRequestFromMail(httpServletRequest, httpServletResponse, sqlReader);
}
  1. QuarantineDownloadUtility
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
public void handleReleaseRequestFromMail(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse, SqlReader paramSqlReader) {
int i = 1;
String str = null;
JSONObject jSONObject = new JSONObject();
boolean bool = true;
try {
str = paramHttpServletRequest.getParameter("release");
String str1 = decode(str);
String[] arrayOfString1 = str1.split("&");
byte b;
int j;
String[] arrayOfString2;
for (j = (arrayOfString2 = arrayOfString1).length, b = 0; b < j; ) {
String str2 = arrayOfString2[b];
String[] arrayOfString = str2.split("=");
if (arrayOfString.length > 2)
bool = false;
jSONObject.put(arrayOfString[0], (arrayOfString.length > 1) ? arrayOfString[1] : "");
b++;
}
if ((jSONObject.getString("hdnSender").equals("") || validateEmail(jSONObject.getString("hdnSender"))) &&
validateEmail(jSONObject.getString("hdnRecipient")) && isSafeFilePath(jSONObject.getString("hdnFilePath")) &&
bool) {
paramHttpServletResponse.setContentType("text/html");
CyberoamLogger.debug("Antivirus/AntiSpam", "CSC Constant value " + CSCConstants.isCCC);
CSCClient cSCClient = new CSCClient();
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
EventBean eventBean = EventBean.getEventByMode(363);
hashMap.put("release", paramHttpServletRequest.getParameter("release"));
if (paramHttpServletRequest != null) {
String str2;
arrayOfString2 = null;
HttpSession httpSession = paramHttpServletRequest.getSession(false);
if (httpSession != null) {
SessionBean sessionBean = (SessionBean)httpSession.getAttribute("sessionbean");
if (sessionBean != null) {
str2 = sessionBean.getUserName();
hashMap.put("___component", CSCConstants.COMPONENT_VALUE);
}
}
if (str2 == null)
str2 = "-";
hashMap.put("___username", str2);
hashMap.put("currentlyloggedinuserip", paramHttpServletRequest.getHeader("X-FORWARDED-FOR"));
} else {
hashMap.put("___username", "-");
}
CyberoamLogger.debug("Spam Mail", "Calling opcode --> " + eventBean.getOPCode());
i = cSCClient.sendWizardEvent(eventBean, hashMap, paramSqlReader);
CyberoamLogger.debug("Spam Mail", "Status After Call--> " + i);
if (i == 200) {
i = 1;
} else if (i == 547) {
i = -2;
} else if (i == 548) {
i = -3;
} else {
i = -1;
}
} else {
i = -3;
}
} catch (Exception exception) {
CyberoamLogger.error("Antivirus/AntiSpam", "Exception occured while releaseing mail from Spam Quarantined Digest Mail: " + exception, exception);
i = -1;
}
CyberoamLogger.debug("Antivirus/AntiSpam", "Mail is released for mode : " + paramHttpServletRequest.getParameter("mode"));
try {
paramHttpServletResponse.setContentType("text/html");
PrintWriter printWriter = paramHttpServletResponse.getWriter();
if (i > 0) {
printWriter.write("<font>The released email will be scanned and delivered.</font>");
} else if (i == -2) {
printWriter.write("<font>Email has been released or deleted.</font>");
} else if (i == -3) {
printWriter.write("<font>Bad Request.</font>");
} else {
printWriter.write("<font>Error while releasing email</font>");
}
} catch (Exception exception) {}
}
  1. cSCClient.sendWizardEvent 发送到 299 端口

QuarantineDownloadUtility 函数中从请求中获取 release 参数,然后对其进行 base64 解码,解码后进行数个参数判断,如果全部通过则执行

1
EventBean eventBean = EventBean.getEventByMode(363);

这里会将 mode 重新设置为 363,从定义中得知 363 对应接口名称为 SEND_MAIL。

从 perl 伪代码中搜索能找到 send_mail 函数,部分代码:

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
opcode send_mail{
$reason = "";
$strSubject="";
</code>

IF("defined $request->{release} and $request->{release} ne '' "){
<code>$param = $request->{release};</code>
param = DLOPEN(base64_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";
}
</code>
IF("$validate_email eq 'false'"){
<code>%response=("status"=>"548","statusmessage"=>"Invalid URL");</code>
REPLY %response 500
}
}
ELSE{
// ...
}
// ...
LOG applog "Values-->\n"
LOG applog "Mail Server: --> $mailServerHost \n"
LOG applog "File Path: --> $file \n"
LOG applog "Mail From: --> $mailFrom \n"
LOG applog "Mail To: --> $mailTo \n"
LOG applog "Subject: --> $strSubject \n"
IF(" -e $file"){
<code>
$file = $file .":message/rfc822";
%mailreq=("mailaction"=>"$MAIL_FORWARD","subject"=>"$strSubject","toEmail"=>"$mailTo","attachmentfile"=>"$file","smtpserverhost"=>"$mailServerHost","fromaddress"=>"$mailFrom");
</code>

out = OPCODE mail_sender json %mailreq
// ...
}
// ...
}

代码中首先判断 release 参数是否为空,然后对其进行 base64 解码,以 $= 将字符串分割,获取 hdnDestDomain、hdnSender、hdnRecipient、hdnFilePath 四个参数并赋值到不同的变量。

接着利用正则表达式过滤 hdnRecipient 和 hdnSender 参数,验证邮箱格式是否正确,不正确直接返回 500 错误。

如果验证通过,就调用 LOG 打印这些变量的内容,然后利用 -e 判断 $file 即 $QUARANTINE_PATH.”/“.$requestData{hdnFilePath} 文件是否存在,其中 $QUARANTINE_PATH 是一个常量,定义为 /var/quarantine。

如果文件存在,就调用 mail_sender 函数

1
2
3
4
5
6
7
8
9
10
11
12
OPCODE mail_sender{
// ...
LOG applog "MAIL_SEND: Values-->\n Mail Server: --> $smtpserverhost\n Mail From: --> $fromaddress\n Mail To: --> $toEmail\n Subject: --> $subject\n Username: --> $mailusername\n MailAction:--> $mailaction\n SMTPSecurityMode:--> $smtpsecuritymode\n SMTPCertificate:-->$smtpcertificate\n Appliance IP(LAN):--> $ip\n"
IF("$mailaction eq $MAIL_WITH_VAR"){
out = EXEC /bin/cschelper 'mail_send' $fromaddress $fromaddresswithname $toEmail $toEmail $subject "" $smtpserverhost $smtpserverport $mailusername $mailpassword $mailaction $smtpsecuritymode $smtpcertificate $certpassword "1" "$mailbody" "text/html"
}ELSE IF("$mailaction eq $MAIL_FORWARD"){
out = EXECSH "/bin/cschelper mail_send '$fromaddress' '$fromaddresswithname' '$toEmail' '$toEmail' '$subject' '$mailbody' '$smtpserverhost' '$smtpserverport' '$mailusername' '$mailpassword' '$mailaction' '$smtpsecuritymode' '$smtpcertificate' '$certpassword' '1' '$attachmentfile'"
}ELSE IF("$mailaction eq $MAIL_ATTACHMENT"){
out = EXECSH "/bin/cschelper mail_send '$fromaddress' '$fromaddresswithname' '$toEmail' '$toEmail' '$subject' '$mailbody' '$smtpserverhost' '$smtpserverport' '$mailusername' '$mailpassword' '$mailaction' '$smtpsecuritymode' '$smtpcertificate' '$certpassword' '1' '$attachmentfile'"
}
// ...
}

此函数头部进行很多初始化操作,随后根据 $mailaction 值的不同执行不同逻辑,在 send_mail 函数中可以看到 传入的 $mailaction 值应该是 $MAIL_FORWARD,那么这里会执行

1
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,同时这条命令中拼接了我们可控的 $smtpserverhost,存在命令注入漏洞。

C. 漏洞触发

在触发漏洞的路径上还存在几个问题

C.1 绕过文件判断

在调用 mail_sender 函数之前存在一个对 $file 是否存在的判断,其中 $file 参数是 $QUARANTINE_PATH 和用户提交的 hdnFilePath 两个变量拼接而来,也就是说只有$file 存在时,才能触发 mail_sender 功能,$QUARANTINE_PATH 变量的值为 /var/quarantine,而通常情况下攻击者无法得知此目录下有哪些文件。

为了绕过这个判断,我们可以提交 hdnFilePath=/,拼接后得到 $file = /var/quarantine/,而这里就变成了判断 /var/quarantine/ 目录是否存在,显然可以通过。

C.2 命令注入

在注入命令的时候,相关代码如下

1
2
3
ELSE IF("$mailaction eq $MAIL_FORWARD"){
out = EXECSH "/bin/cschelper mail_send '$fromaddress' '$fromaddresswithname' '$toEmail' '$toEmail' '$subject' '$mailbody' '$smtpserverhost' '$smtpserverport' '$mailusername' '$mailpassword' '$mailaction' '$smtpsecuritymode' '$smtpcertificate' '$certpassword' '1' '$attachmentfile'"
}

其中 $smtpserverport 变量是用户可控的 hdnDestDomain,但单纯使用反引号或者分号不能执行命令,需要先使用单引号将待注入的命令闭合才可以。

C.3 日志调试

perl 部分没有很好的方法可以调试,但在代码中存在很多 LOG 打印日志信息,我们可以在 /var/tslog/applog.log 文件中找到这些信息,通过调试信息辅助分析。

C.4 漏洞触发演示

asciicast

  • Title: CVE-2019-17059
  • Author: Catalpa
  • Created at : 2021-12-14 00:00:00
  • Updated at : 2024-10-17 08:46:20
  • Link: https://wzt.ac.cn/2021/12/14/CVE-2019-17059/
  • License: This work is licensed under CC BY-NC-SA 4.0.