# Postviewer v2 - writeup

## Challenge's overview

> I fixed all the bugs from the last year challenge so it should be secure
> now. Ri1ght?  
>  
> [Attachment](../attachments/bot.js)  
> https://postviewer2-web.2023.ctfcompetition.com

From the UI side, the challenge looked exactly like the previous year's
[Postviewer](https://gist.github.com/terjanq/7c1a71b83db5e02253c218765f96a710#challenges-
overview).

The core change is that:  
1\. There is no `querySelector` injection anymore but rather the file gets
rendered via `document.getElementById`  
2\. The file is transferred to a new shim via
`iframe.contentWindow?.postMessage({ body, mimeType, sandbox}, url.origin)`,
where:  
1\. `sandbox` is set to `['allow-scripts']`  
2\. `url.origin` is a random origin generated on the client-side  
3\. A new [shim](../challenge/src/sandbox/shim.html) page, that is used to
render the file, is hosted on
`sbx-<random>.postviewer2-web.2023.ctfcompetition.com/shim.html` origin with
`Content-Security-Policy` set to `frame-src: blob:`  
4\. The main page has `Content-Security-Policy` set to `frame-ancestors
*.postviewer2-web.2023.ctfcompetition.com; frame-src
*.postviewer2-web.2023.ctfcompetition.com;`  
5\. [bot.js](../attachments/bot.js) ensures that no popups can be opened and
additionally enables [Strict Origin
Isolation](https://www.maketecheasier.com/enable-chrome-strict-site-
isolation/).

The idea for the challenge was to leak admin's flag.txt file.

## One vulnerability  
Even though the exploit is rather complex, the challenge had only one
vulnerability! The vulnerability lies in
[shim.html](../challenge/src/sandbox/shim.html).  
Players could notice that `allow-same-origin` is strictly forbidden but the
check can be easily bypassed.

```js  
const forbidden_sbx = /allow-same-origin/ig;  
...  
for(const value of e.data.sandbox){  
if(forbidden_sbx.test(value) || !iframe.sandbox.supports(value)){  
console.error(`Unsupported value: ${value}`);  
continue;  
}  
iframe.sandbox.add(value);  
}  
```

It's not widely known, but a regular expression with a global flag cannot be
used indefinitely. It's due to the behavior that after a first successful
match, the `lastIndex` will increase and consecutive searches will yield no
matches.

To bypass the check, players could simply send `sandbox: ['allow-same-origin',
'allow-same-origin', 'allow-scripts']` and hence achieve XSS on any
`sbx-*.postviewer2-web.2023.ctfcompetition.com`. However, the flag is rendered
on a random origin, so it's not enough to utilize it yet.

## Exploitation idea  
The idea of the exploitation is rather straightforward.

1\. Calculate the ID of `flag.txt` and open
`https://postviewer2-web.2023.ctfcompetition.com/#file-87ebbc317d687eeff47403603cc6dfb9b7d6c817`.  
2\. Leak the origin of the shim that loaded `flag.txt` in a sandboxed iframe.  
3\. Embed `<leaked_origin>/shim.html` and execute XSS there.  
4\. From the embedded XSS, access the shim iframe (shim doesn't have sandbox
attributes, only an inner frame does), leak `blob:` URL of the flag, and fetch
it.

### First obstacle - frame-ancestors

The first obstacle was that the main page could only be embedded by
`*.postviewer2-web.2023.ctfcompetition.com` ancestors. This means that the
page could not be embedded on a player's website. The player was supposed to
embed it on `sbx-anything.postviewer2-web.2023.ctfcompetition.com`.

### Second obstacle - frame-src

But then comes the second obstacle - `CSP: frame-src blob:`. Because of the
directive, players couldn't easily embed the main page on `sbx-anything`
origin. Additionally, the server was supposed to respond with a strict CSP for
any other page, e.g. `sbx-
anything.postviewer2-web.2023.ctfcompetition.com/not-found` would result in
`Content-Security-Policy: default-src 'none'`.

## Exploitation

### No-CSP subpage

Someone that follows my research could find a usefull CSP bypass in one of the
[blogposts](https://terjanq.medium.com/arbitrary-parentheses-less-
xss-e4a1cf37c13d). The idea is to find a subpage without a CSP and execute the
payload there. A trick that usually works is to open a page with a very long
url that will be blocked on the intermediate proxy side because of the
overlong headers. Embedding `sbx-
anything.postviewer2-web.2023.ctfcompetition.com/AAAAA....AAA` would work fine
for a long sequence of A's.

### Top-redirection  
Because of the blocked popups, a player had to find a way to redirect the top
window. These redirections are usually blocked because of the [frame
busting](https://chromestatus.com/feature/5851021045661696) protection. There
are a couple ways where the top window could allow the iframe to redirect
itself.

1\. The most popular way among the players was to use `allow-top-navigation`
sandbox flag which makes the iframe be able to redirect the top window. It
strangely works as an `allow` policy, but this as well could be a Chrome bug
because of the
[comment](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/local_frame.cc;l=1861)
that says that the _ancestor chain_ should be respected.

> // Top navigation is forbidden in sandboxed frames unless opted-in, and only  
> // then **if the ancestor chain allowed to navigate the top frame**.  
> // Note: We don't check root fenced frames for kTop* flags since the kTop*  
> // flags imply the actual top-level page.

2\. I've become aware of the sandbox technique before the CTF started but the
intended way was a bit harder. A window can redirect another window if they
are in `openee-opener` relationship. The trick is to call `open('URL',
'iframe')` that will create such a relationship with an iframe named `iframe`
(e.g. `<iframe name='iframe'>`). This allows the iframe to redirect its
`opener` without user-interaction.

### Creating a self-containing exploit

1\. The attacker sends the admin to `attacker.com`.  
2\. In there, the attacker embeds two iframes:  
1\. `sbx-anything.postviewer2-web.2023.ctfcompetition.com/shim.html` used to
get XSS  
2\. `sbx-anything.postviewer2-web.2023.ctfcompetition.com/AAAA...AAA` used to
execute the XSS on a CSP-less subpage and redirect the top-window, let's call
it `redirector`  
3\. The attacker creates a new `blob` document from the `redirector` iframe
that contains a self-containg exploit used to leak the flag and then redirects
top window to it.  
4\. The self-containing exploit executes the following steps:  
1\. Embed the flag on the main page via
`https://postviewer2-web.2023.ctfcompetition.com/#file-87ebbc317d687eeff47403603cc6dfb9b7d6c817`.  
2\. Redirect the most inner iframe to `about:blank` (or some other blob) and
leak shim's origin via `location.ancestorOrigins` (redirection can be easily
done by calling `top[0][0][0].location = 'about:blank'`).  
3\. Spawn a new shim iframe with the leaked origin and set sandbox to `allow-
same-origin allow-scripts`.  
4\. Execute XSS in that iframe and leak the `blob:` of the flag via
`top[0][0].document.querySelector('iframe').src`.  
5\. Fetch the flag from the same iframe (as simple as
`fetch(leaked_blob_url)`).

A full exploit with comments can be found [here](../solution/solve.html).

**CTF{who_needs_popups_when_you_can_simply_have_it_all}**

## Closing thoughts

The idea behind the challenge comes from a real bypass that I found
internally, if we were to allow the `allow-same-origin` sandbox flag in our
framing solutions. We've
[blogged](https://security.googleblog.com/2023/04/securely-hosting-user-data-
in-modern.html) about our approach and this challenge demonstrates how
difficult it is to achieve a proper isolation of user content by utilising
currently available Web Platform features.

Original writeup (https://github.com/google/google-
ctf/blob/main/2023/quals/web-postviewer2/solution/README.md).