Hey again (Inti)griti, hope y'all are doing well. Thanks for the challenge as usual.
The main vulnerability is a very limited XSS on line 41, whereby arbitrary data from the r
URL inserted is appended into the DOM as such ({url}
being the injection point).
If you're not being redirected, click <a href=${url}>here</a>
However, it is limited by two checks in place.
- Every single property in both
window
anddocument
object is checked for the keywordjavascript
. If found, the property is deleted entirely, leaving itundefined
(and possibly causing runtime errors). [line 5-11] - The
url
variable is not allowed to contain<
,>
,"
,'
,(space)
, via a regex check on line 26.
Despite not being able to escape from the <a>
tag, it still allowed attribute injection inside the <a>
tag. To ensure highest possibility of the exploit triggering, we'll make use of the onmouseover
event, as well as tweaking the CSS style of the element so it covers the entire page. Following is the beautified version of the styles we'll be injecting.
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
Next, we cannot create separate attributes in the element the "normal" way because spaces and quotes are not allowed. Despite so, HTML syntax is very lax and attributes do not necessarily need to be in quotes, and we can separate them with newlines as well, or %0a
URL-encoded.
Since this time we're supposed to alert
the flag instead of document.domain
, the value of the alert
function should be document.getElementById('flag').innerText
, but we have to get around the quotation mark for the JS string flag
. One of the common method is to use a chain of String.fromCharCode
concatenated, but for a simple string as such, I prefer to use the regex-to-string trick /flag/.source
, which I've seen posted on Twitter from some time ago.
Lastly, as this element is supposed to be shown a second later only if the initial attempt to redirect via window.location
fails, we have to make it fail. This can be done by attempting to connect to an HTTPS "website" that never exists on a random IP, in this case I'll use https://1.2.3.4/
.
Final payload:
https://challenge-0121.intigriti.io/?r=https://1.2.3.4/%0aonmouseover=alert(document.getElementById(/flag/.source).innerText)%0astyle=position:fixed;left:0;top:0;width:100%;height:100%
which results in (cleaned up by Chrome)...
<a href="https://1.2.3.4/" onmouseover="alert(document.getElementById(/flag/.source).innerText)" style="position:fixed;left:0;top:0;width:100%;height:100%">here</a>
Payload can be triggered by waiting 6 seconds and then moving the cursor in the page.
It is possible to achieve this with DOM clobbering combined with the fact that the domain has a wildcard DNS record (*.challenge-0121.intigriti.io
leads to the same place).
The exploitable snippet in question is line 32, window.location = window.origin + "/" + url;
. If the original window.origin
is undefined, an element with a matching id
will take its place as a global variable. We can abuse the protection mechanism which deletes any variable containing the javascript
keyword and wipe out the original window.origin
. Since this particular challenge subdomain supports wildcard, using the subdomain javascript.challenge-0121.intigriti.io
will lead to the removal of window.origin
.
Before I knew about this subdomain configuration, I researched on window.origin
and found it has some interesting quirks, which would return the parent window's domain if window.origin
is called in an iframed page, but only if the iframed page is about:blank
. I attempted messing with this quirk for a while but failed before discovering the actual solution.
As for assigning a new value for window.origin
, we can just have the previous <a>
element to have an id=origin
attribute. window.origin
would now return the element itself, but when concatenated with other strings, it will pass through .toString()
and return the href
value.
window.origin
now has the same value as url
, so when concatenate to form the new window.location
, we have to negate the repeated JavaScript URL by suffixing our payload with //
, commenting out the second half.
Lastly, we can't have javascript
in our payload as it'll tick off the protection mechanism as well. HTML attribute values can be encoded as HTML entities with ampersand, one of the syntax is �
where 00 is any ASCII character's decimal value. t
in javascript
is ASCII decimal 116 so I'll replace it with t
, or %26%23116%3B
after URL encoding.
Final payload:
https://javascript.challenge-0121.intigriti.io/?r=javascrip%26%23116%3B%3Aalert(flag.innerText)%2F%0Aid%3Dorigin
Wait 5 seconds, and a redirection will be attempted for javascript:alert(flag.innerText)//javascript:alert(flag.innerText)/
.