一、PHP反序列化简介
php 反序列化基本上是围绕着serialize()、unserialize()这两个函数展开的,还有PHAR协议用于解析 phar 文件、phar 文件的 meta-data 字段存在反序列化漏洞,可以使用协议读取文件触发反序列化。
那么什么是序列化呢?序列化就是将一个对象变成可以传输的字符串,而反序列化其实就是将序列化得到的字符串再转变成对象。
首先上例子:
json_encode()
json_decode()
$book =
array
(
'book_1'
=>
'test_1'
,
'book_2'
=>
'test_2'
,
'book_3'
=>
'test_3'
,
'book_4'
=>
'test_4'
);
$json = json_encode($book);
echo
$json;
?>
*左右滑动查看更多
在这里我们有一个 book 数组,如果需要传输这个数组,我们可以利用 json_encode()函数将这个数据序列化成一串字符串,以 key-value 的形式展示出来。
'book_1'
=>
'test_1'
,
'book_2'
=>
'test_2'
,
'book_3'
=>
'test_3'
,
'book_4'
=>
'test_4'
所以我们将数组序列化成 json 格式的字符串的目的就是为了方便传输,我们可以看见,这里用 json 格式来保存数据主要是使用键值对格式来保存的。
json 格式只是为了传输数据而出现的,那么我们讲反序列化漏洞的话,就需要将字符串反序列化成对象。
二、相关概念
在这里笔者写了一个 class,这个 class 中存有一些变量,当这个class被实例化之后,在使用过程中,里面的一些变量发生了改变,如果以后某些时候还会用到这个变量,我们让这个class一直不销毁,就会浪费系统资源。而如果我们将这个对象序列化,将其保存成一个字符串,当你需要使用的时候,再将其反序列化为对象就可以了。
class
DemoClass
{
public
$name =
"test"
;
public
$sex =
"man"
;
public
$age =
"24"
;
}
$example =
new
DemoClass();
$example->name =
"aaron"
;
$example->sex =
"woman"
;
$example->age =
22
;
echo
serialize($example);
在这里,我们首先创建了一个DemoClass,里面存了一些数据,然后我们实例化了一个对象,并将这个对象里的信息改变了,当我们还需要使用这个实例的话,就将序列化(serialize)后的字符串存起来,需要使用的时候再反序列化(unserialize)出来就可以了。
我们可以看一下结果:
这个时候,序列化对象出来的格式和 json 格式不一样。
O:
9
:
"DemoClass"
:
3
:{
s
:
4
:
"name"
;s:
5
:
"aaron"
;s:
3
:
"sex"
;s:
5
:
"woman"
;s:
3
:
"age"
;i:
22
;}
// O 表示 object,这里还有一个情况是A,A表示是Array表示数组
// O:9 这个9 表示对象名表示占9个字符
// O:9:"DemoClass":3 这个3 表示是对象里有三个变量
// {s:4:"name";s:5:"aaron";} s=> 表示String 类型格式,s:4 4=>表示变量名占4位(name),s:5表示name的值(aaron)是String类型格式,且占5位
// i => 表示是int类型格式,后面直接跟数据
// d => 表示double类型格式
*左右滑动查看更多
然后如果反序列化(unserialize)回来:
class
DemoClass
{
public
$name =
"test"
;
public
$sex =
"man"
;
public
$age =
"24"
;
}
$example =
new
DemoClass();
$example->name =
"aaron"
;
$example->sex =
"woman"
;
$example->age =
21
;
$val = serialize($example);
$x = unserialize($val);
echo
$x->name;
三、原理介绍
php 里的魔术方法,通常因为某些条件而触发,不需要手动调用,我理解的是钩子函数,也就是生命周期的概念。
魔术方法:
__construct() //当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__toString() //当一个对象被当作一个字符串使用
__sleep() //在对象在被序列化之前运行
__wakeup //在对象被反序列化时被调用
理解这几个魔术函数,如果 php 接收我们反序列化的字符串,且在魔术方法中能够直接执行我们构造的 payload,就会造成反序列化漏洞。
看一个简单的例子:
class
A
{
var
$test =
"demo"
;
function
__destruct
()
{
echo
$this
->test;
}
}
$a = $_GET[
'test'
];
$a_unser = unserialize($a);
这里表示是我们传入 test 参数,然后在反序列化成对象,然后在其生命周期当这个反序列化生成的对象要被销毁的时候调用 echo 方法,输出 test 参数。
那么我们构造如下 payload
O
:1
:"A"
:1
:{
s
:
4
:
"test"
;
s
:
11
:
"hello,world"
;}
*左右滑动查看更多
test 参数可控的情况下,就会输出 hello,world。
我们再来尝试一下不同的生命周期。
在这里,construct 是处于创建对象的生命周期中,当创建对象的时候会调用该函数,这里要被利用的话,需要配合另一个 Class,这里先用__wakeup在被反序列化时,new 一个新的对象 A,并传入参数,这里表示 test 参数可控的情况下,当 test 参数可控,并在反序列化后,将 test 参数传入 A 的新实例中,那么只要 constructor 中存在可执行代码或者执行命令的函数,那么造成影响。
class
A
{
var
$test =
"demo"
;
function
__construct
($test)
{
echo
"<br/>"
;
echo
$test;
}
}
class
B
{
public
$test_1 =
""
;
function
__wakeup
()
{
$obj =
new
A(
$this
->test_1);
}
}
$a = $_GET[
'test'
];
echo
$a;
$a_unser = unserialize($a);
new
A(
"123"
);
O
:1
:"B"
:1
:{
s
:
6
:
"test_1"
;
s
:
11
:
"hello,world"
;}
*左右滑动查看更多
在这里,destruct 处于对象被销毁的生命周期,当实例化之后,且对该对象的操作完成之后,那么 php 的回收机制则会回收该对象,这里就会调用该钩子函数,这里表示 test 参数可控的情况下,并在反序列化后之后,再打印该值,那么只要 destruct 中存在可执行代码或者执行命令的函数,那么就会造成影响。
class
A
{
var
$test =
"demo"
;
function
__destruct
()
{
echo
"<br/>"
;
echo
$this
->test;
}
}
$a = $_GET[
'test'
];
echo
$a;
$a_unser = unserialize($a);
O
:1
:"A"
:1
:{
s
:
4
:
"test"
;
s
:
11
:
"hello,world"
;}
*左右滑动查看更多
在这里,toString 处于当需要将对象输出的生命周期,当反序列化之后,需要输出对象并将其值用作上下文中使用,那么将会调用该钩子函数。当 $test 参数可控的情况下,在反序列化之后形成对象时,如果需要输出该对象,那么只要 toString 方法中存在可执行代码或者命令的函数,那么就会造成影响。
class
A
{
var
$test =
"demo"
;
function
__toString
()
{
echo
"toString()<br/>"
;
return
$this
->test;
}
}
$a = $_GET[
'test'
];
echo
$a;
echo
"<br/>"
;
$a_unser = unserialize($a);
echo
$a_unser;
O
:1
:"A"
:1
:{
s
:
4
:
"test"
;
s
:
11
:
"hello,world"
;}
*左右滑动查看更多
在这里,sleep 处于当需要序列化对象的生命周期,在序列化之前,存在该钩子,则会返回一个包含对象中所有应被序列化的变量名称的数组,当 $test 参数可控的情况下,在序列化之后形成字符串时,那么只要 sleep 方法中存在可执行代码或者命令的函数,那么就会造成影响。
<?php
class A{
var $test = '';
function __construct($test){
$this->test = $test;
}
function __sleep(){
echo "__sleep()
echo $this->test;
echo
return array('test');
}
}
class B{
public $test_1 = "";
function __wakeup(){
$obj = new A($this->test_1);
echo serialize($obj);
}
}
$a = $_GET['test'];
echo $a,
$a_unser = unserialize($a);
?>
O
:1
:"B"
:1
:{
s
:
6
:
"test_1"
;
s
:
11
:
"hello,world"
;}
*左右滑动查看更多
|
在这里wakeup 是字符串反序列化的时候,会调用该钩子函数,只要执行 unserialize 方法就会触发该方法,其实我们关注 php 反序列化漏洞特别需要关注的魔术方法应该是wakeup,destruct,因为这两个方法只要在反序列化过程中一定会用到的,尤其是__wakeup。
class
A
{
var
$test =
''
;
function
__construct
($test)
{
$this
->test = $test;
}
function
__wakeup
()
{
echo
"__wakeup()<br>"
;
echo
$this
->test;
}
}
$a = $_GET[
'test'
];
echo
$a,
"<br>"
;
$a_unser = unserialize($a);
O
:1
:"A"
:1
:{
s
:
4
:
"test"
;
s
:
11
:
"hello,world"
;}
*左右滑动查看更多
|
四、举例
class
A
{
var
$file =
''
;
function
__construct
($file=
''
)
{
$this
->file = $file;
}
function
readfile
()
{
if
(!
empty
(
$this
->file) && stripos(
$this
->file,
'..'
)===
FALSE
&& stripos(
$this
->file,
'/'
)===
FALSE
&& stripos(
$this
->file,
'\'
)==
FALSE
) {
return
@file_get_contents(
$this
->file);
}
else
{
echo
"false"
;
}
}
}
$x =
new
A();
isset
($_GET[
'test'
]) && $g = $_GET[
'test'
];
if
(!
empty
($g)) {
echo
$g,
"<br>"
;
$x = unserialize($g);
}
echo
$x->readfile();
*左右滑动查看更多
在这里,当实例化之前,调用 construt 魔术方法,如果未给 file 传值,那么 file 默认为空,如果 test 参数为空,则不输出文件,那么要输出文件内容则需要置参数不为空,其需要将参数反序列化,最后再调用反序列化后对象的 readfile 函数,并在这个对象实例中必须要存在 file 值,所以在这里构造反序列化字符串,但是在 readfile 里也有限制,不能使用相对路径,也不能带绝对路径,只能访问当前目录的文件。
O
:1
:"A"
:1
:{
s
:
4
:
"file"
;
s
:
5
:
"1.txt"
;}
*左右滑动查看更多
PHAR
class
AnyClass
{
function
__destruct
()
{
var_dump($_this);
eval
(
$this
-> output);
}
}
file_get_contents($_GET[
"file"
]);
*左右滑动查看更多
生成 phar 文件的 poc:
class
AnyClass
{
function
__destruct
()
{
echo
$this
-> output;
}
}
@unlink(
"phar.phar"
);
$phar =
new
Phar(
'phar.phar'
);
$phar -> stopBuffering();
$phar -> setStub(
'GIF89a'
.
'<?php __HALT_COMPILER();?>'
);
$phar -> addFromString(
'test.txt'
,
'test'
);
$object =
new
AnyClass();
$object -> output=
'system("whoami");'
;
$phar -> setMetadata($object);
$phar -> stopBuffering();
*左右滑动查看更多
在安全开发中,防范php反序列化攻击,常用的建议包括以下几点:
1、不要使用 unserialized() 反序列化来处理未知源的数据。应该使用其他安全的反序列化函数,如 JSON 解码 (json_decode()) 等。
2、对于从未知来源接收到的数据,必须进行数据验证,以避免恶意攻击。应该使用 PHP 序列化自定义数组(serialize())的函数很容易被嵌入一个已在内部定义的 PHP 类名,这个类可能会被用户请求中的序列化字符串调用。
3、序列化的数据应该经过加密,以防止可能存在的中间人攻击,比如数据包嗅探。而且,加密的密钥应该存储在安全的位置。
4、在执行反序列化的代码之前,请确保使用合适的输入校验,以确保没有输入到反序列化(unserialize())函数的未知参数。
下面是一个示例代码,演示如何以安全的方式使用反序列化功能。此示例使用json_decode()函数来解析JSON字符串,避免了使用可能存在漏洞的unserialize()函数:
//接收JSON数据
$serialized_data= $_POST[
'json_data'
];
//解析并验证JSON数据
$data = json_decode($serialized_data,
true
);
if
(!is_array($data)){
die
(
"Invalid input data"
);
}
*左右滑动查看更多
PHP 反序列化漏洞是一类比较常见的安全漏洞,攻击者可以通过在序列化数据中插入恶意代码,在反序列化时触发漏洞,导致代码执行漏洞。为了防范 PHP 反序列化漏洞,可以考虑以下措施:
1、对用户提交的数据进行校验和过滤:可以使用正则表达式、过滤函数等方式对数据进行过滤和校验,确保数据格式正确。例如,限制数据类型、长度、特殊字符等,避免攻击者插入恶意数据。
2、对反序列化数据进行安全检查:需要仔细检查反序列化数据是否存在安全问题,并对特殊字符、敏感函数等进行处理和过滤。
3、使用 PHP 序列化器的安全模式:PHP 提供了一些安全选项,可以在序列化和反序列化时进行校验和限制,防止攻击者插入恶意代码。例如,可以设置 serialize_precision=0、 disable_functions 等参数来限制序列化和反序列化的范围。
4、更新 PHP 版本和扩展库:PHP 在新版本中对一些反序列化漏洞进行了修复,并增加了一些防御措施。可以及时升级 PHP 版本,或者升级相关扩展库,避免已知漏洞。
防范 PHP 反序列化漏洞需要从多方面入手,包括数据校验、安全检查、使用安全模式和升级版本等,提高系统的安全性和鲁棒性。
参考链接:
https:
/
/www.freebuf.com/articles
/web/
167721
.html
原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| PHP反序列化简介、安全开发及防御建议
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论