[EN] Intigriti's XSS challenge - 0122
January 16, 2022
Challenge rules
On the 10th January 2022 Intigriti launched a new XSS challenge, created by @TheRealBrenu. The goal is to find a way to execute arbitrary javascript on the https://challenge-0122-challenge.intigriti.io page.
As we can see on the image above, the solution should comply with the following rules:
- work on the latest version of Chrome and FireFox,
- execute alert(document.domain),
- leverage a cross site scripting vulnerability on https://challenge-0122.intigriti.io/ or the domain of the challenge page,
- not be self-XSS or related to MiTM attacks.
Let the fun begin! đ
Black-box testing
Letâs start with the most important thing - understanding what the application is actually doing. The challenge page says to test our payloads on https://challenge-0122-challenge.intigriti.io/ so letâs go there.
If we insert a simple xss payload <img src=1 onerror=alert(1)>
into the textarea and send it, we can observe at least two things:
alert(1)
was not fired,- we were redirected to the
/result?payload=<img%20src%3D1%20onerror%3Dalert(1)>
page with our payload inpayload
query parameter.
Therefore, we can put our payloads directly in the payload
query parameter on /result
path so we donât need to enter it into the textarea on the root page. This will definitely help us at the end with making the xss completely non-interactive.
Now, letâs inspect the currently renedered element (right click on that element and then Inspect) and see why our alert(1)
was not called.
As we can see on the picture above, the <img>
tag was sanitized and onerror=alert(1)
was removed. Therefore, we know that âsomethingâ is cleaning our payload by removing dangerous HTML.
Understanding the code
Now letâs go to the Sources panel in DevTools and look at the real source-code.
We can see that the webpage loads main.02a05519.js
but this file is minified so itâs really hard to read and understand.
If we look closer we will notice that there are also other files in the /static/
directory. It looks like we have access to non-minified version of the main.02a05519.js
file! The index.js
tells us this is a React app.
App.js
file renders <Router>
element from router.js
which defines two routes:
- index with path: â/â
- result with path: â/resultâ
import { BrowserRouter, Routes, Route } from "react-router-dom"
import I0x1C from "./pages/I0x1C"
import I0x1 from "./pages/I0x1"
const identifiers = {
I0x1: "UmVzdWx0",
I0x2: "cGF5bG9hZEZyb21Vcmw=",
I0x3: "cXVlcnlSZXN1bHQ=",
I0x4: "bG9jYXRpb24=",
I0x5: "c2VhcmNo",
I0x6: "Z2V0",
I0x7: "cGF5bG9hZA==",
//...[snip]...
}
export default function Router() {
return (
<BrowserRouter>
<Routes>
<Route path="/">
<Route index element={<I0x1C identifiers={identifiers} />} />
<Route path="result" element={<I0x1 identifiers={identifiers} />} />
</Route>
</Routes>
</BrowserRouter>
)
}
Each route renders a different React element, the âindexâ route renders <I0x1C>
and the âresultâ route <I0x1>
. There is also a big object called identifiers
containing key-value pair data with values endcoded in base64 format. This object is passed as a identifiers
property to both of these elements. Please, remember this object, we will need it later.
According to the analysis in the previous section, we know that our payload is rendered on /result
path. The âresultâ route renders <I0x1>
element so letâs go straight to the code of it in /pages/I0x1/index.js
:
import { useState } from "react"
import DOMPurify from "dompurify"
import "../../App.css"
function I0x1({ identifiers }) {
const [I0x2, _] = useState(() => {
const I0x3 = new URLSearchParams(
window[window.atob(identifiers["I0x4"])][window.atob(identifiers["I0x5"])]
)[window.atob(identifiers["I0x6"])](window.atob(identifiers["I0x7"]))
if (I0x3) {
const I0x8 = {}
I0x8[window.atob(identifiers["I0x9"])] = I0x3
return I0x8
}
const I0x8 = {}
I0x8[window.atob(identifiers["I0x9"])] = window.atob(identifiers["I0xA"])
return I0x8
})
function I0xB(I0xC) {
for (const I0xD of I0xC[window.atob(identifiers["I0xE"])]) {
if (
window.atob(identifiers["I0x11"]) in
I0xD[window.atob(identifiers["I0xF"])]
) {
new Function(
I0xD[window.atob(identifiers["I0x10"])](
window.atob(identifiers["I0x11"])
)
)()
}
I0xB(I0xD)
}
}
function I0x12(I0x13) {
I0x13[window.atob(identifiers["I0x9"])] = DOMPurify[
window.atob(identifiers["I0x15"])
](I0x13[window.atob(identifiers["I0x9"])])
let I0x14 = document[window.atob(identifiers["I0x16"])](
window.atob(identifiers["I0x14"])
)
I0x14[window.atob(identifiers["I0x17"])] =
I0x13[window.atob(identifiers["I0x9"])]
document[window.atob(identifiers["I0x32"])][
window.atob(identifiers["I0x18"])
](I0x14)
I0x14 = document[window.atob(identifiers["I0x19"])](
window.atob(identifiers["I0x14"])
)[0]
I0xB(I0x14[window.atob(identifiers["I0x1A"])])
document[window.atob(identifiers["I0x32"])][
window.atob(identifiers["I0x1B"])
](I0x14)
return I0x13
}
return (
<div className="App">
<h1>Here is the result!</h1>
<div id="viewer-container" dangerouslySetInnerHTML={I0x12(I0x2)}></div>
</div>
)
}
export default I0x1
Unfortunately, it is not easily readable so we have to deobfuscate it to be able to understand the code. If we look closer we will notice an interesting pattern: window.atob(identifiers["I0x.."])
is used everywhere in this code.
window.atob
is a function which simply decodes a string of data which has been encoded using Base64 encoding. As we mentioned earlier in this section, the identifiers
object is defined in the router.js
file and itâs passed as a property to the I0x1
element.
Now letâs open developer console and try to read the real values in this part of the code. First, copy the indentifiers
object from router.js
and paste it to the developer console - we need it to be in the scope because we will use it in window.atob(identifiers["I0x.."])
in a few moments. Now copy e.g. window.atob(identifiers["I0x4"])
or any similar block:
As you can see on the image above, window.atob(identifiers["I0x4"])
== "location"
, window.atob(identifiers["I0x9"])
== "__html"
.
Beautifying the code
We could manually replace all of the mentioned blocks in <I0x1>
element. Instead, letâs create a script to do this for us automatically:
const identifiers = {
I0x1: "UmVzdWx0",
I0x2: "cGF5bG9hZEZyb21Vcmw=",
I0x3: "cXVlcnlSZXN1bHQ=",
I0x4: "bG9jYXRpb24=",
I0x5: "c2VhcmNo",
I0x6: "Z2V0",
I0x7: "cGF5bG9hZA==",
I0x8: "cmVzdWx0",
I0x9: "X19odG1s",
I0xA: "PGgxIHN0eWxlPSdjb2xvcjogIzAwYmZhNSc+Tm90aGluZyBoZXJlITwvaDE+",
I0xB: "aGFuZGxlQXR0cmlidXRlcw==",
I0xC: "ZWxlbWVudA==",
I0xD: "Y2hpbGQ=",
I0xE: "Y2hpbGRyZW4=",
I0xF: "YXR0cmlidXRlcw==",
I0x10: "Z2V0QXR0cmlidXRl",
I0x11: "ZGF0YS1kZWJ1Zw==",
I0x12: "c2FuaXRpemVIVE1M",
I0x13: "aHRtbE9iag==",
I0x14: "dGVtcGxhdGU=",
I0x15: "c2FuaXRpemU=",
I0x16: "Y3JlYXRlRWxlbWVudA==",
I0x17: "aW5uZXJIVE1M",
I0x18: "YXBwZW5kQ2hpbGQ=",
I0x19: "Z2V0RWxlbWVudHNCeVRhZ05hbWU=",
I0x1A: "Y29udGVudA==",
I0x1B: "cmVtb3ZlQ2hpbGQ=",
I0x1C: "SG9tZQ==",
I0x1D: "c2V0UGF5bG9hZA==",
I0x1E: "ZWRpdG9yUmVm",
I0x1F: "bmF2aWdhdGU=",
I0x20: "aGFuZGxlU3VibWl0",
I0x21: "ZXZlbnQ=",
I0x22: "cHJldmVudERlZmF1bHQ=",
I0x23: "L3Jlc3VsdD9wYXlsb2FkPQ==",
I0x24: "dmFsdWU=",
I0x25: "a2V5",
I0x26: "VGFi",
I0x27: "c2hpZnRLZXk=",
I0x28: "c2V0UmFuZ2VUZXh0",
I0x29: "ICAgIA==",
I0x2A: "c2VsZWN0aW9uU3RhcnQ=",
I0x2B: "ZW5k",
I0x2C: "bGluZVN0YXJ0",
I0x2D: "c3RhcnQ=",
I0x2E: "bGVuZ3Ro",
I0x2F: "c2xpY2U=",
I0x30: "c2V0U2VsZWN0aW9uUmFuZ2U=",
I0x31: "Cg==",
I0x32: "Ym9keQ==",
I0x33: "dGFyZ2V0",
I0x34: "Y3VycmVudA==",
}
let str = `
import { useState } from "react";
import DOMPurify from "dompurify";
import "../../App.css";
function I0x1({ identifiers }) {
const [I0x2, _] = useState(() => {
const I0x3 = new URLSearchParams(
window[window.atob(identifiers["I0x4"])][window.atob(identifiers["I0x5"])]
)[window.atob(identifiers["I0x6"])](window.atob(identifiers["I0x7"]));
if (I0x3) {
const I0x8 = {};
I0x8[window.atob(identifiers["I0x9"])] = I0x3;
return I0x8;
}
const I0x8 = {};
I0x8[window.atob(identifiers["I0x9"])] = window.atob(identifiers["I0xA"]);
return I0x8;
});
function I0xB(I0xC) {
for (const I0xD of I0xC[window.atob(identifiers["I0xE"])]) {
if (
window.atob(identifiers["I0x11"]) in
I0xD[window.atob(identifiers["I0xF"])]
) {
new Function(
I0xD[window.atob(identifiers["I0x10"])](
window.atob(identifiers["I0x11"])
)
)();
}
I0xB(I0xD);
}
}
function I0x12(I0x13) {
I0x13[window.atob(identifiers["I0x9"])] = DOMPurify[
window.atob(identifiers["I0x15"])
](I0x13[window.atob(identifiers["I0x9"])]);
let I0x14 = document[window.atob(identifiers["I0x16"])](
window.atob(identifiers["I0x14"])
);
I0x14[window.atob(identifiers["I0x17"])] =
I0x13[window.atob(identifiers["I0x9"])];
document[window.atob(identifiers["I0x32"])][
window.atob(identifiers["I0x18"])
](I0x14);
I0x14 = document[window.atob(identifiers["I0x19"])](
window.atob(identifiers["I0x14"])
)[0];
I0xB(I0x14[window.atob(identifiers["I0x1A"])]);
document[window.atob(identifiers["I0x32"])][
window.atob(identifiers["I0x1B"])
](I0x14);
return I0x13;
}
return (
<div className="App">
<h1>Here is the result!</h1>
<div id="viewer-container" dangerouslySetInnerHTML={I0x12(I0x2)}></div>
</div>
);
}
export default I0x1;
`
const re = /window.atob\(identifiers\["(I0x[0-9A-F]+)"\]\)/gstr = str.replace(re, function (match, group) { return `"${window.atob(identifiers[group])}"`})
console.log(str)
As you can see, the variable str
contains the js code of <I0x1>
element, the variable re
is a regex that matches all occurrences such as: window.atob(identifiers["I0x4"])
, window.atob(identifiers["I0x14"])
etc.
If you copy the above script and paste it into developer console, it will return a much cleaner code:
import { useState } from "react"
import DOMPurify from "dompurify"
import "../../App.css"
function I0x1({ identifiers }) {
const [I0x2, _] = useState(() => {
const I0x3 = new URLSearchParams(window["location"]["search"])["get"](
"payload"
)
if (I0x3) {
const I0x8 = {}
I0x8["__html"] = I0x3
return I0x8
}
const I0x8 = {}
I0x8["__html"] = "<h1 style='color: #00bfa5'>Nothing here!</h1>"
return I0x8
})
function I0xB(I0xC) {
for (const I0xD of I0xC["children"]) {
if ("data-debug" in I0xD["attributes"]) {
new Function(I0xD["getAttribute"]("data-debug"))()
}
I0xB(I0xD)
}
}
function I0x12(I0x13) {
I0x13["__html"] = DOMPurify["sanitize"](I0x13["__html"])
let I0x14 = document["createElement"]("template")
I0x14["innerHTML"] = I0x13["__html"]
document["body"]["appendChild"](I0x14)
I0x14 = document["getElementsByTagName"]("template")[0]
I0xB(I0x14["content"])
document["body"]["removeChild"](I0x14)
return I0x13
}
return (
<div className="App">
<h1>Here is the result!</h1>
<div id="viewer-container" dangerouslySetInnerHTML={I0x12(I0x2)}></div>
</div>
)
}
export default I0x1
Now letâs rename some variables to make the code even more readable. The final code is as follows:
import { useState } from "react"
import DOMPurify from "dompurify"
import "../../App.css"
function I0x1({ identifiers }) {
const [state, _] = useState(() => {
const payloadParam = new URLSearchParams(window["location"]["search"])[
"get"
]("payload")
if (payloadParam) {
const state = {}
state["__html"] = payloadParam
return state
}
const state = {}
state["__html"] = "<h1 style='color: #00bfa5'>Nothing here!</h1>"
return state
})
function debug(elem) {
for (const child of elem["children"]) {
if ("data-debug" in child["attributes"]) {
new Function(child["getAttribute"]("data-debug"))()
}
debug(child)
}
}
function getSecureHtml(state) {
state["__html"] = DOMPurify["sanitize"](state["__html"])
let template = document["createElement"]("template")
template["innerHTML"] = state["__html"]
document["body"]["appendChild"](template)
template = document["getElementsByTagName"]("template")[0]
debug(template["content"])
document["body"]["removeChild"](template)
return state
}
return (
<div className="App">
<h1>Here is the result!</h1>
<div
id="viewer-container"
dangerouslySetInnerHTML={getSecureHtml(state)}
></div>
</div>
)
}
export default I0x1
Ok, letâs explain what the code is actually doing. The I0x1
function is a React functional component. During initialization it sets up a state (lines 6 - 22
-> useState
method). In a nutshell, this function gets payload
query parameter from url and saves it into the state["__html"]
object which is accessible in the entire component. Do you remember our first observartion when we noticed that the payload query parameter is used to create html? This is the code responsible for that part!
Then, letâs look at the bottom of the I0x1
function, it returns some html (lines 50 - 56
). It is called jsx, and it is a syntax extension to JavaScript. If we look closer we will notice it uses dangerouslySetInnerHTML
property which as it name suggests, is responsible for setting innerHTML
without any kind of santization. This function takes the value returned by getSecureHtml
function that firstly sanitizes provided state using DOMPurify library and secondly creates a template html element and inserts the sanitized html into it. Our first thought would be:
If there is a vulnerability in DOMPurify and we provide a particular xss payload, it will bypass the sanitizer and execute our xss
However, DOMPurify is a well-tested library so there is a low probability to find a vulnerability there. Letâs leave that idea alone for now and read the rest of the getSecureHtml
function.
function getSecureHtml(state) {
state["__html"] = DOMPurify["sanitize"](state["__html"])
let template = document["createElement"]("template")
template["innerHTML"] = state["__html"]
document["body"]["appendChild"](template)
template = document["getElementsByTagName"]("template")[0]
debug(template["content"])
document["body"]["removeChild"](template)
return state
}
In line 9, debug
function is called with the content of template
element so basically with the payload passed in the payload
query parameter. Letâs look at that function:
function debug(elem) {
for (const child of elem["children"]) {
if ("data-debug" in child["attributes"]) {
new Function(child["getAttribute"]("data-debug"))()
}
debug(child)
}
}
As you can see, this function goes through all children of the provided argument and if any child contains data-debug
attribute, takes the value of it, creates a new Function
object and runs it! Therefore anything passed in data-debug
attribute could be run and because data-debug
isnât dangerous html attribute, DOMPurify
wonât remove it.
Aha! So there is the vulnerability!
Executing alert
The last thing to do is to just create a html element containg data-debug
with a value we want to run, eg. alert(document.domain)
. Therefore, the final payload is as follows: <a data-debug="alert(document.domain)">XSS</a>
.
Now we have to pass it to the url we previously discovered and test it: https://challenge-0122-challenge.intigriti.io/result?payload=<a data-debug="alert(document.domain)">XSS</a>
Great, our payload works as intended, alert(document.domain)
was executed!!!
So we donât need to look for a vulnerability in DOMPurify library đ