Stripe CTF Post Mortem

September 5th, 2012

A Would-Be Hacker's Tale

By Stephen Whitmore (@Noffle)

Last week I had the pleasure of taking on Stripe's second CTF security challenge, which was centred this time around the theme of web security.

The gist of the challenge: The kind folks at Stripe would run virtualized web servers running exploitable home-spun web applications. Entrants were then presented with a problem statement and relevant sections of server source code. In each level there would be a password hidden somewhere that would unlock the next level: a specific user account password, a secure file, and so forth. Participants were to gain access to by employing various web security exploits.

This was a truly fantastic opportunity. Actually performing these exploits -- in a safe environment -- served as a stellar means of becoming more conscientious of their existence, and subsequently encouraged me to write better defenses against them. Doing, as they say, is a world of apart from simply reading.

I will be discussing a subset of the nine challenges: presenting the simple web applications provided, discussing possible attack vectors, going through the solutions that I managed to find, and sharing preventative tips to avoid making the same mistakes in your own software.

Level 0: Simple SQL Injection

The very first level's setup is simple enough: secrets are stored and associated with a namespace only known to the poster of the secret. Users can retrieve all secrets for any namespace they'd like, although it requires foreknowledge of the namespace's name.

A quick perusal of the source reveals an SQL query:

SELECT * FROM secrets WHERE key LIKE ? || ".%"

This query does not take proper care to escape user input, enabling the would-be hacker to pass in a "%" as the namespace (the SQL wildcard character) to retrieve the secrets for all namespaces.

Moral and Countermeasures: Although this tends to be the first lesson drilled into a web developer's head, it continues to be a lucrative exploit: always perform SQL escaping on all user input that will appear in a query. One can rig up this escaping logic ad-hoc, but as with most software, someone has already done a better job of it than you will: investigate SQL libraries for your language of choice.

Level 1: Dangerous PHP Functions

Enter Level 1. Level 1 is a guessing game. Much like the previous level, if the user enters the correct string, the password for the next level will be revealed.

An initial scan of the source code reveals one glaring anomaly: the use of a PHP function named extract. The PHP documentation describes it as follows:

Import variables from an array into the current symbol table.

To understand the terrible power of this function, consider the following code snippet:

$filename = 'secret-combination.txt';

The implication is that if you have code like this, a user can simply provide a GET parameter of ?filename=/dev/zero or ?filename=/dev/null or ?filename=foo.txt or whatever else suits their fancy, and overwrite whatever local variables were already present in the PHP symbol table, allowing the would-be hacker very powerful manipulation of how the script operates.

Moral and Countermeasures: Do not use excessively and unnecessarily powerful functions like PHP's extract or eval. Especially when user input is involved.

Level 2: User File Uploads

Level 2 looks like a snippet out of a social network application. It provides the user with the means to upload a display image for their account, which is stored on the web server locally under an uploads/ directory.

On its own, this might not be much of a security hole. However, the web application leaves three1 significant holes wide open that make the work of the would-be hacker much easier:

In light of this information, the would-be hacker can simply upload a PHP script containing some malicious code:

echo `cat ../password.txt`;

She may then visit /uploads/exploit.php, which will execute the script and provide the password to the next level.

Moral and Countermeasures: The instant a website enables users to upload arbitrary files from their computer (or, to a lesser extent, mobile device), there is room for a potential security hole. Remember: just because a user names their file foo.jpg does not make the file a JPEG image. Users can upload Linux binaries, PHP scripts, and other nefarious data. This cannot inherently be prevented: the flood gates are open the instant you supply them with an OS-level file picker. However, there are appropriate safety measures the vigilant web developer can take:

Level 3: More Advanced SQL Injection

The third level of Stripe CTF presents another type of safe: one where users must authenticate in order to view their stored secrets. User account registration is prohibited.

The astute web developer will quickly notice the first hole in the application by inspecting the following lines of code:

query = """SELECT id, password_hash, salt FROM users WHERE username = '{0}' LIMIT 1""".format(username)

This logic makes no attempt to escape the username, making it a tantalizing target for SQL injection. Although there is only a SELECT statement that can be manipulated, this is sufficient to retrieve much information from the database, as demonstrated below.

The first attack vector to come to mind is to simply pass in '; UPDATE users SET password_hash = ... or similar in order to end the current query and then execute a second, more directed query. This second query would set all passwords to something trivial, granting entry to all accounts to the would-be hacker. However, the SQLite library (wisely) prevents multiple queries from being run inside of one excute() call. Attempting to run the above injected query results in an error page, complete with stack trace of the error and the ability to view the full server source. Hullo!

The debug mode for the underlying web server library, Flask (and its underlying CGI library, Werkzeug was left enabled, probably by a negligent web developer. The source is already provided to us in this case by the Stripe CTF folks, but this would be invaluable information for the would-be hacker2.

Although coalescing two queries together will not work, it is possible to make use of the SQL UNION operator to achieve similar ends. By injecting ' UNION SELECT id, '', '' FROM users WHERE username = 'bob, the full query becomes

SELECT id, password_hash, salt FROM users WHERE username = '' UNION SELECT id, 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', '' FROM users WHERE username = 'bob' LIMIT 1

The use of UNION here is to completely negate whatever the application originally wanted to query by selecting nothing, and then UNIONing it with information that we have hand-picked: the user holding the password for Level 4, 'bob'. The text e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 is the SHA-256 hash for the empty string. So, by passing in an empty password string for login, the empty string is concatenated with the empty salt, resulting in hash(salt + password) == password_hash evaluating to hash("" + "") == hash("") giving the would-be hacker access to any user account.

Moral and Countermeasures:

Level 4: Request Forgery

The fourth level presents a something resembling a social network, facilitating the exchange of 'karma' between users. Any user is free to donate karma to any other user, but with the caveat that whomever a user gives her karma to will be able to view her password. The website's rationale is: "In order to ensure you're transferring karma only to good people, transferring karma to a user will also reveal your password to him or her". The other important piece of information is that there is a special user, karma_fountain, which holds infinite karma, and visits the site every minute.

Some playful experimentation with the username and password fields on the registration page reveals that one can set their username to something such as <script>alert('hello world!');</script>, which displays a Javascript alert each time I -- or any other user -- sees my name. Ah ha! The web application makes no effort to escape Javascript, meaning any user who views the username or password of another user containing Javascript will unwittingly have that code executed by their browser. This forms the basis for forging requests within the same domain3.

Since browser-executed Javascript is perfectly capable of populating input fields and submitting forms, this is a perfect attack vector to pursue. It's just a matter of registering a user named, say, 'some_user', with a password of

<script> document.forms[0].elements[0].value = 'some_user'; document.forms[0].elements[1].value = '1'; document.forms[0].submit(); </script>

Then, once you use this user account to donate some amount of karma to the karma_fountain (or any other user), they will have that the malicious user account's password revealed to them, causing their browser to automatically submit karma back to an account of the would-be hacker's choosing, thus revealing the unsuspecting user's password. Better yet, the affected user may not even notice that her browser performed this malicious act underneath her.

Moral and Countermeasures:

Level 7: Hash Length Extension Attacks

Jumping to the penultimate level, we see something new: a web application centred around a JSON-based API for request submission. Each user account has a secret key assigned to it, which only the web application and the user know. The user digitally signs her requests by computing the SHA-1 hash of the concatenation of the secret key and the request: hash(secret_key + request). The final request to appear in the HTTP GET will be


The web application identifies the user by an 'endpoint URL', such as, and is able to verify the user's MAC (message authentication code) by computing the same hash and comparing it to the one provided by the user.

This style of MAC is vulnerable to a flavour of cryptographic attack called hash length extension. You can find a better treatment on it in this blog post.

SHA-1, like other cryptographic hashing aglorithms, operates sequentially on blocks of binary input provided to it. The hash it produces at the end of a block is exactly equivalent to the internal state of the algorithm once it processes said blocks. As such, it is possible to simply take an existing hash and continue running the hashing function on it with further blocks of input. This enables the would-be attacker to add more data to the end of her request without knowledge of the secret key. HTTP requests that rely on GET parameters are especially vulnerable to this attack, since a successive assignment of a parameter will override an earlier use.

The goal of this level is to order a premium waffle, the 'liege', which the would-be hacker's user account is not permitted to order. It is possible to sniff the logs of previous requests other users have made though. In the above example, the user has ordered a chicken(?) waffle. This exploit can be used to add the string &waffle=liege to the GET parameters, causing the preceding waffle=chicken parameter to be overridden, but still producing the correct MAC. The new request, using the length-extended attack (and some sample code), is


Which will grant the would-be hacker the ability to order the much sought-after liege waffles.

Examine the following binary dump of the above request:

0000000: 636f 756e 743d 3226 6c61 743d 3337 2e33  count=2&lat=37.3
0000010: 3531 2675 7365 725f 6964 3d31 266c 6f6e  51&user_id=1&lon
0000020: 673d 2d31 3139 2e38 3237 2677 6166 666c  g=-119.827&waffl
0000030: 653d 6368 6963 6b65 6e80 0000 0000 0000  e=chicken.......
0000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000070: 0238 2677 6166 666c 653d 6c69 6567 657c  .8&waffle=liege|
0000080: 7369 673a 6166 6635 3030 3563 6361 3837  sig:aff5005cca87
0000090: 6465 3531 3866 3639 3838 3765 6236 6665  de518f69887eb6fe
00000a0: 3235 3365 3863 3132 3434 3732            253e8c124472

Here you can see the explicit padding being added to the original request in order to force the start of a new SHA-1 block. Cool!

Moral and Countermeasures: Use HMAC. Do not try to invent your own MAC algorithm. Heck, do not even bother implementing it yourself: most modern languages offer a native implementation that will trump your own ad-hoc implementation.


This brings to an end my brief treatment of some of my favourite levels of the Stripe CTF 2.0 challenge. Despite having read this, I still highly recommend taking the time to go through the various CTF challenges -- which Stripe says they will be making available soon -- so that you can hack on them yourself. I hope that this article has been informative, and inspires you to write more secure software.


The biggest thanks of all goes out to to Stripe for generously conceiving of, developing, and hosting the challenges. You've done the web development community a favour.

A sizable thanks to Colton Pauderis, my partner-in-crime who puzzled out most of the later problems with me. Colton is also responsible for a much-needed round of revisions on this article.

Thanks also go out to Edmund Lo for his assistance and SQL wizardry.

This article was written in markdown, and makes use of a modified version of Kevin Burke's markdowncss CSS theme.

[1] There are two additional, less severe quibbles:

[2] Werkzeug also exposes an option to provide a Python shell inlined into the debug page as well, although in this case it was (wisely) disabled. The would-be hacker could have done far more terrifying things with this!

[3] Cross-Site Request Forgery (CSRF or XSRF) is a different beast, and can be read about more on Jeff Atwood's blog.

[4] In case you still aren't sold, consider this additional piece of persuasive prose.