0ctf-ezdoor-复现分析

在学习opcache的时候,看到了这个题目,刚好有环境,就来复现一下,这个题目也让我学到了很多。

创建镜像:

docker build -t 0ctf-ezdoor .

启动容器:

docker run -itd -p 9010:80 --name 0ctf-ezdoor 0ctf-ezdoor

源码如下:

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"); //如果index.php不存在,则直接用touch创建
} 
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"); //显示phpinfo信息 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")) { //取文件的后缀并且过滤了h,
则所有php后缀都不能上传后面的stristr(pathinfo)是用来判断以“.”隔断后的字符串中是否含有“h”字符,在这里pathinfo是以字符串中最后一个“.”来进行隔断。
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; }

最终包含的是$dir."index.php",并且无法上传php后缀,最后有include,那么应该是包含shell,所以我们如果能够通过上传覆盖index.php,就能够getshell,然后根据给出的flag路径去读flag

此时首先读一下phpinfo,这个是个txt的phpinfo信息,并且里面删除了一些配置项, 在里面发现opcache是开启的,

0ctf-ezdoor-复现分析_第1张图片

预期解法:

opcache突破口

A网站的网页index.php具有缓存文件index.php.bin
而访问index.php的时候加载缓存index.php.bin
倘若这时候具有上传,我们可以覆盖index.php.bin
是不是就会加载我们的恶意文件了呢?
题目中虽然过滤php类型的结尾,但是却未过滤bin的结尾

 0ctf-ezdoor-复现分析_第2张图片

通过opcache.file_cache可以看到opcache的存储路径信息在/tmp/cache下

执行docker exec -it bash_name bash 进入docker容器发现实际上目录是cache/systemid,就是每个用户都会有一个id,来鉴别的

所以我们的目的很明确,就是去覆盖此index.php.bin来上传我们自己的index.php.bin,那么当再次访问index.php.bin时实际上就是访问的我们的恶意index.php文件

所以首先要知道服务器缓存文件的目录,计算一下服务器端的systemid,利用https://github.com/GoSecure/php7-opcache-override,因为代码需要指定一个phpinfo的页面,但是其最终解析出来进行计算的一些配置项才是最重要的,因此找到目标服务器的这些配置项

0ctf-ezdoor-复现分析_第3张图片

 

php_version=7.0.28

zend_extension_id=API320151012,NTS

0ctf-ezdoor-复现分析_第4张图片

zend_bin_id由这两部分组成

即zend_bin_id=BIN_SIZEOF_CHAR48888

接下来利用公式计算一下就能得到:

systemid=7badddeddbd076fe8352e80d8ddf3e73

 但是这个systemid计算出来的跟我在docker容器里面看到的名字不一样,这里查看一下php的版本,题目给的是7.0.28,但是此时我复现的时候从hub库拖过来的php7版本是7.0.33,因此这里计算systemid时要和题目的php版本一致,这里把php的版本改成7.0.33计算一下就行了,得到system_id为0b8bd94e9858e5d32d058dc0acf75014

 

和我docker是相符的,说明没问题,此时已经得到了服务器端的opcache路径,那么下一步就是通过上传去覆盖此index.php.bin

opcache文件生成

首先要在本地搭一个根目标服务器一样的环境,所以pull一个环境下来:

sudo docker pull php:7.0.33-apache

然后通过镜像创建容器:

docker run -itd -p 9010:80 --name php:7.0.33-apache opcache

此时容器已经起来了,进入配置与服务器相同的路径

docker exec -it opcache bash

因为我们的index.php在用户的沙盒中,因此可以使用pwd先看看路径

 0ctf-ezdoor-复现分析_第5张图片

可以看到路径为sandbox/fac849dc498b60000e200f3f2a2712b54da39b92/,所以首先新建一个文件夹吧,然后开一个index.php,先看看能不能成功

0ctf-ezdoor-复现分析_第6张图片

此时文件生成了,需要配置一下php.ini的opcache

直接从docker hub拉来的镜像我发现里面没有加载php.ini,但是看到加载php.ini的路径为/usr/loca/etc/php,所以去这个目录看看,

0ctf-ezdoor-复现分析_第7张图片

应该是给了两个ini,选定一个更名为php.ini让apache去加载,所以cp拷贝一份就行了,我配置了如下选项:

zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/opcache.so //默认存在该扩展,只要把so文件引进来即可
opcache.file_cache => /tmp/cache => /tmp/cache opcache.enable => On => On opcache.validate_timestamps => On => On opcache.file_cache_only => 1 => 1

然后重启一下apache,这里不能用service apache2 restart,容器会断掉,因为容器相当于一个cmd环境,重启自己会断,这里用reload重新加载一下配置文件,此时刷新phpinfo看看

0ctf-ezdoor-复现分析_第8张图片

 

 0ctf-ezdoor-复现分析_第9张图片

0ctf-ezdoor-复现分析_第10张图片

opcache扩展也打开了,访问我们模拟环境的index.php试试生成bin文件:

 

此时,因为目标服务器timestamps为on,因此我们不仅要置换bin里面的systemid还要置换一下timestamps

运行以下代码就能获得最新的文件时间戳:

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

 

 

此时只要用010修改一些systemid和timestamps,直接使用https://github.com/GoSecure/php7-opcache-override中提供的模板文件来帮助我们解析bin文件,此时就能看到要修改的两个字段

0ctf-ezdoor-复现分析_第11张图片

 修改以后保存,然后本地构造上传表单,进行上传,因为我们想要覆盖目标服务器的bin文件,那么路径必须与其相同才行,这里直接将$_GET['name']与沙盒路径拼接在了一起,没有对变量进行过滤

0ctf-ezdoor-复现分析_第12张图片

所以此时确定路径可以进行路径穿越,可以穿越到任意目录,所以可以直接通过systemid来构造路径为:

../../../../../tmp/cache/0b8bd94e9858e5d32d058dc0acf75014/var/www/html/sandbox/fac849dc498b60000e200f3f2a2712b54da39b92/index.php.bin
<html>
<body>
 <form action="http://127.0.0.1:8585/?action=upload&name=../../../../../tmp/cache/0b8bd94e9858e5d32d058dc0acf75014/var/www/html/sandbox/fac849dc498b60000e200f3f2a2712b54da39b92/index.php.bin" method="post" enctype="multipart/form-data">
 <input type="file" name="file" />
 <input type="submit" value="upload" />
 form>
body>
html>

然后上传我们的bin文件,此时再访问action=shell,来触发index.php加载我们bin文件

 0ctf-ezdoor-复现分析_第13张图片

由上图可以看到此时已经成功加载了我们的bin文件,我们继续读一下/var/www/html和/var/www/html/flag目录,可以看到服务器做了限制,只能读到flag目录有个奇怪的文件,可以读一读它

0ctf-ezdoor-复现分析_第14张图片

 

 接下来读一下这个文件

0ctf-ezdoor-复现分析_第15张图片

将其base64解码以后存到本地的flag.php.bin文件中,拖到010进行分析,发现解析出来其systemid出现了错误,对比一下正常的bin文件发现其头部少了一个字节00

0ctf-ezdoor-复现分析_第16张图片

0ctf-ezdoor-复现分析_第17张图片

所以在其magic头部补充一个字节就行了,此时systemid还原正常,其它字段的值也正常了,接下来就要让bin文件还原成我们可以阅读的代码或者语言,做到这web部分结束,暂时不往下看了==

非预期解: 

1.通过条件竞争

因为pathinfo会获取最后一个点之后的扩展,通过index.php/. 就可以绕过pathinfo,但是move_uploaded_file这个函数在调用stat检测到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); //然后删除目录
}

删除时,先删除目录中的文件,再删除目录,那么我们知道如果目录里面有文件调用rmdir将无法删除,所以我们可以给要删除的目录传递大量没用的文件,那么在还在scandir的for循环结束时,原有的正常的index.php

也被删除了,此时沙盒中有还有无用文件,不能删除此沙盒目录,因此我们可以再上传自己的index.php,然后以此来getshell

2.绕过php底层文件操作函数

x/../index.php/. 直接将路径修改为该路径,就可以覆盖原来的index.php,因为首先php调用tsrm_realpath去掉/.将其转换为一个标准路径,然后调用lstst获取文件属性,也就是判断文件存不存在,不存在将写文件,x/../index.php将绕过lstat让其认为index.php不存在所以重新写入,所以可以getshell

参考:

 https://www.kingkk.com/2018/04/2018-0ctf-ezdoor%E5%88%86%E6%9E%90/#%E5%8F%A6%E4%B8%80%E7%A7%8D%E9%AA%9A%E6%93%8D%E4%BD%9C

https://www.cdxy.me/?p=790

http://pupiles.com/%E7%94%B1%E4%B8%80%E9%81%93ctf%E9%A2%98%E5%BC%95%E5%8F%91%E7%9A%84%E6%80%9D%E8%80%83.html

https://lorexxar.cn/2016/05/27/opcache-jcfx/

http://wonderkun.cc/index.html/?p=626

https://skysec.top/2018/04/11/0ctf-ezdoor/#%E9%A2%84%E6%9C%9F%E8%A7%A3

http://elssm.top/2018/05/04/2018-0ctf-ezdoor%E5%A4%8D%E7%8E%B0/

https://altman.vip/2018/10/10/0ctf-Ezdoor/#%E6%89%A7%E8%A1%8C%E5%91%BD%E4%BB%A4

https://www.angelwhu.com/blog/?p=438

你可能感兴趣的:(0ctf-ezdoor-复现分析)