在本文中我们将分析通过利用PHP7默认的 OPcache引擎来对漏洞进行利用的技巧。通过这个漏洞利用技巧,我们将能绕过“禁止web目录的文件读写”(http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html)的防护,还能在主机中执行任意代码。

OPcache

OPcache是PHP 7.0内建的新型缓存引擎,它会编译php的脚本,然后在内存中生成对应的字节码。

图片1.png

它还支持在文件系统中进行缓存,我们可以在PHP.ini中指定缓存目录

opcache.file_cache=/tmp/opcache

在上面指定的目录中,OPcache会将编译好的PHP脚本和对应的PHP脚本放在同一个目录结构之中。比如说,/var/www/index.php所编译的脚本会被保存为/tmp/opcache/[system_id]/var/www/index.php.bin。

这里的system_id是一个包含了当前PHP版本信息,Zend框架的扩展ID和各种数据类型信息的哈希值。在Ubuntu最新版16.04中,system_id是由当前Zend框架和PHP的版本号所组成的(81d80d78c6ef96b89afaadc7ffc5d7ea),这些哈希值有可能是被用来确保二进制兼容性的,这个目录会在OPcache第一次进行缓存时生成。

我们将会在下面看到的是每一个OPcache文件还会在文件的header域中保存system_id的对应的副本。

关于OPcache文件夹最有意思的点就在于,用户启动该服务后就会拥有OPcache生成的所有文件夹/文件(在/tmp/opcache/目录之下)的写入权限。

下面是OPcache文件夹的权限情况

图片2.png 

你可以看到OPcache生成的文件夹对用户www-data是可写的,这就导致了我们可以通过重写目录中的缓存文件为webshell,然后执行任意代码。

攻击场景

首先,我们必须获得缓存文件夹的地址(/tmp/opcache/[system_id]),以及目标PHP文件的地址(/var/www/…)。

为了简单起见我们假设网站存在一个phpinfo()文件我们可以从这个文件中获取到缓存文件夹和文件源代码的存储地址,还有在计算system_id的时候将会用到的数据(我们已经开发出一款能够通过phpinfo()文件来计算system_id的工具。你可以在这里下载(https://github.com/GoSecure/php7-opcache-override))。

这里还要再提的一点是目标网站不能对文件上传进行限制。

我们假设php.ini中配置的额外数据为:


opcache.validate_timestamp = 0    ; PHP 7's default is 1

opcache.file_cache_only = 1       ; PHP 7's default is 0

opcache.file_cache = /tmp/opcache

接下来,我们来分析一下攻击的过程:

如下图,我们已经在网站中找到了一个任意上传漏洞,并且/var/www/可写,我们的目标就是将后门代码替换到/tmp/opcache/[system_id]/var/www/index.php.bin中。

图片3.png

1、在本地创建一个包含Webshell的PHP文件,将其命名为”index.php”:


<?php

   system($_GET['cmd']);

?>

2、配置PHP.ini文件中的opcache.file_cache选项。

3、使用php -S 127.0.0.1:8080命令在本地启动一个Web服务器,通过使用命令wget 127.0.0.1:8080向服务器请求index.php文件来触发缓存引擎。

4、定位到我们在第一步中设置的缓存文件夹,你就会发现一个名为index.php.bin的文件,这个文件就是经过编译处理后的webshell,如下图。

图片4.png

5、由于本地system_id很可能与目标主机的system_id不同,所以我们必须打开index.php.bin文件,并将我们的system_id修改成目标主机的system_id。正如之前所提到的,system_id可以被猜解,例如暴力破解,或者根据phpinfo()文件中的服务器信息计算出来(https://github.com/GoSecure/php7-opcache-override/blob/master/system_id_scraper.py)。我们可以在文件签名之后替换system_id,如下图。

图片5.png

6、利用任意上传漏洞将文件上传至/tmp/opcache/[system_id]/var/www/index.php.bin

7、刷新网站的index.php,网站将会自动执行我们的webshell。

图片6.png

更深入一点

php.ini中至少有两个配置项可以造成另类的行为

1、禁止file_cache_only

2、允许validate_timestamp

绕过内存缓存(file_cache_only = 0)

如果内存缓存的优先级高于文件缓存,那么重写OPcache文件并不会执行我们的webshell。在上传完webshell后,如果服务器重启之后,我们就可以绕过这种限制。由于内存缓存将被清空,OPcache这时会将文件缓存填充到内存中,从而执行了我们的webshell。

因为上面说到的这种机制的原因,我们将很有可能在服务器不重启的情况下实现webshell的执行。

在WordPress等网站框架之中,还是会有一些过时的文件可以被公开访问到(比如registration-functions.php(https://github.com/WordPress/WordPress/blob/703d5bdc8deb17781e9c6d8f0dd7e2c6b6353885/wp-includes/registration-functions.php))。

由于这些文件已经过时了,所以系统不会再加载这些文件,也不会在内存和文件系统的缓存中有这些缓存的文件。在我们上传了恶意代码(registration-functions.php.bin)之后访问相关的网页(/wp-includes/registration-functions.php),OPcache就会自动执行我们的webshell。

绕过时间戳认证(validate_timestamps = 1)

如果服务器启用了时间戳认证功能,OPcache将会对被请求的PHP源文件的时间戳进行验证。如果该文件的时间戳与缓存文件header域中的时间戳相匹配,那么服务器就会允许访问。反之,缓存文件将会被丢弃,并创建出一个新的缓存文件。为了绕过这种限制,攻击者必须知道目标源文件的时间戳。

这也就意味着,在WordPress等网站框架之中,源文件的时间戳是可以获取到的,因为当开发人员将代码文件从压缩包中解压出来之后,时间戳信息仍然是保持不变的,如下图。

图片7.png

有趣的是,其中的有些文件从2012年起就再也没有进行过任何的修改(请注意registration-functions.php和registration.php文件)。因此,即使是不同版本的WordPress,它们的时间戳也是一样的。在获取到了文件时间戳的信息之后,攻击者就可以修改他们的恶意代码,并且成功覆盖服务器的缓存数据。时间戳位于文件开头的第34字节的位置,如图

图片8.png

Demo

正如我们在此之前提到的,大家可以在我们的GitHub代码库(https://github.com/GoSecure/php7-opcache-override)中下载工具。工具中包含了 010编辑模板、生成SYSTEM_ID的工具还有本篇文章所用到的网页代码。

结论

总之,这种新型的攻击方法针对具体的环境而言的,因为这并不是PHP的通用漏洞。很多操作系统,例如Ubuntu 16.04都会默认安装PHP 7,所以在我们的开发过程中,更加应该谨慎地审查我们的代码,并检查网站中是否存在文件上传漏洞。