PHP反序列化入门

基础

概念

1
2
3
序列化:将变量转换为可保存或传输的字符串的过程;
反序列化:在适当的时候把这个字符串再转化成原来的变量使用。
优点:这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。

举个栗子:将木桌拆分成木块,是为序列化;将木块重组成木桌,是为反序列化。

PHP序列化的字母标识:

a - array

b - boolean

d - double

i - integer

o - common object

r - reference

s - string

C - custom object

O - class

N - null

R - pointer reference

U - unicode string

N - NULL

解释:

  • a - 数组(array): 通常指存储相同类型元素的数据结构,这些元素在内存中是连续存储的。数组具有索引,可以有效地访问元素。
  • b - 布尔(boolean): 布尔是一种数据类型,只能取两个值,通常为true(真)或false(假)。它通常用于条件表达式。
  • d - 双精度浮点数(double): Double是一种用于表示带有双精度的浮点数(带有小数点的数)的数据类型。
  • i - 整数(integer): 整数是一种用于表示没有小数点的整数的数据类型。它可以是正数或负数。
  • o - 通用对象(common object): 这通常指编程上的一般对象。术语“通用对象”可能会根据特定的上下文或编程语言而有所不同。
  • r - 引用(reference): 引用是一个值,它引用另一个对象的内存位置。通常用于避免复制大量数据。
  • s - 字符串(string): 字符串是字符的序列。在编程中,它是用于表示文本的数据类型。
  • C - 自定义对象(custom object): 这指的是自定义类的对象,自定义类是面向对象编程中用户定义的数据类型。
  • O - 类(class): 在面向对象编程中,类是创建对象的蓝图。对象是类的实例,可以具有属性和方法。
  • N - 空(null): Null是一个特殊的值,表示没有值或不指向任何对象的引用。
  • R - 指针引用(pointer reference): 这可能是指使用指针实现的引用。指针存储内存地址,通常用于更直接的内存操作。
  • U - Unicode字符串(unicode string): Unicode字符串是使用Unicode标准编码的字符序列,可以表示各种书写系统中的广泛字符。
  • N - 空(NULL): 类似于前面的“null”,这可能指的是空值或没有值。确切的解释可能取决于特定的编程语言或上下文。

private和 protected详解

1
PHP 序列化的时候 private和 protected 变量会引入不可见字符%00,%00类名%00属性名 为private,%00*%00属性名 为protected,注意这两个 %00就是 ascii 码为0 的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得清楚.我们可以将序列化的字符用urlencode编码之后,打印出来查看.

PHP常见魔术方法

__construct 当一个对象创建时被调用,

__destruct 当一个对象销毁时被调用,

__toString 当一个对象被当作一个字符串被调用。

__wakeup() 使用unserialize时触发

__sleep() 使用serialize时触发

__call() 对不存在的方法或者不可访问的方法进行调用就自动调用

__callStatic() 在静态上下文中调用不可访问的方法时触发

__get() 用于从不可访问的属性读取数据

__set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

__isset() 在不可访问的属性上调用isset()或empty()触发

__unset() 在不可访问的属性上使用unset()时触发

__toString() 把类当作字符串使用时触发,返回值需要为字符串

__invoke() 当脚本尝试将对象调用为函数时触发,该对象必须是直接拥有__invoke()魔术方法的对象

详细介绍:PHP反序列化入门手把手详解 - FreeBuf网络安全行业门户

魔术方法执行顺序

推荐文章:[CTF/Web] PHP 反序列化学习笔记 - 掘金 (juejin.cn)

对于魔术方法的调用顺序, 不同的情况下会有不同的顺序

首先, 一个对象在其生命周期中一定会走过 destruct, 只有当对象没有被任何变量指向时才会被回收

当使用 new 关键字来创建一个对象时会调用 construct

绕过

推荐文章:https://pankas.top/2022/08/04/php(phar)%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%8F%8A%E5%90%84%E7%A7%8D%E7%BB%95%E8%BF%87%E5%A7%BF%E5%8A%BF/

绕过__wakeup()(CVE-2016-7124)

条件:php版本 PHP5<5.6.25,PHP7 < 7.0.10

须知:__wakeup()魔术方法在执行unserialize()时,会优先调用这个函数,而不会执行__construct() 函数

绕过方法:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行**

举个栗子: O:4:"test":1:{s:1:"a";s:3:"abc";}

改成:O:4:”test”:10000086:{s:1:”a”;s:3:”abc”;}

反序列化时不会执行__wakeup()

绕过__destruct()

1
__destruct`是PHP对象的一个魔术方法,称为析构函数,顾名思义这是当该对象被销毁的时候自动执行的一个函数。其中以下情况会触发`__destruct
  • 主动调用unset($obj)

  • 主动调用$obj = NULL

  • 程序自动结束

PHP还拥有垃圾回收Garbage collection即我们常说的GC机制

当一个对象没有任何引用的时候,则会被视为“垃圾”

绕过正则

preg_match('/^O:\d+/')匹配序列化字符串是否是对象字符串开头

绕过方法

  • 利用加号绕过(注意在url里传参时+要编码为%2B)
  • 利用数组对象绕过,如 serialize(array($a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)

例子:

1
2
3
4
5
6
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// 将对象放入数组绕过 serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

利用引用绕过

在 PHP 中,引用是一种变量的别名。当你创建一个变量并将其赋值给另一个变量时,这两个变量实际上引用了相同的数据。这意味着对其中一个变量的更改也会影响到另一个变量,因为它们指向相同的内存位置。

$a = 5;

$b = &$a; // $b 现在是 $a 的引用

echo $a; // 输出 5

echo $b; // 输出 5

$a = 10; // 修改 $a

echo $b; // 输出 10,因为 $b 是 $a 的引用

跟C语言的指针不同

16进制绕过字符的过滤

序列字符串中表示字符类型的s大写时,会被当成16进制解析。

没见过,先不记笔记。

POP链构造

推荐文章:https://cjlusec.ldxk.edu.cn/2023/02/15/pop/#POP%E9%93%BE%E4%BB%8B%E7%BB%8D

介绍:是一条思路链。

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

下面着重讲解一题:

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>

思路

一般采用倒推法:

找到flag执行处:get_flag()方法,须使string1类的str1 = GetFlag类的对象;

可是执行$this->str1->get_flag();的条件是__toString()魔术方法,须使string1的对象被读取,利用这个:$this->mod2 = "字符串拼接".$this->mod1,使这里的mode2 = string1类的对象;

可是执行$this->mod2 = "字符串拼接".$this->mod1的条件是 __invoke(),这段代码符合:$s1 = $this->mod1; $s1(); 因为这段代码在funct类中,需要符合__call魔术方法;

可是执行__call魔术方法的条件是:对不存在的方法或者不可访问的方法进行调用就自动调用。使Call类的mod1 = funct类对象。可是test1()方法需要在start_gg类的mod1激活;

令start_gg类的mod1 = Call类的mod1。

方法

找到链尾,一步一步溯源直到链头。中间需要满足各种条件,当然一般会有一些暗示。