Skip to main content

Fetch the Flag CTF 2022 writeup: Roadrunner

Written by:
Giovanni Funchal
wordpress-sync/feature-ctf-roadrunner

November 10, 2022

0 mins read

Thanks for playing Fetch with us! Congrats to the thousands of players who joined us for Fetch the Flag CTF. And a huge thanks to the Snykers that built, tested, and wrote up the challenges!

If you like escape rooms, you’re going to love the Roadrunner challenge from Snyk’s 2022 Fetch the Flag competition! In this blog post, I will explain how I approached and solved the challenge by exploiting an input validation vulnerability in the code that allows us to escape a sandbox.

Challenge

Can you outrun a roadrunner? No way José!

This challenge starts by taunting us. Will we find the flag?

First, we are given access to a webpage, where we find something similar to an online code playground. Online playgrounds like this are commonly used for learning or experimenting without having to install a language toolchain. The Go language even has an official one.

We can type some Go code, and click Run. The code is sent to a backend, which runs and returns the result, and then the webpage displays it.

We are also given the Go source code of the backend service running that webpage (roadrunner.go), as well as the Dockerfile used to package this service.

Our first step is to review the hints that we were given and do some poking around.

Walkthrough

Poking around

Curiosity is generally a good skill to have when solving these challenges, so the first thing I did when I started was to have a look around and try a few things. I am not a Go programmer, so I searched online for a “hello world”, pasted it in the webpage, clicked Run, and got a “hello world” back. So far so good.

wordpress-sync/blog-roadrunner-hello

This “hello world” example is using the fmt library to print a message in the output.

Then, I inspected the webpage’s source using my browser. I noticed that the Run button was tied to a submit action in a form.

wordpress-sync/blog-roadrunner-run

I then looked at the JavaScript handler, which is sending a POST request to the /run endpoint in the backend and then setting the contents of the result box from the response, as expected.

wordpress-sync/blog-roadrunner-js

Then, I turned my attention to the backend source code which is provided in the challenge. Let’s start with the Dockerfile:

wordpress-sync/blog-roadrunner-dockerfile

We found the flag! We can see that the deployed image contains a flag.txt file at the root directory. Now we just have to capture it. We need to find a way to read the contents of this file.

Finding a lead

Let’s look at roadrunner.go. The main function is always a good place to start.

wordpress-sync/blog-roadrunner-main

Here we can see the two endpoints, / and /run. Let’s look at / first which is handled by the welcome function.

wordpress-sync/blog-roadrunner-welcome

The code here is using the Go html/template library to generate the HTML of the webpage by reading the index.html file and interpolating attributes. Interesting.

Now let’s look at the /run endpoint, which is handled by the runner function.

wordpress-sync/blog-roadrunner-runner

There are several interesting things going on here. The code first creates a sandbox, takes the input script from the request, and then writes it to a file in the sandbox. Next, there is a script sanitization step, which is attempting to check that the script is safe to run and may reject the input. If the input is accepted, the script is then run and the result is captured and returned in the response.

When I looked at this, at first I noticed that the dirname field in the Sandbox structure is also decoded from the request JSON and therefore user-controllable. This field is then used to create a path for writing a file, which is a vulnerability. However, I wasn’t sure how to exploit that, so I decided to refocus on the sanitization step. Let’s look at sanitizeScript more closely.

wordpress-sync/blog-roadrunner-sanitizescript

This code is parsing the input script and then looking at the imports. There is a blocklist containing the most common Go libraries for doing file manipulation. If our input script imports any of those libraries, it will be rejected with a funny message.

Remember our goal is to read the contents of the /flag.txt file.

My first thought here was that this list might not be exhaustive, or there might be other ways to read files. As I mentioned earlier, I am not a Go programmer, so I searched online for a few minutes.

The realization

Do you remember having seen some other place earlier where we read files?

wordpress-sync/blog-roadrunner-welcome

Yes, it’s the welcome function again. It is clearly reading a file, and it is using the template library which is not in the forbidden blocklist of sanitizeScript. Maybe we can use it?

So, I had a look at the documentation for this library. After creating a Template object, we execute the template which writes the result to the writer.

wordpress-sync/blog-roadrunner-template

We need to adapt the io.Writer interface into a string, which we can then print. With some more searching online, I found that I could use strings.Builder (note strings library is also not in the blocklist).

So, our final solution looks like this:

wordpress-sync/blog-roadrunner-solution

Running this snippet dumps the entire contents of the flag.txt file into the output. We did it!

Wrapping up Roadrunner

So, what have we learned? We used carefully constructed tainted input to exploit an input validation vulnerability in an online code playground. We escaped the sandbox due to a flaw in the script sanitization. Some knowledge of the internal workings of the application helped us, as we could go from idea to exploit very quickly. This was simply a CTF challenge. In a real-world scenario, constructing this type of exploit may need a little trial and error with guesswork.

Snyk Code — a free static applications security testing (SAST) tool — can help find and fix input validation vulnerabilities in your code.

Other techniques such as layered security can also make this exploit harder to achieve. For example, the runScript function could be changed to execute the script as a different user with reduced permissions. In this case, the vulnerability would not have been exploitable on its own, unless combined with a second vulnerability — for example privilege escalation.

Secure sandboxed execution of untrusted code is notoriously hard. Sandboxes are intended to prevent applications from gaining access to other resources and data. In practice, it is best to avoid running untrusted code completely, if at all possible.

That’s all folks. I hope you enjoyed! Want to learn how we found all the other flags? Check out our Fetch the Flag solutions page to see how we did it.