渗透学习
不安全的反序列化之反序列化基础
文章目录
- 渗透学习
- 前言
- *本文只做学习用途,严禁利用本文提到的技术进行非法攻击,否则后果自负,本人不承担任何责任。*
- 一、序列化和反序列化
- 二、PHP反序列化漏洞
- 1.serialize()
- 2.unserialize()
- 3.反序列化漏洞
- 总结
前言
本系列用于记录本人渗透学习的过程,主要内容围绕Owasp TOP 10展开。
不安全的反序列化之反序列化基础主要以PHP反序列化漏洞为例,理解不安全的反序列化的成因。为接下来反序列化引发的诸多场景介绍奠定基础。
本文只做学习用途,严禁利用本文提到的技术进行非法攻击,否则后果自负,本人不承担任何责任。
一、序列化和反序列化
序列化:把对象转换为字节序列,永久存到磁盘中。在网络中传输对象也要进行序列化。
反序列化:从磁盘中读取字节序列将它们反序列化成对象读出来。
简单来说:把对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。
序列化的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。
我们必须在把对象转成字节数组的时候就制定一个规则(序列化),那我们从IO流里读出数据的时候再以这种规则把对象还原回来(反序列化)
现在常见的序列化方式有:JDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言。
二、PHP反序列化漏洞
php反序列化漏洞,又叫php对象注入漏洞
php中有两个函数serialize()和unserialize()
1.serialize()
当在php中创建了一个对象后,可以通过serialize()把这个对象转变成一个字符串,保存对象的值方便之后的传递与使用
<?php
class chybeta{var $test = '123';
}$class1 = new chybeta;
$class1_ser = serialize($class1);
print_r($class1_ser);
?>
我们创建一个新的对象,并打印其序列化结果打印出来:
O:7:“chybeta":1:{s:4:"test";s:3:"123";}
这里的O
代表存储的是对象(object),假如给serialize()传入的是一个数组,那它会变成字母a。7
表示对象的名称有7个字符。"chybeta"
表示对象的名称。1
表示有一个值。{s:4:"test";s:3:"123";}
中,s
表示字符串,4
表示该字符串的长度,"test"
为字符串名称,之后类似。
2.unserialize()
unserialize()可以从已存储的表示中创建PHP的值,单就本次案例而言,它可以从序列化后的结果中恢复对象(object)。
<?php
class chybeta{var $test = '123';
}
$class2 = 'O:7:"chybeta":1:{s:4:"test";s:3:"123";}'; print_r($class2);
echo "</br>";
$class2_unser = unserialize($class2);
print_r($class2_ser);
?>
当使用unseriallize()恢复对象时,将调用_wakeup()成员函数。
3.反序列化漏洞
由此可得,当传给unserialize()参数可控时,我们可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数
首先我们先来了解PHP的魔术方法:
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(),__invoke(), __set_state(), __clone() 和 __debugInfo() 等方法在 PHP 中被称为"魔术方法"(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。
着重关注以下几个:
构造函数__construct():当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
析构函数__destruct():当对象被销毁时会自动调用。
__wakeup() :如前所提,unserialize()时会自动调用。
__sleep() :在对象在被序列化之前运行
__toString() :在一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。
借用学习案例理解
在log.php中构建一个WTFLog 类,调用 loginfo 方法会记录一条记录到access.log文件中,当这个类完成它的使命的时候(exit),会删除掉filename对应的文件,现在是access.log。
设计一个存在反序列化漏洞的案例:
该代码的业务目标是从用户处收集序列化后的数据,前面的内容不存在问题,问题是直接使用客户端可以控制的输入点($_GET[‘pet_serialized’]),在不进行验证的情况下,直接实例化了这段代码。
接下来是危险注入点的利用方式(重新创建一个新的poc.php,把log.php中的WTFlog类引进来)。
index.php是我们设置的一个文件,会输出一句话:
运行poc.php,就可以得到我们想要序列化后的WTFlog字符串:
O:6:“WTFLog”:1:{s:8:“filename”;s:9:“index.php”;}
利用这个字符串,去调用pets.php
我们发现本来要删除掉的access.log文件,因为filename的更改删除了index.php
回顾一下整个漏洞利用的过程:
1.需要有一个漏洞触发点 pets.php 内的$pet = unserialize($_GET['pet_serialized']);
2.需要有一个相关联的,有魔术方法(会被自动调用)的类。log.php (WTFLog 内的__destruct函数)
3.漏洞的效果取决于__destruct 这个魔术函数内的操作,这里的是可以操控的可以删除的log 的 filename。
4.构建poc.php ,利用程序,先序列化后,从可控输入$_GET['pet_serialized']
输入进去。
再看另一个学习案例
先创建一个类,并输出filename的值,最后输出序列化字符串:
<?phpclass F{public $filename='a.txt';
}$a = new F();
echo $a->filename.'<br />';
echo serialize($a);
输出结果:
再创建一个类,并输出filename的值,最后输出序列化字符串:
<?phpclass F{public $filenameF='bcda.txt';
}$a = new F();
echo $a->filenameF.'<br />';
echo serialize($a);
输出结果:
这两个代码定义的类一样, 只是属性不一样。
当我们使用以下代码反序列时:
<?phpclass F{public $filename='a.txt';function __destruct(){echo '--------------><br />';}
}$a = new F();
echo $a->filename.'<br />';
echo serialize($a);
$b = unserialize('O:1:"F":1:{s:9:"filenameF";s:8:"bcda.txt";}');
echo '<br />'.$b->filename;
echo '<br />'.$b->filenameF;
结果如图所示:
可以看到析构函数__destruct()输出了两次,说明a,b是同一类,a,b对象被销毁的时候分别执行了一次。只不过b多了一个属性filenameF。
我们可以利用自动执行某些函数或方法的特性,执行我们想要的操作:
创建如下代码(销毁对象时会读取文件中内容并输出):
<?phpclass F{public $filename='d:\\phpstudy\\www\\a.txt';#$filename为publicfunction __destruct(){$data = readfile($this->filename);echo $data;}
}$a = new F();
echo $a->filename.'<br />';
执行结果:
再衍生创建以下测试代码:
<?phpclass F{public $filename='d:\\phpstudy\\www\\a.txt';#$filename为publicfunction __destruct(){$data = readfile($this->filename);echo $data.'<br />';}
}$a = new F();
echo $a->filename.'<br />';
$b = unserialize($_GET[a]);
这代码中我们用unserialize反序列一个字符串变成一个类对象, 也就是说这个代码中, 会有两个类对象, 一个是 a , 一 个 是 用 户 可 控 的 a, 一个是用户可控的 a,一个是用户可控的b ($b 中的filename可控, 因为class F中的 filename为public)。
当代码运行结束时, 会运行两个析构函数。 第一次运行的析构函数中, filename为$a
中默认的 ‘d:\phpstudy\www\a.txt’, 第二个因为是从$_GET[a]
获得字符串, 所以我们可以控制第二个对象中的filename。
从而使得 __destruct 函数可以读取到我们想要读的文件。
构建一个类和上面代码的类相同的代码:
<?phpclass F{public $filename='a.txt';
}$a = new F();
$a->filename = 'd:\\phpstudy\\www\\2.txt';
echo serialize($a);
此代码修改了filename的值,并生成了序列化字符串:O:1:"F":1:{s:8:"filename";s:21:"d:\phpstudy\www\2.txt";}
。
2.txt里存储着我们想要得到的password
我们将这个会改变原有filename的值的代码的序列化字符串发送到测试代码中去:http://localhost/11.php?a=O:1:%22F%22:1:{s:8:%22filename%22;s:21:%22d:\phpstudy\www\2.txt%22;}
这样测试代码除了有对象 a 外 , 还 反 序 列 化 创 建 了 一 个 对 象 a外,还反序列化创建了一个对象 a外,还反序列化创建了一个对象b,而这个b中的属性filename已经被我们修改了。最后运行两次__destruct析构函数时,第一次读取了a.txt,另一次读取2.txt
总结一下反序列化漏洞出现的原因:
<?php
include "xxx.php";#此文件中有类定义, 有魔术函数或方法, 且输入参数能被控制
class Classname{#存在有害魔术函数或方法,且输入参数能被控制
}do something...
do something...
do something...#存在反序列化函数
unserialize('用户输入有害参数未过滤')
do something...
do something...
do something...
总结
以上介绍了序列化和反序列化,然后举了两个PHP反序列化漏洞的案例。案例都是因为使用了魔术函数和方法,而且存在没有进行过滤的反序列化函数,在对象被销毁时触发析构函数造成危险。接下来的反序列化的文章将会以一道CTF比赛的题目入手,通过代码审计发现反序列化漏洞并进行利用,加深对反序列化的理解。