We’ve hosted an internal Security Capture the Flag (CTF) event for four years in a row now, with each year getting better than the last!



The event

Previously, we were only open to Tumblr employees. This year we decided to extend an invite out to the other teams housed under our parent company, Oath.

All participants had a three hour window to hack, a buffet of tacos, beer, and wine to dive into, and a stack of prizes for the top four players (see Prizes below for details)!

Challenges were available Jeopardy-style, broken down by category. We had eight fun categories to select from:

We also sprinkled a few “inside joke” Easter eggs around the system that awarded bonus points to anyone that discovered them! For example, if they attempted to find a hole in the CTF system itself and navigated to /wp-admin , we’d give them a flag on a prank WordPress page; or perhaps testing to find XSS with a <marquee> tag — only the greatest of all XSS tags!

While the Security Team walked around and helped out, we also setup a mini lockpick village just because.

Solving challenges & scoring points

To complete a challenge, the player had to achieve the goal within one of the listed categories.

In XSS challenges, the player would need to cause the browser to create an alert dialog (e.g. alert() ).

Conversely, in SQL Injection challenges the player would need to read the flag column from the flags table in that challenge’s database.

When the player successfully solved the challenge they were awarded with a flag, each in the format of Tumblr{s0mE_cHalL3nGe_j0kE-abcdef012} . That last piece is a unique hash for the user, per challenge, so that they couldn’t directly share their flag. They can help others — even provide the solution — but they can’t simply give away their flag.

Each challenge, when solved, is worth a certain number of points based on the challenge’s difficulty and whether or not the player used the challenge’s hints.

There were 3800 points available, though no player was able to break 1000!

At the end, we locked the leaderboard and announced the winners.

Prizes

We awarded the top four players based on their ranking on the leaderboard. First place got first dibs from the list. Second place gets to select theirs from the remaining lot, and so on.

Up for grabs this year:

Challenge snapshot

Throughout the eight categories we had a total of 46 challenges. We wanted to have a wide range of challenges that welcomed players of all backgrounds and experience levels.

The goal for XSS challenges was to get an alert dialog to appear. The player is presented with a vulnerable web page and they needed to determine where the vulnerability is and how to exploit it. Example:

These challenge levels ranged everywhere between simple non-sanitized output to DOM reflection to CSP bypasses.

One fo the more unique challenges to develop was SQL Injection. These offered players the ability to put their SQL skills to the test with a variety of basic input injection, blind injection, and filter bypassing challenges.

In at least one of the SQLi challenges, players had to inject into an INSERT statement. When creating challenges like this, special care had to be taken to give players the full capabilities of MySQL but also prevent them from revealing the flag to other players — it’s a tricky thing making vulnerabilities secure!

The infrastructure

A frequent question I receive when I talk about deving on the CTF is “are you using CTFd?” Short answer? Nope! A slightly less short answer is that CTFd wasn’t out when we started this =P.

The framework we’re using is called “Security Training Grounds” and it’s a custom-written project using PHP, PhantomJS, and MySQL (with HTML + JavaScript too, of course), running in Amazon Web Services.

An advantage of writing this in-house was that it gave us the ability to create a dynamic and robust system that has endless capabilities.

PHP + MySQL

The website was created from scratch, written in PHP with a little bit of jQuery + Bootstrap on the frontend and MySQL as the database.

The big thing here are the challenges themselves. Each challenge is hosted on its own subdomain. This enables us to provide live and interactive challenges like XSS or SQLi while still providing support static challenge types like Crypto or Reverse Engineering.

We accomplished this by allowing dynamic hostnames on the webserver and defining a subdomain hostname for each challenge that’s stored in MySQL. When a web request comes in, the app checks whether it’s a subdomain or not. If so, it hits the database to determine what to display.

For most challenges, we were able to handle all of the dynamic pieces directly in PHP. For some, such as the C or Java reverse engineering challenges, we did need to shell out to gcc or javac to build the custom binaries for each user.

PhantomJS

A difficulty for XSS and CSRF challenges is determining whether or not the participant successfully exploited the system. Surely we don’t want to manually confirm for each flag, and attempting to pattern-match on their input would be crazy.

Our solution: execute what the player submits!

This is my own little baby, a piece of the system I’m so excited by. See, what better way to test XSS than to actually test XSS. As mentioned in the “Challenge snapshot” section above, when a player is working on a XSS challenge, they are given a website that has a XSS vulnerability. Their goal is to make an alert dialog appear. This is key and the requirement of the XSS framework itself.

On the client, we use this fancy little snippet:

var ctf_alert = alert; alert = function(msg) { ctf_phantomjs_alert_test(document.location, msg); };

This overrides the window’s actual alert() function and lets us put some processing logic in the middle. The logic is to take a snapshot of the current page - the URL, query string, POST parameters, the cookies and then pass the full snapshot to a backend PhantomJS service (via a PHP proxy, to help prevent tampering).

The PhantomJS service replicates that entire request and loads the target web page. If the page invokes an alert() call, which we catch via PhantomJS’s onAlert , then we return with a “success” and the PHP proxy will return the user’s flag. Our alert() overriding logic will then replace whatever message the user attempted to display and display their flag instead. Fancy af.

CSRF has a similar setup, except the player needs to submit their full CSRF payload:

After submitting the payload to the PHP proxy, we pass the payload to PhantomJS. This executes the payload in the context of an empty web page. If the PhantomJS worker successfully falls victim to the targeted action, the PHP proxy will return a flag to the user!

Open source

The framework code, as-is, is still relatively hacky and written with internal dependencies. We do believe in OSS though! We expect a near-future initiative to rewrite portions of it so we can release it for others to use for their CTF events, too.

Wanna Play?

Quick, come apply so you can participate in the next one: https://www.tumblr.com/jobs