序列化与反序列化
通常我们定义了一个类的对象,其中保存了一些属性值,为了方便下次可以继续使用在这个对象或者在其他的文件中可以使用该对象,于是就可以调用serialize()函数将该对象序列化为字符串的形式,将该字符串保存起来,等到需要使用该对象时只需将该字符串传过去并调用unserialize()函数对其反序列化即可。
serialize():将一个对象转成字符串形式,方便保存以便于下次再次反序列化出该对象直接使用。
unserialize():将序列化后的字符串反序列化成一个对象。
简单示例:
demo.php
<?php
class Test{
public $name;
public $blog;
}
$test = new Test();
$test->name = "SKI12";
$test->blog = "https://blog.csdn.net/ski_12";
echo "创建对象并给其属性赋值:<br>";
foreach($test as $key => $value) {
echo $key." => ".$value."<br>";
}
$str = serialize($test);
echo "<br>对象序列化后的字符串:".$str;
$f = fopen('test.txt', 'w');
fwrite($f, $str);
fclose($f);
?>
运行后,可以看到创建的对象序列化后的字符串:
demo2.php
<?php
$f = fopen("test.txt", "r");
$str = fread($f, filesize("test.txt"));
echo "读取序列化的字符串:".$str;
echo "<br><br>经过反序列化后的结果如下:<br>";
$test = unserialize($str);
foreach($test as $key => $value) {
echo $key." => ".$value."<br>";
}
?>
运行后,可以看到反序列化后得到对象实例:
可知,创建的对象序列化后的内容为:
O:4:"Test":2:{s:4:"name";s:5:"SKI12";s:4:"blog";s:28:"https://blog.csdn.net/ski_12";}
简单分析一下,“O”即Object对象,“4”为对象名的长度,“Test”即对象名,“2”为对象的属性个数;进入大括号为属性的内容,“s”即string字符串类型,“4”即该属性名的长度,“name”即该属性名,接着“;”分号间隔键值或不同属性,这里为间隔键和值,后续的同理分析。
反序列化漏洞
基本概念
PHP在进行反序列化操作时,若存在相应的魔法函数、unserialize()函数的参数可控且可以传递到魔法函数中执行相应的敏感操作,则会造成PHP反序列化漏洞的风险。
利用前提
- unserialize()函数的参数可控;
- 代码中存在一个构造函数、析构函数、__wakeup()函数中有向php文件中写数据的操作的类或执行PHP代码或命令执行的类;
- 所写的内容需要有对象中的成员变量的值。
PHP魔法函数
下面列下可能经常碰到的魔法函数,其余的查查资料也知道了。
__construct():构造函数,当一个对象创建时被调用。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
__destruct():析构函数,当一个对象销毁时被调用。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
__toString():当一个对象被当作一个字符串使用。此方法必须返回一个字符串,否则将发出一条E_RECOVERABLE_ERROR级别的致命错误。
__sleep():常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误。
__wakeup():经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup(),预先准备对象需要的资源。
一个简单的测试Demo:
<?php
class Vuln{
public $name;
public $blog;
function __construct(){
echo "[*]调用__construct()<br>";
}
function __destruct(){
echo "[*]调用__destruct()<br>";
}
function __wakeup(){
echo "[*]调用__wakeup()<br>";
}
function __sleep(){
echo "[*]调用__sleep()<br>";
return array('name', 'blog');
}
function __toString(){
echo "[*]调用__toString()<br>";
return $this->name." : ".$this->blog."<br>";
}
}
echo "开始初始化对象...<br>";
$test = new Vuln();
$test->name = "SKI12";
$test->blog = "https://blog.csdn.net/ski_12";
echo "创建对象并给其属性赋值:<br>";
foreach($test as $key => $value) {
echo $key." => ".$value."<br>";
}
echo "开始序列化对象...<br>";
$str = serialize($test);
echo "对象序列化后的字符串:".$str."<br>";
echo "开始反序列化对象...<br>";
$str2 = unserialize($str);
echo $str2;
?>
运行后可以看到各个魔法函数的调用次序:
反序列化漏洞Demo
PHP代码执行示例:
<?php
class Vuln{
public $name;
function __destruct(){
eval($this->name);
}
}
$str = $_GET['ski_12'];
unserialize($str);
?>
然后根据利用前提条件,构造序列化的字符串payload访问即可:
或者是命令执行,只需将eval()换为system()等即可:
<?php
class Vuln{
public $name;
function __destruct(){
system($this->name);
}
}
$str = $_GET['ski_12'];
unserialize($str);
?>
防御
1、要严格控制unserialize()函数的参数,坚持用户所输入的信息都是不可靠的原则;
2、要对于反序列化后的变量内容进行检查,以确定内容没有被污染。