本文我会先将一些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 | <?php |
解析如下:
- 类使用 class 关键字后加上类名定义。
- 类名后的一对大括号({})内可以定义变量和方法。
- 类的变量使用 var 来声明, 变量也可以初始化值。
- 函数定义类似 PHP 函数的定义,但函数只能通过该类及其实例化的对象访问。
php序列化与反序列化
什么是序列化
序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命是随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时再用unserialize()反序列化为对象
序列化后的格式:
布尔型
1 | b:value |
整数型
1 | i:value |
字符型
1 | s:length:"value"; |
NULL型
1 | N; |
数组
1 | a:<length>:{key, value pairs}; |
对象
1 | O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}; |
注意:
- 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 | a - array 数组型 |
PHP序列化需注意以下几点:
1、序列化只序列属性,不序列方法
2、因为序列化不序列方法,所以反序列化之后如果想正常使用这个对象的话我们必须要依托这个类要在当前作用域存在的条件
3、我们能控制的只有类的属性,攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击
反序列化
将特定格式的字符串转换成对象
魔术方法(magic函数)
PHP中把以两个下划线__
开头的方法称为魔术方法(Magic methods)
类可能会包含一些特殊的函数:magic函数,这些函数在某些情况下会自动调用。
1 | __construct() //类的构造函数,创建对象时触发 |
1 | 反序列化漏洞的常见起点: |
我们需要重点关注一下5个魔术方法,所以再强调一下:
1 | __construct:构造函数,当一个对象创建时调用 |
从序列化到反序列化这几个函数的执行过程是:
__construct()
->__sleep()
-> __wakeup()
-> __toString()
-> __destruct()
__toString()
这个魔术方法能触发的因素太多,所以有必要列一下:
1 | echo($obj)/print($obj)打印时会触发 |
魔术方法在反序列化攻击中的作用
反序列化的入口在unserialize(),只要参数可控并且这个类在当前作用域存在,就能传入任何已经序列化的对象,而不是局限于出现unserialize()函数的类的对象。
如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击。
但是,利用魔术方法就可以扩大攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的。
反序列化实例
ex1
1 | <?php |
ex2
_sleep
方法在一个对象被序列化时调用,_wakeup
方法在一个对象被反序列化时调用
1 | <?php |
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 | 1. php.ini先设置session.serialize_handler为php_serialize |
删除注释后再次访问得到
1 | array(2) { |
默认php方式和修改为php序列化方式输出的session文件是不一样的
1 | //默认php方式 |
不同处理器存储方式不同
实际利用
1.session.auto_start=On
1 | Q:session.auto_start参数会在脚本执行前会自动注册Session会话,所以在脚本中设置的php.ini中(序列化处理器\session)相关参数是无效的。 |
先将php中session.serialize_handler设置为php
ex4:
1 |
|
流程:
1 | 1、提交链接:foo1.php?a=|O:8:"stdClass":0:{} |
可以看到成功生成临时文件
2.session.auto_start=Off
当两个脚本的序列化处理器不同就会有问题出现
ex5:
foo1.php
1 |
|
foo2.php
1 |
|
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";} |
然后访问foo2.php,就会执行代码,输出hilemon
字符逃逸
使用str_replace进行过滤出现了序列化后的得到的字符数量与真实字符的数量不同的情况。
具有以下特点
- php序列化后的字符串经过了替换或者修改,导致字符串长度发生变化。
- 总是先进行序列化,再进行替换修改操作。
顶” 出来的payload就会被当做当前类的属性被继续执行。
闭合当前的变量,而因为长度错误所以此时php把闭合的双引号当做了字符串,所以下一个字符就成了分号,没能闭合导致抛出了错误。就可以控制后面变量的值了。
第一种情况:替换修改后导致序列化字符串变长
1 | <?php |
这段代码生成的序列化内容为
O:8:”atlantic”:3:{s:8:”username”;s:5:”admin”;s:8:”password”;i:123456;s:4:”code”;i:0;}
然后添加一个过滤的函数,得到新的代码
1 | <?php |
进行一个拼接得到
将
1 | ;s:8:"password";s:6:"123456";s:4:"code";i:0;} |
拼接到第17行的admin后面
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是序列化后的字符数量与替换后的相同
逃逸是对序列化后的字符串进行一个替换
第二种情况——替换之后导致序列化字符串变短
一般是传入多个变量的值的时候可以利用
例如
1 | <?php |
接下来利用漏洞,通过输入name和sign来间接修改number的值:
1 | ?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:2:"nb";} |
通过修改name传入的值使得name的长度刚好为反序列化替代后的字符串长度完成闭合
phar反序列化
phar反序列化过程中,对metadata进行解析的时候会进行php_var_unserialize()
将Phar中的metadata进行反序列化
Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data
。以扩展反序列化漏洞的攻击面,配合phar://
协议使用。
Phar文件结构
a stub
是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
。manifest
是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。contents
是被压缩的内容。signature
签名,放在文件末尾。
就是这个文件由四部分组成,每种文件都是有它独特的一种文件格式的,有首有尾。而__HALT_COMPILER();
就是相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用。
生成phar文件
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件
1 | <?php |
访问之后会在同目录生成 phar.phar 文件,放到IDA查看文件结构
该方法在文件系统函数(file_exists()
、is_dir()
等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()
直接进行反序列化操作。
https://paper.seebug.org/680/得知:有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过`phar://`伪协议解析phar文件时,都会将`meta-data`进行反序列化,测试后受影响的函数如下:(大佬的图)
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 | - 命令执行:exec()、passthru()、popen()、system() |
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 | <?php |
POST参数 O:4:"User":1:{s:4:"name";s:3:"Bob";}
:
1 | exit |
如果在某些情况下不想让 __wakeup() 执行,可以将 “User” 后的 1 改为一个比 1 大的数字。
POST参数 O:4:"User":2:{s:4:"name";s:3:"Bob";}
:
1 | nameis Bob |
fast destruct
__PHP_Incomplete_Class 不完整的类
在PHP中,当我们在反序列化一个不存在的类时,会发生什么呢
1 | <?php |
可以发现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 | #修改序列化数字元素个数 |
本质上,fast destruct 是因为unserialize过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的__destruct()
,提前触发反序列化链条。
触发__tostring()魔术方法
使用PHP引用为die()中的变量赋值,使得反序列化对象参与格式化字符串,进而触发__tostring()魔术方法