mikufanpage f12一下,有如下代码:
1 2 3 <div class ="carousel-item active" > <img class ="d-block w-100" src ="/image?path=miku1.jpg" alt ="miku" > </div >
确认文件包含,查看源码:
1 2 3 4 5 6 7 app.get ("/image" , (req, res ) => { if (req.query .path .split ("." )[1 ] === "png" || req.query .path .split ("." )[1 ] === "jpg" ) { res.sendFile (path.resolve ('./img/' + req.query .path )); } else { res.status (403 ).send ('Access Denied' ); } });
以“.“分割路径,检查文件后缀
1 /image?path=miku1.png./ ../flag.txt
后缀添加”.“绕过即可
profile-page xss注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 app.post ("/api/update" , requiresLogin, (req, res ) => { const { bio } = req.body ; if (!bio || typeof bio !== "string" ) { return res.end ("missing bio" ); } if (!req.headers .csrf ) { return res.end ("missing csrf token" ); } if (req.headers .csrf !== req.cookies .csrf ) { return res.end ("invalid csrf token" ); } if (bio.length > 2048 ) { return res.end ("bio too long" ); } req.user .bio = renderBio (bio); res.send (`Bio updated!` ); });
该路由下通过post传参bio可往页面添加内容,为xss注入点,详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const renderBBCode = (data ) => { data = data.replaceAll (/\[b\](.+?)\[\/b\]/g , '<strong>$1</strong>' ); data = data.replaceAll (/\[i\](.+?)\[\/i\]/g , '<i>$1</i>' ); data = data.replaceAll (/\[u\](.+?)\[\/u\]/g , '<u>$1</u>' ); data = data.replaceAll (/\[strike\](.+?)\[\/strike\]/g , '<strike>$1</strike>' ); data = data.replaceAll (/\[color=#([0-9a-f]{6})\](.+?)\[\/color\]/g , '<span style="color: #$1">$2</span>' ); data = data.replaceAll (/\[size=(\d+)\](.+?)\[\/size\]/g , '<span style="font-size: $1px">$2</span>' ); data = data.replaceAll (/\[url=(.+?)\](.+?)\[\/url\]/g , '<a href="$1">$2</a>' ); data = data.replaceAll (/\[img\](.+?)\[\/img\]/g , '<img src="$1" />' ); return data; };const renderBio = (data ) => { const html = renderBBCode (data); const sanitized = purify.sanitize (html); return sanitized.replaceAll ( /\[youtube\](.+?)\[\/youtube\]/g , '<iframe sandbox="allow-scripts" width="640px" height="480px" src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen></iframe>' ); };
由提示也可发现,针对<iframe>
标签进行的过滤较少,闭合双引号后即可外带cookie完成xss
经测试会把+过滤,需要稍微注意下
stream-vs 参考:osu!gaming CTF 2024 报道 - Hamayan Hamayan
贴个现成脚本:
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 from websocket import create_connectionimport jsonfrom decimal import *import time ws = create_connection("wss://stream-vs.web.osugaming.lol/" )def send_and_recv (payload ): ws.send(json.dumps(payload)) return json.loads(ws.recv()) send_and_recv({"type" :"login" ,"data" :"evilman" }) send_and_recv({"type" :"challenge" }) songs = send_and_recv({"type" :"start" })['data' ]['songs' ]for song in songs: start = int (time.time()) end = start + song['duration' ] * 1000 interval = 60000 / song['bpm' ] / 4 clicks = [start] while clicks[-1 ] + interval <= end: clicks.append(clicks[-1 ] + interval) p = {"type" :"results" ,"data" :{"clicks" :clicks,"start" :start, "end" :end}} send_and_recv(p) print (ws.recv()) time.sleep(song['duration' ])
这个脚本的思路是在分析stream-vs.js,理解游戏胜利判断条件后,模仿玩家行为通过正常游玩通关获得flag
不过不一定能一次成功得多试几次
pp-ranking 参考:osu!gaming CTF 2024 Writeups | 廢文集中區 (maple3142.net)
分析代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 app.get ("/rankings" , (req, res ) => { let ranking = [...baseRankings]; if (req.user ) { ranking.push (req.user ); } ranking = ranking .sort ((a,b ) => b.performance - a.performance ) .map ((u, i ) => ({ ...u, rank : `#${i + 1 } ` })); let flag; if (req.user ) { if (ranking[ranking.length - 1 ].username === req.user .username ) { ranking[ranking.length - 1 ].rank = "Last" ; } else if (ranking[0 ].username === req.user .username ) { flag = process.env .FLAG || "osu{test_flag}" ; } } res.render ("rankings" , { ranking, flag }); });
获得第一名时能获得flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 app.post ("/api/submit" , requiresLogin, async (req, res) => { const { osu, osr } = req.body ; try { const [pp, md5] = await calculate (osu, Buffer .from (osr, "base64" )); if (req.user .playedMaps .includes (md5)) { return res.send ("You can only submit a map once." ); } if (anticheat (req.user , pp)) { users.delete (req.user .username ); return res.send ("You have triggered the anticheat! Nice try..." ); } req.user .playCount ++; req.user .performance += pp; req.user .playedMaps .push (md5); return res.redirect ("/rankings" ); } catch (err) { return res.send (err.message ); } });
此段代码要求提供.osu
和.osr
文件作为分数的验证
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 import { StandardRuleset } from 'osu-standard-stable' ;import { BeatmapDecoder , ScoreDecoder } from "osu-parsers" ;import crypto from "crypto" ;const calculate = async (osu, osr ) => { const md5 = crypto.createHash ('md5' ).update (osu).digest ("hex" ); const scoreDecoder = new ScoreDecoder (); const score = await scoreDecoder.decodeFromBuffer (osr); if (md5 !== score.info .beatmapHashMD5 ) { throw new Error ("The beatmap and replay do not match! Did you submit the wrong beatmap?" ); } if (score.info ._rulesetId !== 0 ) { throw new Error ("Sorry, only standard is supported :(" ); } const beatmapDecoder = new BeatmapDecoder (); const beatmap = await beatmapDecoder.decodeFromBuffer (osu); const ruleset = new StandardRuleset (); const mods = ruleset.createModCombination (score.info .rawMods ); const standardBeatmap = ruleset.applyToBeatmapWithMods (beatmap, mods); const difficultyCalculator = ruleset.createDifficultyCalculator (standardBeatmap); const difficultyAttributes = difficultyCalculator.calculate (); const performanceCalculator = ruleset.createPerformanceCalculator (difficultyAttributes, score.info ); const totalPerformance = performanceCalculator.calculate (); return [totalPerformance, md5]; };export default calculate;
通过计算md5的值验证文件是否有效,之后返回作为分数,可以直接修改文件值生成足够分数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const THREE_MONTHS_IN_MS = 3 * 30 * 24 * 60 * 1000 ;const anticheat = (user, newPP ) => { const pp = parseInt (newPP); if (user.playCount < 5000 && pp > 300 ) { return true ; } if (+new Date () - user.registerDate < THREE_MONTHS_IN_MS && pp > 300 ) { return true ; } if (+new Date () - user.registerDate < THREE_MONTHS_IN_MS && pp + user.performance > 5_000 ) { return true ; } if (user.performance < 1000 && pp > 300 ) { return true ; } return false ; };export default anticheat;
反作弊验证,但calculate函数返回的值类型为double,而parseInt会将参数转为字符串再转为整数,在转换科学计数法时会有问题
所以让pp是很大的数字就行,但不能是NaN或者Infinity