Hed9eh0g

前进的路上总是孤独的

OPCACHE文件利用

本文共计有4724个字

前言

最近更sqli-labs的writeup同时了解了opcache文件的利用问题,与.user.ini文件利用相同,也算是文件上传的一种奇技淫巧。如果网页可以查看phpinfo文件,那么就可以判断能否利用,结合网上相关文章在此做个记录。

opcache文件

定义

opcache文件的作用是什么?先看看php中文手册的解释吧:

《OPCACHE文件利用》

也就是说,opcache是一种缓存文件,用于缓存我们访问某个网页时需要加载的php代码。比如我们平时加载网页时,浏览器会提前帮我们缓存css和js代码,唯独没有提前缓存php代码,总是需要再访问的时候再进行代码的解析和执行,而opcache文件的作用就是在此。

过程

在 PHP.ini 配置中,你需要指定一个opcache文件的缓存目录,例如:

opcache.file_cache=/tmp/opcache

在该指定的目录中,OPcache 存储了已编译的 PHP 脚本文件,这些缓存文件被放置在和 Web 目录一致的目录结构中。如,编译后的 /var/www/index.php 文件的缓存会被存储在 /tmp/opcache/[system_id]/var/www/index.php.bin 这个目录中。

其中:system_id 是当前 PHP 版本号,Zend 扩展版本号以及各个数据类型大小的 MD5 哈希值。在最新版的 Ubuntu(16.04)中,system_id 是通过当前 Zend 和 PHP 的版本号计算出来的,其值为 81d80d78c6ef96b89afaadc7ffc5d7ea。这个哈希值很有可能被用来确保多个安装版本中二进制缓存文件的兼容性。当 OPcache 在第一次缓存文件时,上述目录就会被创建。

适用前提

查看phpinfo.php文件,如果有下面配置即可使用opcache:

opcache.enable => On => On

利用原理

设想存在某个网站:其网页index.php具有缓存文件index.php.bin,而访问index.php的时候加载缓存index.php.bin,倘若这时候能够上传一个我们精心设计的index.php.bin,我们便可以覆盖原来所缓存的index.php.bin文件,让网页加载我们所设计的文件,即可getshell了。

而想要达成以上设想,我们需要做的是找到原bin文件所在的目录,这样才能够覆盖它。而根据刚才提到的过程可知:我们需要推断出system_id的值。然而网上的大佬早已把脚本写好了:

https://github.com/GoSecure/php7-opcache-override

例子:

《OPCACHE文件利用》其中,生成system_id所需要的字段值需要能够查看phpinfo文件内容相关配置才能获得。

大致过程

假设目标网页能够执行phpinfo文件,且其中包含opcache相关配置,且能够上传bin文件。

1、首先在本地创建 webshell 文件 index.php ,例如:

<?php
    system($_GET['cmd']);
?>

2、在 PHP.ini 文件中设置opcache.file_cache与目标phpinfo文件中显示的opcache目录相同。

3、然后访问这个文件,即可在所设定的目录下生成opcache缓存文件。

4、修改 index.php.bin 文件头里的 system_id 为目标站点的system_id。在文件头里的签名部分的后面就是system_id的值。

《OPCACHE文件利用》

5、通过上传漏洞将修改后的 index.php.bin 上传至 /tmp/opcache/[system_id]/var/www/index.php.bin ,覆盖掉原来的 index.php.bin。

6、重新访问 index.php ,此时就运行了我们的 webshell。

0ctf ezdoor

这里以0ctf的一道题为例,给出代码:

<?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;
}

先逐行分析一下代码:

首先程序会在sandbox下根据你的ip创建一个文件夹,然后在该文件夹下创建一个index.php文件。

$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
  mkdir($dir);
}
if(!file_exists($dir . "index.php")){
  touch($dir . "index.php");
}

接下来就是一个clear的方法,其功能就是删除文件,再删除文件夹。

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);
}

然后就是对所传的action变量,在switch下的几个选项,总共有六个选项,分别是:

打印你的路径、打印phpinfo文件内容、重置(也即运行clear方法)、打印当前时间、上传、文件包含index.php

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;
}

首先对文件大小有限制,太大会触发clear函数。然后限制文件后缀名不能存在h,这意味着phtml,php,phps,.htaccess等全军覆没。最终调用move_uploaded_file转移文件。

根据刚才说将的内容,发现本题十分适合利用opcache来解决,上传的文件后缀限制了h,但是bin后缀文件恰好可以上传。另外system_id也可以利用脚本结合phpinfo文件内容来解决。解决方法的详细步骤就不必再次阐述了。

但是与刚才提到的过程不同的是,本题存在一个新的限制,它对opcache文件存储目录开启了时间戳效验。什么是时间戳效验?如果服务器启用了时间戳校验,OPcache 会将被请求访问的 php 源文件的时间戳与对应的缓存文件的时间戳进行对比校验。如果两个时间戳不匹配,缓存文件将被丢弃,并且重新生成一份新的缓存文件。要想绕过此限制,攻击者必须知道目标源文件的时间戳。

如何绕过?不要忘了switch的六个选项中还有一个time的case,只要我们利用重置的case结合time即可得到目标文件的时间戳。

import requests
print requests.get('http://202.120.7.217:9527/index.php?action=time').content
print requests.get('http://202.120.7.217:9527/index.php?action=reset').content
print requests.get('http://202.120.7.217:9527/index.php?action=time').content

运行后可发现两次action=time的结果一致。

然后利用hex工具,更改缓存文件的system_id和timestamp两个字段为题目中的值。

最终可以运行我们构造的shell文件。

参考文章

1、文章1

2、文章2

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注