2019 年 10 月 1 日,CVE 官方发布了编号为 CVE-2019-17059 的漏洞:Cyberoam 防火墙任意代码执行。本文对此漏洞进行分析。
漏洞的发现者在 2019 年 10 月 7 日左右发布了关于此漏洞的具体信息,但文章中隐藏了很多关键细节。
A. 数据处理流程
Cyberoam 底层使用一个修改过的 Linux 系统,整体框架设计比较复杂,包含一个 JAVA 编写的前端服务,以及 Embedded perl 编写的后端服务,前后端通过一种自定义的类 HTTP 格式数据进行通信。
A.1 前端
netstat 查看端口监听情况,发现监听 80 端口的服务启动命令为
来到 /_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 类中可以看到这个功能无需身份验证即可访问。
前端中对此功能处理逻辑
- CyberoamCustomHelper
 
1 2 3
   | if (eventBean.getMode() == 458) {     new QuarantineDownloadUtility().handleReleaseRequestFromMail(httpServletRequest, httpServletResponse, sqlReader); }
  | 
 
- 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) {} }
  | 
 
- 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 漏洞触发演示
