PHP反序列化字符串逃逸原理与实战详解

许南星
许南星
发布时间:2026-10-16 | 4人浏览过
原创

PHP反序列化过程中的字符串逃逸是一种利用`str_replace`等过滤函数导致序列化字符串长度发生变化的攻击技术。当被过滤的字符串长度与被替换后的字符串长度不一致时,原有的结构闭合会被打破,攻击者可借此注入伪造的属性值。本文将详细拆解长度增加和长度减少两种逃逸类型的原理及构造方法。

PHP反序列化之字符串逃逸原理

PHP在进行反序列化时,底层机制严格依赖序列化字符串的结构特征:

  • 分隔符:以 ; 作为字段之间的分隔。
  • 结尾标记:以 } 作为对象序列化的结束。
  • 长度校验:序列化工具会根据 s:N:"value" 中的数值 N 来截取内容,若实际内容长度与 N 不匹配,将导致反序列化失败或结构错位。

字符串逃逸的核心在于:通过在输入数据中填充特定字符串,触发服务器端的 str_replace 操作,使得序列化后的字符串总长度或内部结构长度发生改变。这种变化可以“挤掉”原本属于后续字段的字符,或者“溢出”新的字符,从而让攻击者能够覆盖原本不可控的参数(如权限标识 token、管理员状态等)。

第1步:过滤后字符串变多(正向逃逸)

这种情况通常发生在过滤逻辑是将一个短字符串替换为一个较长的字符串,例如将 'fuck' 替换为 'loveU'。每输入4个字符,替换后变成5个字符,总长度增加1位。

操作入口:代码逻辑分析

以 Ctfshow-web262 为例,核心代码逻辑如下:

error_reporting(0);
class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    // 关键点:序列化后进行替换,且替换后变长
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    setcookie('msg',base64_encode($umsg));
    echo 'Your message has been sent';
}

接收端代码验证逻辑为:if($msg->token=='admin') 则输出 flag。默认 token 为 'user'。

执行动作:构造Payload

  1. 确定目标逃逸串:我们需要覆盖 $token 的值。观察 message 类的序列化结构,$token 位于最后。我们需要构造一段能闭合前面字段并开始赋值给 $token 的字符串。";s:5:"token";s:5:"admin";} 即为我们需要注入的目标部分,其长度为 44 个字符。
  2. 计算填充量:由于每个 'fuck' 替换为 'loveU' 会增加 1 位长度(5-4=1),为了产生 44 位的额外长度以容纳上述注入串,我们需要在输入的变量(如 m)中传入 44 个 'fuck'。
  3. 组合参数:
    - f: <任意值>
    - m: <44个fuck>
    - t: ";s:5:"token";s:5:"admin";}

上图为本地测试生成的序列化初始状态,展示了正常输入时的结构。

当传入构造好的参数后,序列化结果发生变化。注意观察结构,原来的 $token 被成功覆盖为 admin,且末尾的闭合符号位置正确。

最终 Cookie 中的 Base64 解码内容即为逃逸成功的序列化字符串,提交后即可获取 Flag。

第2步:字符串变多原理详解(通用模型)

为了更好地理解上述过程,我们可以通过一个简化的例子来看长度变化对反序列化的影响。

现象验证:长度不匹配

假设我们有一个类,属性 name 值为 'hanx'(长度4)。如果在序列化后,将 'x' 替换为 'xx',字符串变为 'hanxx'(长度5),但序列化头部的长度声明仍然是 s:4:。

此时如果直接反序列化,PHP 会严格按照长度 4 截取,导致字符串截断,或者因为后续字符错位导致反序列化报错。

但是,如果我们故意输入足够多的 'x',使得替换后的总长度增加,正好等于我们需要注入的恶意字符串的长度,就可以实现“溢出”。

构造思路

假设我们要将另一个属性 age 的值设为 'woaini'(长度6)。对应的序列化片段为:";i:1;s:6:"woaini";},总长 20 位(含前缀 ";i:1;s:6:" 和闭合 }" 需根据具体类结构调整,此处以原文示例为准,假设需要逃逸出20位有效覆盖)。

如果过滤规则是 x -> xx,每位增加1。我们需要输入 20 个 x。这 20 个 x 会在替换后膨胀成 20 个额外的字符空间,正好用来安放我们的注入串 ";i:1;s:6:"woaini";}。

Payload 构造示例:
name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";} (其中 x 的数量等于注入串长度)

结果显示,age 的值成功被覆盖为 woaini,证明了正向逃逸的可行性。

第3步:过滤后字符串变少(反向逃逸)

当过滤逻辑是将字符串替换为空(如删除 'lemon' 或 'shy')时,序列化字符串长度会变短。这与上述情况相反,但也同样可以利用。

环境复现

假设存在以下逻辑:输入 name 和 sign,反序列化后执行 str_replace(['lemon','shy'], '', $data),并检查 $number 的值。

初始状态下,number 默认为 2020。

正常输入无过滤关键词时,输出一致。

漏洞触发

如果输入包含过滤关键词,字符串长度缩短,会导致反序列化结构错位,出现错误提示。

这表明长度变化确实影响了反序列化结构。

反向逃逸构造

我们的目标是修改 $number 为 2002。
目标注入串:";s:6:"number";s:4:"2002";},长度为 27 位。

我们需要在第一个可控变量(如 name)中填入足够的待删除字符,使其在替换为空后,腾出的空间正好容纳这 27 位注入串。
例如,若删除 'shy',每1个 'shy' 删除3个字符。我们需要填充 ceil(27/3) = 9 个 'shy'。

构造步骤:
1. name 参数:填入用于凑长度的前缀 + 若干待删除字符。原文中使用 YKing 配合 shy 进行占位。
2. sign 参数:直接填入目标注入串 ";s:4:"sign";s:4:"evan";s:6:"number";s:4:"2002";}。

在这个 Payload 中:
- 第一部分负责通过删除机制“挖空”原有 number 字段的值。
- 第二部分 sign 被赋予了一个无害值 evan。
- 紧接着是我们要篡改的 number 字段,值为 2002。
- 最后的 } 闭合整个对象。

由于删除操作导致的长度减少,原序列化中的 2020 被挤出,取而代之的是我们注入的 2002。反序列化后,$number 即为 2002,达成目的。

总结

PHP反序列化字符串逃逸的本质是利用 serialize() 生成字符串与 str_replace() 修改字符串之间的时间差和长度差异。无论是“由少变多”还是“由多变少”,关键在于计算好**插入的填充字符数量**与**目标注入字符串长度**之间的比例关系。掌握这一技巧,能在众多CTF题目和安全审计场景中有效绕过权限校验。

以上就是PHP反序列化字符串逃逸原理与实战的详细内容,更多关于PHP安全反序列化的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表