PHP反序列化

PHP反序列化

序列化与反序列化

序列化:对象转换为数组或字符串等格式

反序列化:将数组或字符串等格式转换成对象

  • serialize () // 将对象转换成一个字符串
  • unserialize () // 将字符串还原成一个对象

5e66456bcfd56815ccfddb11e7fc49f6

PHP对象转换为字符串格式

2bc61502185b655b040ea12778d66964

魔术方法

3184e231d615bdc32d49a1b1cc90cd68

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
<?php
header("Content-type: text/html; charset=utf-8");


////第一组
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
public $name;
public $age;
public $string;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct($name, $age, $string){
echo "__construct 初始化"."<br>";
}
// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
/*
* 当对象销毁时会调用此方法
* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
*/
function __destruct(){
echo "__destruct 类执行完毕"."<br>";
}
}

// 主动销毁
$test = new Test("xiaodi",31, 'Test String');
unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';

//程序结束自动销毁
$test = new test("xiaodi",31, 'Test String');
echo '第二种执行完毕'.'<br>';



////第二组
////__sleep():serialize之前被调用,可以指定要序列化的对象属性。
//class Test1{
// public $name;
// public $age;
// public $string;
//
// // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
// public function __construct($name, $age, $string){
// echo "__construct 初始化"."<br>";
// $this->name = $name;
// $this->age = $age;
// $this->string = $string;
// }
//
// // __sleep() :serialize之前被调用,可以指定要序列化的对象属性
// public function __sleep(){
// echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
// // 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
// return array('name', 'age','string');
// }
//}
//
//$a = new Test1("xiaodi",31, 'good teacher');
//echo serialize($a)."<br>";
//
//
//////__wakeup:反序列化恢复对象之前调用该方法
//class Test2{
// public $sex;
// public $name;
// public $age;
//
// public function __construct($name, $age, $sex){
// echo "__construct被调用!<br>";
// }
//
// public function __wakeup(){
// echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
// }
//}
//
//$person = new Test2('xiaodi',31,'男');
//$a = serialize($person);
//unserialize($a);



//__INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做。
//class Test{
// // _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
// public function __invoke($param1, $param2, $param3)
//{
// echo "这是一个对象<br>";
// var_dump($param1,$param2,$param3);
// }
//}
//
//$a = new Test();
////将对象当做函数调用 触发__invoke魔术方法
//$a('xiaodi',31,'男');


//__toString():如果一个对象类中存在__toString魔术方法,这个对象类被当做字符串进行处理时,就会触发__toString魔术方法
//class Test
//{
// public $variable = 'good is string';
//
// public function good(){
// echo $this->variable . '<br />';
// }
//
// // 在对象当做字符串的时候会被调用
// public function __toString(){
// return '__toString魔术方法被执行!';
// }
//}
//
//$a = new Test();
////$a->good();
////输出调用
//echo $a;


//__CALL 魔术方法 调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
//class Test{
//
// public function good($number,$string){
// echo '存在good方法'.'<br>';
// echo $number.'---------'.$string.'<br>';
// }
//
// // 当调用类中不存在的方法时,就会调用__call();
// public function __call($method,$args){
// echo '不存在'.$method.'方法'.'<br>';
// var_dump($args);
// }
//}
//
//$a = new Test();
//$a->good(1,'xiaodisec');
//// 不存在xiaodi方法 触发__call魔术方法
//$b = new Test();
//$b->xiaodi(899,'no');



//__get() 魔术方法 读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数
//class Test {
// public $n=1233234;
//
// // __get():访问不存在的成员变量时调用
// public function __get($name){
// echo '__get 不存在成员变量'.$name.'<br>';
// }
//}
//
//$a = new Test();
//// 存在成员变量n,所以不调用__get
//echo $a->n;
//echo '<br>';
//// 不存在成员变量xiaodi,所以调用__get
//echo $a->xiaodi;


//__set()魔术方法 设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。
//class Test{
// public $noway=0;
//
// // __set():设置对象不存在的属性或无法访问(私有)的属性时调用
// /* __set($name, $value)
// * 用来为私有成员属性设置的值
// * 第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
// * */
//
// public function __set($name,$value){
// echo '__set 不存在成员变量 '.$name.'<br>';
// echo '即将设置的值 '.$value."<br>";
// $this->noway=$value;
// }
//
// public function Get(){
// echo $this->noway;
// }
//}
//
//$a = new Test();
//// 访问noway属性时调用,并设置值为899
//$a->noway = 899;
//// 经过__set方法的设置noway的值为899
//$a->Get();
//echo '<br>';
//// 设置对象不存在的属性xiaodi
//$a->xiaodi = 31;
//// 经过__set方法的设置noway的值为31
//$a->Get();



//__isset(): 检测对象的某个属性是否存在时执行此函数。当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用
//class Person{
// public $sex; //公共的
// private $name; //私有的
// private $age; //私有的
//
// public function __construct($name, $age, $sex){
// $this->name = $name;
// $this->age = $age;
// $this->sex = $sex;
// }
//
// // __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
// public function __isset($content){
// echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>";
//// echo "123<br>";
// return isset($this->$content);
// }
//}
//
//$person = new Person("xiaodi", 31,'男');
//// public 成员
//echo ($person->sex),"<br>";
//echo isset($person->name),"<br>";
//echo empty($person->sex),"<br>";
//// private 成员
//isset($person->name);
//empty($person->age);


//__unset():在不可访问的属性上使用unset()时触发 销毁对象的某个属性时执行此函数
//class Person{
// public $sex;
// private $name;
// private $age;
//
// public function __construct($name, $age, $sex){
// $this->name = $name;
// $this->age = $age;
// $this->sex = $sex;
// }
//
// // __unset():销毁对象的某个属性时执行此函数
// public function __unset($content) {
// echo "当在类外部使用unset()函数来删除私有成员 {$content} 时自动调用的<br>";
// //echo isset($this->$content)."<br>";
// }
//}
//
//$person = new Person("xiaodi", 31,"男"); // 初始赋值
//unset($person->sex);//不调用 属性共有
//unset($person->name);//调用 属性私有 触发__unset
//unset($person->age);//调用 属性私有 触发__unset



?>

第一组

  • __construct (): // 当对象 new 的时候会自动调用,类似构造函数
  • __destruct ():// 当对象被销毁时会被自动调用,包含主动销毁 (即手动销毁对象) 和被动销毁 (即程序运行结束),类似析构函数

第二组

  • __sleep (): //serialize () 执行时被自动调用
  • __wakeup (): //unserialize () 时会被自动调用

其他

  • __invoke (): // 当尝试以调用函数的方法调用一个对象时会被自动调用
  • __toString (): // 把类当作字符串使用时触发
  • call (): // 调用某个方法,若方法存在,则调用;若不存在,则会去调用call 函数。
  • get (): // 读取对象属性时,若存在,则返回属性值;若不存在,则会调用get 函数
  • set (): // 设置对象的属性时,若属性存在,则赋值;若不存在,则调用set 函数。
  • __isset (): // 在不可访问的属性上调用 isset () 或 empty () 触发
  • __unset (): // 在不可访问的属性上使用 unset () 时触发

代码中未测试的函数

  • __callStatic (): // 在静态上下文中调用不可访问的方法时触发
  • __set_state (),调用 var_export () 导出类时,此静态方法会被调用
  • __clone (),当对象复制完成时调用
  • __autoload (),尝试加载未定义的类
  • __debugInfo (),打印所需调试信息

反序列化漏洞产生

原理

  • 未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL 注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。

实例

1
2
3
4
5
6
7
8
9
<?php
class B{
public $cmd='ipconfig';
public function __destruct(){
system($this->cmd);
}
}
//函数引用,无对象创建触发魔术方法
unserialize($_GET['x']);
利用
1
2
3
4
5
6
7
8
9
//POP链构造
//<?php
class B
{
public $cmd = 'ver';
}

$x = new B();
echo serialize($x);

POP 链构造

什么是POP

  • POP:面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,序列化攻击都在 PHP 魔术方法中出现可利用的漏洞,因自动调用触发漏洞,但如关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造 POP 链寻找相同的函数名将类的属性和敏感函数的属性联系起来。

反序列化常见起点

方法名 调用条件
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用

反序列化常见跳板

方法名 调用条件
__toString 当一个对象被当作字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()是被调用

反序列化常见终点

方法名 调用条件
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里

示例详见CTFshow

web254

web255

web256

web257

属性类型

对象变量属性

public(公共的):在本类内部、外部类、子类都可以访问
protect(受保护的):只有本类或子类或父类中可以访问
private(私人的):只有本类内部可以使用

序列化数据显示

public属性序列化的时候格式是正常成员名
private属性序列化的时候格式是%00类名%00成员名
protect属性序列化的时候格式是%00*%00成员名

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
header("Content-type: text/html; charset=utf-8");
//public private protected说明
class test{
public $name="xiaodi";
private $age="31";
protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

protect 修饰的属性

父类自己访问受保护属性:类内设置接口,类外创建对象访问接口从而访问属性

1
2
3
4
5
6
7
8
9
10
11
12
13

class ParentClass {
protected $protectedProperty = "Protected Property";


public function getProtectedProperty() {
return $this->protectedProperty;
}

}

$parentObj = new ParentClass();
echo $parentObj->getProtectedProperty(); // 输出: Protected Property

子类访问父类的受保护属性:子类内设置接口,类外子类创建对象访问接口从而访问属性

1
2
3
4
5
6
7
8
9
10
11
12
class ParentClass {
protected $protectedProperty = "Protected Property";
}

class ChildClass extends ParentClass {
public function getProtectedPropertyFromParent() {
return $this->protectedProperty;
}
}

$childObj = new ChildClass();
echo $childObj->getProtectedPropertyFromParent(); // 输出: Protected Property

类外访问父类的受保护属性

1
2
3
4
5
6
7
class ParentClass {
protected $protectedProperty = "Protected Property";
}

$parentObj = new ParentClass();
// 尝试直接访问父类的受保护属性
echo $parentObj->protectedProperty; // 报错: Fatal error: Uncaught Error: Cannot access protected property

类内访问就是在类内的方法访问,类外访问就是创建对象调用属性的方式

private 修饰的属性
父类访问自己的私有属性:类内设置接口,类外创建对象访问接口从而访问属性

1
2
3
4
5
6
7
8
9
10
11
12
class ParentClass {
private $privateProperty = "Private Property";


public function getPrivateProperty() {
return $this->privateProperty;
}

}

$parentObj = new ParentClass();
echo $parentObj->getPrivateProperty(); // 输出: Private Property

子类无法直接访问父类的私有属性:子类内设置接口,类外子类创建对象也不能访问接口从而访问属性

1
2
3
4
5
6
7
8
9
10
11
12
13
class ParentClass {
private $privateProperty = "Private Property";
}

class ChildClass extends ParentClass {
public function getPrivatePropertyFromParent() {
// 尝试访问父类的私有属性
return $this->privateProperty; // 报错: Fatal error: Uncaught Error: Cannot access private property
}
}

$childObj = new ChildClass();
echo $childObj->getPrivatePropertyFromParent();

类外无法直接访问父类的私有属性

1
2
3
4
5
6
7
class ParentClass {
private $privateProperty = "Private Property";
}

$parentObj = new ParentClass();
// 尝试直接访问父类的私有属性
echo $parentObj->privateProperty; // 报错: Fatal error: Uncaught Error: Cannot access private property

protect 修饰的方法

父类自己访问受保护方法:类内设置接口,类外创建对象访问接口从而访问方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ParentClass {
protected function protectedMethod() {
return "Protected Method";
}


public function callProtectedMethod() {
return $this->protectedMethod();
}

}

$parentObj = new ParentClass();
echo $parentObj->callProtectedMethod(); // 输出: Protected Method

子类访问父类的受保护方法:子类内设置接口,类外子类创建对象访问接口从而访问方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ParentClass {
protected function protectedMethod() {
return "Protected Method";
}
}

class ChildClass extends ParentClass {
public function callProtectedMethodFromParent() {
return $this->protectedMethod();
}
}

$childObj = new ChildClass();
echo $childObj->callProtectedMethodFromParent(); // 输出: Protected Method

类外无法直接访问父类的受保护方法:

1
2
3
4
5
6
7
8
9
class ParentClass {
protected function protectedMethod() {
return "Protected Method";
}
}

$parentObj = new ParentClass();
// 尝试直接调用父类的受保护方法
echo $parentObj->protectedMethod(); // 报错: Fatal error: Uncaught Error: Call to protected method

private 修饰的方法

父类访问自己的私有方法:类内设置接口,类外创建对象访问接口调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ParentClass {
private function privateMethod() {
return "Private Method";
}


public function callPrivateMethod() {
return $this->privateMethod();
}

}

$parentObj = new ParentClass();
echo $parentObj->callPrivateMethod(); // 输出: Private Method

子类无法直接访问父类的私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ParentClass {
private function privateMethod() {
return "Private Method";
}
}

class ChildClass extends ParentClass {
public function callPrivateMethodFromParent() {
// 尝试访问父类的私有方法
return $this->privateMethod(); // 报错: Fatal error: Uncaught Error: Call to private method
}
}

$childObj = new ChildClass();
echo $childObj->callPrivateMethodFromParent();

类外无法直接访问父类的私有方法

1
2
3
4
5
6
7
8
9
class ParentClass {
private function privateMethod() {
return "Private Method";
}
}

$parentObj = new ParentClass();
// 尝试直接调用父类的私有方法
echo $parentObj->privateMethod(); // 报错: Fatal error: Uncaught Error: Call to private method

总结

概念约定:

  • 什么是类内访问:通过类内的方法访问属性/方法。
  • 什么是类外访问:类外创建对象,直接访问属性/方法

protect 访问权限:只有子类,父类自己可以类内访问到,类外不行

  • protect 修饰的属性:类内设置访问属性的接口(public),类外的子类,本类创建对象调用接口从而访问属性
  • protect 修饰的方法:类内设置访问属性的接口(public),类外的子类,本类创建对象调用接口从而调用方法

private 访问权限:只有父类自己可以类内访问,类外不行

  • private 修饰的属性:类内设置接口,只有本类自己可以通过创建对象访问类内接口从而访问私有属性
  • private 修饰的方法:类内设置接口,只有本类自己可以通过创建对象访问类内接口从而访问类内私有方法

示例

CVE-2016-7124(wakeup:unserialize()时会被自动调用)
漏洞编号:CVE-2016-7124
影响版本:PHP 5<5.6.25; PHP 7<7.0.10
漏洞危害:如存在wakeup方法,调用unserilize()方法前则先调用wakeup方法,但序列化字符串中表示对象属性个数的值大于真实属性个数时会跳过wakeup执行

极客大挑战 2019

1、下载源码分析,触发flag条件

2、分析会触发调用__wakeup 强制username值

3、利用语言漏洞绕过 CVE-2016-7124

4、构造payload后 修改满足漏洞条件触发

字符串逃逸

两种场景

  1. 字符增多型
    通过构造特殊字符串使序列化结构逃逸

    1
    2
    // 原代码过滤机制:将'x'替换为'xx'
    // 构造payload使s:5:"value"被篡改
  2. 字符减少型
    利用过滤机制缩短字符串长度

利用公式

1
2
原结构:...s:10:"payload";...
过滤后:...s:8:"payload";...(通过计算长度差注入恶意代码)

详见:

PHP反序列化——字符逃逸漏洞(肯定能看懂的!)_php {i:1;s:65:’’}-CSDN博客

反序列化链项目

NotSoSecure(综合类)

项目地址:https://github.com/NotSoSecure/SerializedPayloadGenerator

为了利用反序列化漏洞,需要设置不同的工具,如 YSoSerial(Java)、YSoSerial.NET、PHPGGC 和它的先决条件。DeserializationHelper 是包含对 YSoSerial(Java)、YSoSerial.Net、PHPGGC 和其他工具的支持的Web界面。使用Web界面,您可以为各种框架生成反序列化payload.

Java – YSoSerial
NET – YSoSerial.NET
PHP – PHPGGC
Python - 原生

PHPGGC(单项类)

项目地址:https://github.com/ambionics/phpggc

PHPGGC是一个包含unserialize()有效载荷的库以及一个从命令行或以编程方式生成它们的工具。当在您没有代码的网站上遇到反序列化时,或者只是在尝试构建漏洞时,此工具允许您生成有效负载,而无需执行查找小工具并将它们组合的繁琐步骤。 它可以看作是frohoff的ysoserial的等价物,但是对于PHP。目前该工具支持的小工具链包括:CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、ThinkPHP、Slim、SwiftMailer、Symfony、Wordpress、Yii和ZendFramework等。

使用方法:

从0到1掌握反序列化工具之PHPGGC_phpggc安装方法-CSDN博客

示例

[安洵杯 2019]iamthinking Thinkphp V6.0.X 反序列化

CTFSHOW 反序列化 267 Yii2反序列化

CTFSHOW 反序列化 271 Laravel反序列化