Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save atxsinn3r/0b53cbf12b582b06cda1dc5aa75f6c4e to your computer and use it in GitHub Desktop.
Save atxsinn3r/0b53cbf12b582b06cda1dc5aa75f6c4e to your computer and use it in GitHub Desktop.

CVE-2018-20323: MailCleaner SOAP API Logs_StartTrace Command Injection Vulnerability

MailCleaner is an anti-spam and anti-virus software that functions as a filter SMTP gateway. It comes in two editions: Enterprise and Community. The community edition is open source, and then codebase can be found on Github.

A vulnerability was found by Mehmet Ince, a Turkish researcher, who also submitted the exploit to Metasploit as a module, and this is how the analysis started. The vulnerability is a remote command execution in the Logs_StartTrace SOAP web request, which can be triggered after authentication.

We will review it starting from the attack surface, and go through every major component that is part of the code path, and eventually to the actual vulnerable code.

Publicly, the vulnerability is referred as CVE-2018-20323.

Vulnerable Applications

The community version "Jessie" (2018092601) should be vulnerable to this.

To audit the vulnerable codebase, the easiest way is to checkout the source on Github:

$ git clone https://github.com/MailCleaner/MailCleaner.git
$ cd MailCleaner
$ git checkout a0281293c31b534cc7db4f798346722c321d078e
$ git checkout -b CVE_2018_20323

Vendor Patching

The patch was applied using PHP's escapeshellarg to sanitize the inputs, and was commited to Github as: c2bc42c3df013dbc5b419ae746ea834cf7542399. Although this seems to work fine on Linux, escapeshellarg has a history of security problems specific to Windows that would allow bypasses. This means even after the patch, the SOAP API would potentially still be a threat to Windows users that are running an older version of PHP.

It is also interesting to point out that instead of submitting the patch as a pull request, which is a typical development practice, it was committed directly to master. Usually this would be considered as wreckless, but for a vulnerability patch, this would appear to be an attempt to hide the patch from public attention.

Vulnerability Analysis

The Starting Point

Since a Metasploit module was submitted to Github, our analysis began with a proof-of-concept:

send_request_cgi({
  'method' => 'POST',
  'uri' => normalize_uri(target_uri.path, 'admin', 'managetracing', 'search', 'search'),
  'cookie'    => cookie,
  'vars_post' => {
    'search' => rand_text_alpha(5),
    'domain' => cmd,
    'submit' => 1
}})

In here, cmd stands for command, which means we can inject a system command in the domain parameter of an HTTP POST request. The HTTP request also tells us we're sending the malicious input to path /admin/managetracing/search/search.

The Controller Component

MailCleaner is written as a PHP application using the Zend Framework (based on the model-view-controller architecture), and typically the URL path would give us a hint of the name of the controller, which in this case is: managetracing, so our search begins there.

A quick search on Github tells us the managetracing controller is handled by the ManageTracingController class in www/guis/admin/application/controllers/ManagetracingController.php. A controller may contain multiple actions, and they are pretty easy to identify because they share the same naming convention like this:

public function somethingAction()

In the case of ManageTracingController, it supports these actions: index, search, logextract, downloadtrace. Since our PoC points to search in the URL, obviously we want to look at the search action.

In the PoC, the domain parameter is passed as a way to inject malicious commands. The serach parameters get loaded rather early in searchAction, specifically the last line of this code block:

public function searchAction() {
    $layout = Zend_Layout::getMvcInstance();
    $view=$layout->getView();
    $layout->disableLayout();
    $view->addScriptPath(Zend_Registry::get('ajax_script_path'));
    $view->thisurl = Zend_Controller_Action_HelperBroker::getStaticHelper('url')->simple('index', 'managecontentquarantine', NULL, array());
     
    $request = $this->getRequest();
     
    $loading = 1;
    if (! $request->getParam('load')) {
        sleep(1);
        $loading = 0;
    }
    $view->loading = $loading;
    $view->params = $this->getSearchParams();

Looking at the getSearchParams function (also found in ManagetracingController.php), we can tell this more of a normalizer. The first thing it does is converting an array of supported parameters into a hash:

foreach (array('search', 'domain', 'sender', 'mpp', 'page', 'sort', 'fd', 'fm', 'td', 'tm', 'submit', 'cancel', 'hiderejected') as $param) {
  $params[$param] = '';
  if ($request->getParam($param)) {
    $params[$param] = $request->getParam($param);
  }
}

Let's continue focusing on the domain parameter the PoC uses. About toward the end of the function, the domain parameter is concatnated to a string, and is saved as a regexp key in the $params hash:

if (isset($params['search']) && isset($params['domain'])) {
    $params['regexp'] = $params['search'].'.*@'.$params['domain'];
}

The above piece of information is important, because that gets used later to get our remote code execution.

The Model Component

Back to the previous searchAction function, we then trigger this code path:

$element = new Default_Model_MessageTrace();

... some code here ...

if ($request->getParam('domain') != "") {
  $trace_id = $element->startFetchAll($params);
  $session->trace_id = $trace_id;
}

The startFetchAll call is interesting to us, because it handles our normalized parameters. We can tell startFetchAll comes from the Default_Model_MessageTrace class found in www/guis/admin/application/models/MessageTrace.php, so naturally that's where we investigate first:

public function startFetchAll($params) {
  return $this->getMapper()->startFetchAll($params);
}

Turns out this is just a wrapper to another startFetchAll function, which is also quick to find if you have a decent code editor (in my case is Sublime) to guide you:

public function startFetchAll($params) {
  $trace_id = 0;
  $slave = new Default_Model_Slave();
      $slaves = $slave->fetchAll();
      
      foreach ($slaves as $s) {
        $res = $s->sendSoapRequest('Logs_StartTrace', $params);
        if (isset($res['trace_id'])) {
          $trace_id = $res['trace_id'];
              $params['trace_id'] = $trace_id;
        } else {
                      continue;
        }
      }
      return $trace_id;
}

The SOAP Web API

So the above code tells us our parameters get sent via a SOAP request, specifically to the Logs_StartTrace handler. A quick grep for that in the codebase tells us that all the SOAP API can be found in the /www/soap directory, and we come up these results:

$ grep -R Logs_StartTrace *
application/MCSoap/Logs.php:	static public function Logs_StartTrace($params) {
application/SoapInterface.php:	static public function Logs_StartTrace($params) {
application/SoapInterface.php:		return MCSoap_Logs::Logs_StartTrace($params);

In auditing, you should probably always look at the interface first to make sure you're following the flow correctly. And the SOAP interface basically tells us go look at the static function in MCSoap_Logs:

static public function Logs_StartTrace($params) {
    return MCSoap_Logs::Logs_StartTrace($params);
}

OK, so let's look at the static Logs_StartTrace function. The code looks a bit more complicated, but pay close attention to the regexp parameter, and you will notice that it gets absorbed into a $cmd variable, and this variable later gets executed as a system command. See second line from the bottom, before the return statement:

static public function Logs_StartTrace($params) {
  $trace_id = 0;

  require_once('MailCleaner/Config.php');
  $mcconfig = MailCleaner_Config::getInstance();

  if (!isset($params['regexp'])
  || !$params['datefrom'] || !preg_match('/^\d{8}$/', $params['datefrom'])
  || !$params['dateto'] || !preg_match('/^\d{8}$/', $params['dateto']) ) {
    return array('trace_id' => $trace_id);
  }
  $cmd = $mcconfig->getOption('SRCDIR')."/bin/search_log.pl ".$params['datefrom']." ".$params['dateto']." '".$params['regexp']."'";
  if (isset($params['filter']) && $params['filter'] != '') {
    $cmd .= " '".$params['filter']."'";
  }

              if (isset($params['hiderejected']) && $params['hiderejected']) {
                  $cmd .= ' -R ';
              }

  if (isset($params['trace_id']) && $params['trace_id']) {
    $trace_id = $params['trace_id'];
  } else {
    $trace_id = md5(uniqid(mt_rand(), true));
  }
              $cmd .= " -B ".$trace_id;

  $cmd .= "> ".$mcconfig->getOption('VARDIR')."/run/mailcleaner/log_search/".$trace_id." &";
  $res = `$cmd`;
  return array('trace_id' => $trace_id, 'cmd' => $cmd) ;
}

The code also tells us that the $regexp isn't the only parameter that can be used to inject malicious inputs. The $filter parameter (which is the normalized version of the $sender parameter) can also trigger the same problem, but it's patched.

Now that we are at the code that ends up executing the malicious input, this concludes our root cause analysis for MailClient command injection vulnerability.

Metasploit Module

In order to turn this proof of concept into a Metasploit module, the HttpClient mixin should suffice. The Metasploit module can be found here.

Summary

MailCleaner is vulnerable to a command injection vulnerability in the Logs_StartTrace of the SOAP API, however, password is required in order to acheive this.

The thing about passwords is that it could be an underestimated exploitable condition. To some people, we tend to have a soft spot for pre-auth shells, so a post-auth one doesn't always sound all that exciting. However, when a web application starts relying on system functions such as executing system commands and read/write files, which a lot of them do, weak passwords become the worst vulnerabilities you can find on a network, and it doesn't take a lot of skills to crack passwords. Having a strong password policy and enforcement would really prevent a lot of post-auth 0days from succeeding, and the Logs_StartTrace vulnerability would be a good example of that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment