I removed the flag :P
Category: web
Solver: aes, Liekedaeler, lukasrad02
Flag: GPNCTF{1_L0V3_L3G4CY_F34TUR3S}
Writeup
This challenge — like a few other web challenges in this CTF — is a nodeJS- and express-based web application. It has four routes that we should examine further.
First off, there are the /
and /removeFlag.js
HTTP GET routes. These only serve static strings but their responses will become important later.
There also is an admin bot that can be triggered via the /admin
POST route. We can provide an HTML string that is passed into a form field in the home page’s HTML along with the flag in another field. When these two values have been entered, the admin bot’s browser is redirected to the /chal
page we will look at later. After the redirect to the page, the browser waits five seconds and then waits for the successful execution of a small JavaScript snippet. Afterwards, it takes a screenshot and returns it to us.
So, let’s take a look at the /chal
HTTP POST route. This route takes both the flag and HTML payload that are given by the admin bot and displays them in the returned HTML. Thus, we should in theory be able to see the flag in the screenshot returned by the admin bot, shouldn’t we? Well, we don’t and that is because the returned HTML includes the removeFlag.js
script that removes the flag. In addition, DOMPurify is called on the HTML code we submit before it is included in the response.
This script is the core of the challenge, so we should take a closer look at it (reformatted a bit here for better legibility):
try {
let els = document.body.querySelectorAll('.flag');
if (els.length !== 1) throw "nope";
els[0].remove();
} catch(e) {
location = 'https://duckduckgo.com/?q=no+cheating+allowed&iax=images&ia=images'
}
We see that the whole code is wrapped in a try-catch-Block that, in case of an error, redirects to a completely different page. Thus, we cannot simply make this code crash to prevent the flag from being deleted. So instead, our options would be to prevent the execution of the script completely or let the script run but somehow prevent it from removing the flag.
We first tried blocking script execution. One of our ideas was to load large resources or otherwise slow down the rendering of the page so that at the time of the screenshot, the script could not run yet. This approach did, however, not lead to anything. The content security policy prevents us from loading anything from anywhere and ultimately, the test for script execution that is run in the admin bot before taking the screenshot would have likely still ensured that the script would be run before the screenshot is taken.
Thus, we took a look at the second approach: Running the script but ensuring that it does not remove the flag. To do this, let’s examine what the script does. At first, it queries all DOM elements from the document body that have the flag
class. Afterwards, it checks that it found exactly one match and then removes the respective element. Thus, simply adding another DOM element with the flag class does not help and rather results in a redirect to the error page. What is interesting, however, is the fact that the querySelectorAll
call is performed on the document.body
object. There is an attack class that allows overriding these attributes of the window object: DOM Clobbering.
In this attack class, one creates HTML elements, typically forms, that overwrite properties of the JavaScript document
object. This allows us to replace the document.body
object which would usually be the <body>
tag of the HTML document with a different DOM element.
When querySelectorAll
is queried on that, we receive only DOM elements inside that DOM element, anything else in the actual document body remains untouched. Thus, we can simply place another element with a flag
class inside that is then removed instead of the actual flag.
But there is one issue that remains: DOMPurify is of course aware of such attacks and prevents naming DOM elements in ways that could impact normal functionality of a web application. Thus, naming a form body
is sanitized. There is, however, one more critical mistake in the server code: After the sanitization, the string <body>
is removed. Thus, we can simply name our form element body<body>
. This is not caught by DOMPurify but then turns into body
after the additional string replacement.
The complete payload now looks like this:
<form name="<body>body"><a class="flag"></form>
The flag DOM element remains intact and we receive the screenshot of the flag from the server.