序列化和反序列化介绍
serialize()将一个对象转换成一个字符串,unserialize()将字符串还原为一个对象,在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。简单点讲序列化就是把一个对象变为可以传输的字符串,而反序列化就是把字符换换原为对象。
简单例子
<?php class test{ public $suifeng="shuai"; } $a=new test(); //实例化一个对象 $b=serialize($a); //进行序列化 echo $b; //输出序列化后的字符串 echo '<br>'; echo "我是分割线"; $c=unserialize($b); //把序列化后的字符串反序列化 echo '<br>'; echo $c->suifeng; ?>
这里我们再来看看反序列化后输出字符的含义
首先输出的内容为
O:4:"test":1:{s:7:"suifeng";s:5:"shuai";}
O->object 4->object的长度 test->object的名称 1->object中变量个数 s->变量名数据类型 7->变量名长度 suifeng->变量名 S->变量值数据类型 5->变量值长度 shuai->变量的值
PHP其他数据类型
a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
PHP常见魔法函数
__construct() //一个对象创建时被调用
__destruct() //一个对象销毁前被调用
__call() //调用类不存在的方法时执行
__callStatic() //调用类不存在的静态方式方法时执行。
__wakeup() //将在反序列化之后立即被调用
__sleep() //在对象被序列化前被调用
__toString() //当一个对象被当做字符串使用时被调用
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问属性
__invoke() //调用函数的方式调用一个对象时的回应方法
__isset() //在不可访的属性上调用isset()或empty()触发
__unset() //在不可访的属性上使用unset()时触发
public、protected、private下序列化对象区别
php v7.x反序列化的时候对访问类别不敏感
public变量
直接变量名反序列化出来
protected变量
\x00 + * + \x00 + 变量名
可以用S:5:"\00*\00op"来代替s:5:"?*?op"
private变量
\x00 + 类名 + \x00 + 变量名
反序列化漏洞形成条件
1、unserialize函数的参数可控
2、后台使用了相应的PHP中的魔法函数
反序列化漏洞原理
我们先运行如下一串代码
<?php class ABC{ function __construct(){ echo '调用了构造函数<br>'; } function __destruct(){ echo '调用了析构函数<br>'; } function __wakeup(){ echo '调用了苏醒函数<br>'; } } echo '创建对象a<br>'; $a=new ABC; echo '序列化<br>'; $a_ser=serialize($a); echo '反序列化<br>'; $a_unser=unserialize($a_ser); echo '对象快死了!'; ?>
PHP语言本身漏洞
还有一种PHP语言本身漏洞碰到某种特点情况导致的反序列化漏洞
如:__wakeup失效引发(CVE-2016-7124)
php版本< 5.6.25 | < 7.0.10
当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行
PHP_session序列化问题
当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。
PHP中有三种序列化处理器,如下:
处理器 |
对应的存储格式 |
php |
键名 + 竖线 + 经过serialize()函数反序列化处理的值 |
php_binary |
键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数反序列化处理的值 |
php_serialize(php>=5.5.4) |
经过serialize()函数反序列处理的数组 |
配置文件php.ini中含有这几个与session存储配置相关的配置项:
session.save_path="" --设置session的存储路径,默认在/tmp
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字。默认使用php
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式),比如files就是session默认以文件的方式进行存储
且在PHP中默认使用的是PHP引擎,如果想要修改成其他引擎,我们需要添加代码ini_set('session.serialize_handler', '需要设置的引擎'),例:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容,例如session文件名称为:sess_1ja9n59ssk975tff3r0b2sojd5
如果PHP在反序列化存储的$_SEESION数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的伪造,甚至可以伪造任意数据。
PHP反序列化可以利用的原生类
__call
SoapClient
这个也算是目前被挖掘出来最好用的一个内置类,php5、7都存在此类。
SSRF
<?php
$a = new SoapClient(null,array('uri'=>'http://example.com:5555', 'location'=>'http://example.com:5555/aaa'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
__toString
Error
适用于php7版本
XSS
开启报错的情况下:
<?php
$a = new Error("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);
//Test
$t = urldecode('O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D');
$c = unserialize($t);
echo $c;
Exception
适用于php5、7版本
XSS
开启报错的情况下:
<?php
$a = new Exception("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);
//Test
$c = urldecode('O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D');
echo unserialize($c);
phar://协议
概念
一个php应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在window操作系统上面的安装程序、一个jquery库等等,为了做到这点php采用了phar文档文件格式,这个概念源自java的jar,但是在设计时主要针对 PHP 的 Web 环境,与 JAR 归档不同的是Phar归档可由 PHP 本身处理,因此不需要使用额外的工具来创建或使用,使用php脚本就能创建或提取它。phar是一个合成词,由PHP和 Archive构成,可以看出它是php归档文件的意思(简单来说phar就是php压缩文档,不经过解压就能被 php 访问并执行)
phar组成结构
stub:它是phar的文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
manifest:也就是meta-data,压缩文件的属性等信息,以序列化存储
contents:压缩文件的内容
signature:签名,放在文件末尾
这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多
前提条件
php.ini中设置为phar.readonly=Off
php version>=5.3.0
demo测试
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $o = new TestObject(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
可以很明显看到manifest是以序列化形式存储的
如果现在通过phar://包装器对我们现有的Phar文件执行文件操作,则其序列化元数据将被反序列化。这意味着我们在元数据中注入的对象被加载到应用程序的范围中。如果此应用程序具有已命名的类AnyClass并且具有魔术方法__destruct()或已__wakeup()定义,则会自动调用这些方法。这意味着我们可以在代码库中触发任何析构函数或唤醒方法。更糟糕的是,如果这些方法对我们注入的数据进行操作,那么这可能会导致进一步的漏洞。
以下是受影响函数列表
这时利用phar://协议即可
利用条件
phar 文件能够上传
文件操作函数参数可控, : ,/ phar 等特殊字符没有被过滤
有可用的魔术方法作为"跳板"
反序列化字符逃逸
PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
下面我们来分析一段代码
<?php class test{ public $suifeng="shuai"; } $a=new test(); $b=serialize($a); echo $b; $c=unserialize($b); echo '<br>'; echo $c->suifeng; ?>
发现序列化为 O:4:"test":1:{s:7:"suifeng";s:5:"shuai";},也正常进行了反序列化。当我们把序列化结果修改为O:4:"test":1:{s:7:"suifeng";s:5:"shuai";}i:1;s:4:"test"; 后发现还是会正常解析。
但是我们修改其长度就会报错,如 O:4:"test":1:{s:7:"suifeng";s:4:"shuai";}
知道这个特性我们再来分析如下一段代码
<?php function filter($string){ return str_replace('test','test1',$string); } $username = "admin"; $password = "1234"; $user = array($username, $password); var_dump(serialize($user)); echo '\n'; $r = filter(serialize($user)); var_dump($r); echo '\n'; var_dump(unserialize($r));
可以看到反序列化为了a:2:{i:0;s:5:"admin";i:1;s:4:"1234";},当我们把username参数修改为admintest时,后续代码流程先反序列化$user,然后再执行Filter函数里面的str_place函数,把test替换成了test1,这样就导致了长度不一致,最终导致反序列化失败。
a:2:{i:0;s:9:"admintest";i:1;s:4:"1234";}
a:2:{i:0;s:9:"admintest1";i:1;s:4:"1234";}
假设这个代码流程是一个创建账号的代码流程,此时$username可由用户可控制,这时我们就可以通过控制可控参数导致反序列化字符逃逸。其本质其实也是和sql注入一样,对双引号,大括号的闭合,只不过反序列化字符逃逸需要满足一些其特点的条件。接下来我们就对其进行构造payload。
因为其严格按照以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),所以我们可以这么对其进行闭合。
可以看到我们构造$username=admintest";i:1;s:6:"123456";},经过序列化后序列化为了a:2:{i:0;s:29:"admintest";i:1;s:6:"123456";}";i:1;s:4:"1234";},这样的序列化我们再对其进行反序列化红色部分就不会进入到反序列化中。但是可以看到替换后还是没有反序列化成功,我们来分析一下。
替换后我们得到
a:2:{i:0;s:29:"admintest1";i:1;s:6:"123456";}";i:1;s:4:"1234";},红色部分在进行反序列化的时候会被进行忽略,那进行反序列化的字段就为
a:2:{i:0;s:29:"admintest1";i:1;s:6:"123456";}
可以看到这里的s:29:"admintest1"明显不对,所以我们的反序列化会失败,那么怎么去让这里保持正确呢,这也就是我们反序列化字符字符逃逸特点条件需要考虑的东西。
在str_replace('test','test1',$string)代码里,test替换为了test1,test1相比之前test多了一个字符,所以只要我们再加上只够的test让其替换成test1,且让长度相等,这样就可以让我们的反序列化正常进行了。
我们构造payload
admintesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest";i:1;s:6:"123456";}
这样我们就对改业务的密码进行了修改