Fetch the Flag CTF 2022 writeup: Roadrunner
2022年11月10日
0 分で読めます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.
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.
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.
Then, I turned my attention to the backend source code which is provided in the challenge. Let’s start with the 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.
Here we can see the two endpoints, /
and /run
. Let’s look at /
first which is handled by the welcome
function.
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.
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.
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?
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.
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:
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.