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 漏洞触发演示