什么是序列化,反序列化
序列化是对象转换为数组或字符串等格式,反序列化是将数组或字符串等格式转换成对象
php序列化
serialize()
序列化函数, unserialize()
反序列化函数
// 序列化数组
$data = ["name" => "John", "age" => 25];
$serialized = serialize($data);
echo $serialized;
// 输出:a:2:{s:4:"name";s:4:"John";s:3:"age";i:25;}
a:2
:表示数组(Array),包含 2 个元素。s:4:"name"
:表示字符串(String),长度 4,值为 "name"。i:25
:表示整数(Integer),值为 25。
//反序列化
$restoredData = unserialize($serialized);
print_r($restoredData);
// 输出:Array ( [name] => John [age] => 25 )
//序列化对象
class User {
public $name;
private $id;
public function __construct($name, $id) {
$this->name = $name;
$this->id = $id;
}
}
$user = new User("Alice", 1001);
$serializedUser = serialize($user);
echo $serializedUser;
// 输出:O:4:"User":2:{s:4:"name";s:5:"Alice";s:7:"Userid";i:1001;}
o表示object,4表示类名长度,user是类名,2是属性数量,s:4表示字符串长度为4,即变量名的长度,s:5表示name的值Alice长度为5
序列化私有属性private时会在属性名前后加空字符null,url编码后是%00,所以属性名的长度写的9
序列化受保护属性protected时属性名变为*,且前后加%00,即%00*%00
产生漏洞的原因
当数据被反序列化时,程序不仅要还原数据结构,构造函数、析构函数、魔术方法还可能自动执行某些初始化逻辑,以P HP为例:反序列化对象时,会自动调用 __wakeup()
、__destruct()
等魔术方法。若这些方法中存在危险操作(如文件操作、命令执行),攻击者可通过构造恶意对象触发漏洞。
前置知识php类
语法格式
class 类名 {
// 属性(变量)
[访问修饰符] $属性名 [= 初始值];
// 方法(函数)
[访问修饰符] function 方法名(参数) {
// 方法体
}
}
定义一个用户类
class User {
// 属性
public $username;
private $password;
protected $email;
// 构造函数
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
// 方法
public function showInfo() {
return "用户名:" . $this->username;
}
}
核心组成部分
1,访问修饰符
控制属性和方法的可见性:
public
:公开(默认),任何位置可访问。private
:私有,仅本类内部可访问。protected
:受保护,本类及子类可访问。
2. 属性(Properties)
- 类的变量,描述对象特征。
- 可在类内直接定义并初始化:
class Car {
public $brand = "Toyota";
private $price = 200000;
}
3. 方法(Methods)
- 类中的函数,定义对象行为。
class Calculator {
public function add($a, $b) {
return $a + $b;
}
}
4,魔术方法
- 以
__
开头,控制对象的生命周期、属性访问、方法调用等
__construct(): //构造函数,当对象 new 的时候会自动调用
__destruct()://析构函数当对象被销毁时会被自动调用
__wakeup(): //unserialize()时会被自动调用
__invoke(): //当尝试以调用函数的方法调用一个对象时,会被自动调用
__call(): //在对象上下文中调用不可访问的方法时触发
__callStatci(): //在静态上下文中调用不可访问的方法时触发
__get(): //用于从不可访问的属性读取数据
__set(): //用于将数据写入不可访问的属性
__isset(): //在不可访问的属性上调用 isset()或 empty()触发
__unset(): //在不可访问的属性上使用 unset()时触发
__toString(): //把类当作字符串使用时触发
__sleep(): //serialize()函数会检查类中是否存在一个魔术方法__sleep() 如果存在,该方法会被优先调用
创建对象与使用
实例化对象
$user = new User("Alice", "secret123");
访问属性与方法
echo $user->username; // 输出:Alice
echo $user->showInfo(); // 输出:用户名:Alice
// echo $user->password; // 报错:私有属性不可访问
$this
和 ->
->
->
是一个运算符,用于访问对象的属性和方法。它用于对象实例和其属性或方法之间
<?php
class MyClass {
public $property = "Hello, World!";
public function displayProperty() {
echo $this->property;
}
}
$obj = new MyClass();
echo $obj->property; // 输出: Hello, World!
$obj->displayProperty(); // 输出: Hello, World!
?>
$obj->property
用于访问对象 $obj
的属性 $property
,而 $obj->displayProperty()
用于调用对象 $obj
的方法 displayProperty()
。
$this
$this
是一个特殊的变量,它在一个类的方法中被用来引用当前对象本身。通过 $this
,可以访问当前对象的属性和方法。
<?php
class MyClass {
public $property = "Hello, World!";
public function displayProperty() {
echo $this->property;
}
}
$obj = new MyClass();
$obj->displayProperty(); // 输出: Hello, World!
?>
在这个示例中,$this->property
用于访问当前对象的属性 $property
。
反序列化应用简单例子
序列化$a,$a是A的对象
再把序列化的结果传参,反序列化后传给$t
可以看到显示出了end
因为没有调用方法,且没有通过new创建对象,所以只有__destruct()执行了
重要魔术方法
__construct():
构造函数,当对象 new 的时候会自动调用
__destruct():
析构函数当对象被销毁时会被自动调用,对象被销毁有两种,一是用unset()主动销毁,二是程序结束时自动销毁
__sleep()
序列化serialize()函数会检查类中是否存在一个魔术方法sleep(),如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组量名称的数组。如果该方法未返回任何内容,则null被序列化,并产生一个e_notice级别的错误。
触发时机:序列化serialize0)之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
参数:成员属性
返回值:需要被序列化存储的成员属性
const表示常量,一旦被赋值就不能在改变,__sleep()在序列化之前被触发,表示只序列化username,nickname两个属性
__wakeup()
unserialize()会检查是否存在一 个wakeup()方法。如果存在,则会先调用 wakeup()方法,预先准备对象需要的资源
var_dump()判断一个变量的类型与长度,并输出变量的数值,__wakeup()让password属性也反序列化,并且值等于username的值
绕过方法:当反序列化字符串中的对象属性个数大于实际属性个数时,就不会执行wakeup()函数了,(PHP版本为5.6.25或早期版本,或者PHP7版本小于7.0.10)
__toString()
把对象当做字符串调用时就会触发,调用对象$test可以用print_r()或者var_dump(),若使用echo或者print用字符串的方式去调用对象,就会触发__toString函数
__invoke()
格式表达错误触发
__call
触发时机:调用一个不存在的方法
callxxx方法不存在,所以触发了__call(),并且返回$arg1=方法名callxxx和$arg2=a
__get
触发时机:调用的属性不存在时
调用的var2属性不存在,所以触发了__get(),返回$arg1=属性名var2
__set
触发条件:给不存在的属性赋值
赋值的var2属性不存在,所以触发了__set函数,返回了$arg1=属性名var2和$arg2=赋的值1
__isset
触发条件:对不可访问属性(不存在的或非公开的)使用isset或empty时__isset会被调用
isset里调用了私有属性var,所以__isset被调用,返回了$arg1=属性名var
__unset
触发条件:对不可访问属性(不存在的或非公开的)使用unset时
unset用于销毁给定的变量
__clone
触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
结果:__clone test
ctfshow
254关
username和password的值都传xxxxxx即可
255关
$user等于cookie传参的反序列化,所以可以把ctfshowuser类的序列化传给$user,再根据代码分析,同时要把$isvip的值改True,get传参username=password=xxxxxx
建立一个对象$a,打印出$a的序列化结果,同时进行url编码,进行url编码的目的是把空字符转成%00,这样才能反序列化,但是这里没有对私有属性private的序列化,所以编不编码都行,但是有私有属性编码产生了空字符就一定要URL编码后再反序列化,而且在 PHP 中,通过超全局变量获取 URL 参数时,会自动对这些参数进行 URL 解码,所以不用担心解码的问题
其实还有更简单的,就写个public $isvip=true就行,因为事实上只用到了它
<?php
class ctfShowUser{
public $isVip=true;
}
$a = new ctfShowUser();
echo urlencode(serialize($a));
?>
把运行出的结果传入cookie,同时username和password改成xxxxxx,就可以获得flag了
256关
相比上一关多了一条判断,就是username!==password,所以我们get传参$username=x,$password=y,同时序列化时$username='x',$password='y',这样就保证了$username!==$password,但username=$u,password=$p
序列化
传参
257关
在反序列化中,我们能控制的数据就是对象中的属性值(成员变量)
所以在php反序列化中有一种漏洞利用方法叫面向属性编程
即pop
pop链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload.
原生类
原生类是指由 PHP 语言核心或扩展直接提供的预定义类,开发者无需手动声明即可直接使用。这些类通常用于实现 PHP 的底层功能(如文件操作、异常处理、网络通信等)
许多原生类默认实现了 __toString() 魔术方法。当对象被当作字符串操作(如 echo、字符串拼接、strval() 等)时,该方法会被自动触发。在渗透测试和反序列化漏洞利用中,这些类的 __toString() 方法可能成为攻击链的关键入口
普通类中的 __toString(),当echo该类的对象时,需要我们主动实现 __toString(),否则直接 echo 对象会报错。而原生类如Exception 类(及其子类如 Error)已默认实现了 __toString() 方法
我们创建一个Exception的对象$a,echo它,就会输出Exception内的语句,可以在这个位置进行渗透
例一:
exception原生类会输出错误信息或直接运行代码
传参,弹窗了
例二:
ctfshow259关
字符串逃逸
序列化形式规范
序列化后的字符串要保证成员属性数量一致,成员属性名称长度一致,内容长度一致,不然反序列化就直接回返回bool(false)
例如:
反序列化后的字符串不符合规范,有一个属性,但是写了两个属性,此时反序列化它只会返回bool(false)
写成两个属性,就好了
字符串的长度也得保持规范
"是字符还是格式符号,是由字符串长度判断的
属性逃逸
这里虽然类A定义时有两个属性v1和v2,序列化时写了v1和v3两个属性,且序列化规范,但是我们反序列化下发现,不仅没出错,还凭空多了个v2属性,其中,v1,v3的值取决于序列化字符串$b,v2的值取决于定义类A时的值
反序列化分隔符
序列化规范的前提下,反序列化以;}结束,后面在写什么字符串不影响正常的反序列化
字符减少
先序列化,有两个属性v1和v2
再用空字符替换system,此时v1的长度依然是27,所以就会侵吞后面的字符
最后反序列化,结果是有了三个属性v1,v2,v3
字符增多
通过v1传参22个ls以及v3属性,22个ls再被替换成22个pwd