文章目录
  1. 1. opcache原理
  2. 2. 如何利用
  3. 3. 本地检测

在 PHP 7.0 发布之初,就有不少 PHP 开发人员对其性能提升方面非常关注。在引入 OPcache 后,PHP的性能的确有了很大的提升,之后,很多开发人员都开始采用 OPcache 作为 PHP 应用的加速器。OPcache 带来良好性能的同时也带来了新的安全隐患。

在 PHP 7.0 发布之初,就有不少 PHP 开发人员对其性能提升方面非常关注。在引入 OPcache 后,PHP的性能的确有了很大的提升,之后,很多开发人员都开始采用 OPcache 作为 PHP 应用的加速器。OPcache 带来良好性能的同时也带来了新的安全隐患。

opcache原理

PHP是一种脚本语言,默认情况下,它会编译任何要求它运行的文件,从编译中获取OPCodes,运行它们并立即将其清除。在生产服务器上,PHP代码在几个请求之间不太可能发生变化,因此,编译步骤将始终读取相同的源代码,从而导致运行完全相同的OPCode。这对于时间和资源来说是一个很大的浪费,因为每一个脚本都会针对每个请求调用PHP编译器。

image

于是大佬设计出了OPCode缓存,每个PHP脚本只编译一次,并将生成的OPCodes缓存到共享内存中,这样PHP-FPM就可以从内存中直接读取OPCodes执行。

image

OPCache已经成为官方推荐的OPCode缓存解决方案,并从PHP 5.5.0开始捆绑到PHP源代码中

image

如何利用

可以针对上传攻击或者是拿到主机权限做修改来进行利用。如果是上传攻击,需要php.ini配置如下

1
2
3
4
5
6
[opcache]
zend_extension=opcache.so
opcache.enable=1
opcache.file_cache = /tmp/opcache
opcache.validate_timestamps = 0
opcache.file_cache_only = 1 ; PHP 7's default is 0

对应解释如下,参考

名字 默认 可修改范围 含义
opcache.enable “1” PHP_INI_ALL 是否启用opcache
opcache.validate_timestamps “1” PHP_INI_ALL 如果置为1,则OPCACHE会自动检测文件的时间戳(检测周期为revalidate_freq),并根据文件的时间戳来更新opcode,如果置为0,则只能手动去重启opcache或重启webserver以使更新后的php文件生效
opcache.revalidate_freq “1” PHP_INI_ALL opcache自动检测文件是否更新的周期,单位秒。如果是0,则每次请求时opcache都要进行检测。当validate_timestamps为0时,本指令无效
opcache.file_cache_only “0” PHP_INI_SYSTEM 启用或禁用在共享内存中的 opcode 缓存
opcache.file_cache NULL PHP_INI_SYSTEM 配置二级缓存目录并启用二级缓存。启用二级缓存可以在 SHM内存满了、服务器重启或者重置 SHM 的时候提高性能。 默认值为空字符串 “”,表示禁用基于文件的缓存。

TIPS:

PHP总共有4个配置指令作用域:(PHP中的每个指令都有自己的作用域,指令只能在其作用域中修改,不是任何地方都能修改配置指令的)
PHP_INI_PERDIR:指令可以在php.ini、httpd.conf或.htaccess文件中修改
PHP_INI_SYSTEM:指令可以在php.ini 和 httpd.conf 文件中修改
PHP_INI_USER:指令可以在用户脚本中修改
PHP_INI_ALL:指令可以在任何地方修改

所以我们需要的关键条件不能通过ini_set动态更改

在指定的目录中,OPcache 会存储已编译的 PHP 脚本文件,这些缓存文件被放置在和 Web 目录一致的目录结构中。如,编译后的 /var/www/index.php 文件的缓存会被存储在 /tmp/opcache/[system_id]/var/www/index.php.bin 中。system_id 是当前 PHP 版本号,Zend 扩展版本号以及各个数据类型大小的 MD5 哈希值。计算system_id可以用这个,如果计算出的system_id值和预期不同,可以参考我改动的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env python

# Copyright (c) 2016 GoSecure Inc.

import sys
import re
import requests
from md5 import md5

if len(sys.argv) != 2:
print sys.argv[0] + " [file|URL]"
exit(0)

if (sys.argv[1].startswith("http")):
text = requests.get(sys.argv[1]).text
else:
with open(sys.argv[1]) as file:
text = file.read()
file.close()

# PHP Version
php_version = re.search('<tr><td class="e">PHP Version </td><td class="v">(.*) </td></tr>', text)

if php_version == None:
php_version = re.search('<h1 class="p">PHP Version (.*)', text)

if php_version == None:
print "No PHP version found, is this a phpinfo file?"
exit(0)

php_version = php_version.group(1)

# Zend Extension Build ID
zend_extension_id = re.search('<tr><td class="e">Zend Extension Build </td><td class="v">(.*) </td></tr>', text)
if zend_extension_id == None:
print "No Zend Extension Build found."
exit(0)
zend_extension_id = zend_extension_id.group(1)

# Architecture
architecture = re.search('<tr><td class="e">System </td><td class="v">(.*) </td></tr>', text)
if architecture == None:
print "No System info found."
exit(0)
architecture = architecture.group(1).split()[-1]

# Zend Bin ID suffix
if architecture == "x86_64":
bin_id_suffix = "148888"
else:
bin_id_suffix = "144444"

zend_bin_id = "BIN_" + bin_id_suffix

# Logging
print "PHP version : " + php_version
print "Zend Extension ID : " + zend_extension_id
print "Zend Bin ID : " + zend_bin_id
print "Assuming " + architecture + " architecture"

digest = md5(php_version + zend_extension_id + zend_bin_id).hexdigest()
print "------------"
print "System ID : " + digest

如果是上传攻击,需要知道以上的opcache的配置信息(可以是从phpinfo),需要能上传到opcache.file_cache指定的目录,需要计算出正确的system_id,然后,本地创建一个webshell,生成一个编译后的 webshell 二进制缓存文件,把文件头的system_id改成前面提到的计算出来的目录机器的就行。

image

最后,上传恶意webshell 二进制缓存文件到opcache.file_cache指定的目录,覆盖掉目标机器的正常php文件生成的二进制缓存文件。访问那个原本正常的php文件,getshell

如果目标机器file_cache_only = 0,内存缓存方式的优先级高于文件缓存,那么重写后的 OPcache 文件(webshell)是不会被执行的。但是,当 Web 服务器重启后,就可以绕过此限制。因为,当服务器重启之后,内存中的缓存为空,此时,OPcache 会使用文件缓存的数据填充内存缓存的数据,这样,webshell 就可以被执行了。还有就是如果对方存在一些常年不会访问到的php文件,就不会存在该文件的内存缓存和文件缓存。比如WordPress 等这类框架里面,有许多过时不用的文件依旧在发布的版本中能够访问,如:registration-functions.php,通过上传 webshell 的二进制缓存文件为 registration-functions.php.bin ,之后请求访问 /wp-includes/registration-functions.php ,此时 OPcache 就会加载我们所上传的 registration-functions.php.bin 缓存文件。

如果目标机器validate_timestamps = 1,php会按照opcache.revalidate_freq设定的时间间隔定期校验php 源文件的时间戳与对应的缓存文件的时间戳的差别,如果两个时间戳不匹配,缓存文件将被丢弃,并且重新生成一份新的缓存文件。要想绕过此限制,攻击者必须知道目标源文件的时间戳。在比如wordpress等的框架中,有些文件从2012年之后从没有被修改过,如:registration-functions.php 和 registration.php 。知道了时间戳,攻击者就可以绕过 validate_timestamps 限制,成功覆盖缓存文件,执行 webshell。二进制缓存文件的时间戳在 34字节偏移处。

image

当然如果攻击者能拿到主机权限,攻击者可能会按照上面的配置藏一个opcache后门。一般的基于检测源文件的webshell扫描工具无法扫描出当前php存在后门,如D盾。

本地检测

如何知道自己中招没有呢?不是没有办法。使用这个

其中OPcache Malware Hunter通过比对和现有缓存文件的差异来判断是否发生了任何更改

1
2
$ ./opcache_malware_hunt.py
Usage : ./opcache_malware_hunt.py [opcache_folder] [system_id] [php.ini]

image
OPcache Disassembler用于反编译opcache文件

1
2
3
4
$ ./opcache_disassembler.py
Usage : ./opcache_disassembler.py [-tc] [file]
-t Print syntax tree
-c Print pseudocode

提供了两种显示选项,语法树和伪代码,伪代码更容易理解

参考链接:http://blog.gosecure.ca/2016/04/27/binary-webshell-through-opcache-in-php-7/

文章目录
  1. 1. opcache原理
  2. 2. 如何利用
  3. 3. 本地检测