First off, I want to thank Steven van der Baan, Project Lead for the OWASP CTF Project, and the AppSecUSA 2011 team for putting everything together. This pre-challenge was thoughtfully crafted and deserves credit for driving multiple contestants crazy! I am putting this walk-through together to show the thought process we took throughout the challenge and to be a resource to anyone who is wanting to learn more about application security. I also want to give a shout out to @lobobastich, my team member who I couldn’t have done the challenge without.
Ok, lets get started!
We are directed to challenge.appsecusa.org where the challenge resides; it is an OWASP book store application which appears to be selling a variety of OWASP related products. The first thing that catches our attention is the products titled “OWASP AppSecUSA Ticket”… unfortunately there is no “add to cart” link for this item and in its place says “(you wish)”… hmm, maybe something is there.
There is a login form which doesn’t seem to have any kind of SQLi or XSS vulnerabilities. We went ahead and created an account to use with my email address: firstname.lastname@example.org. Then on the info/contact page we have another form which doesn’t have any SQLi or XSS; but if you enter in your email address that you registered with you will see your complete database record printed to the screen, nice!
We now know the structure of the users table. (save that for later)
If you view the source on this page you find some interesting HTML comments that don’t really end up helping you with anything in regards to the challenge. It was still a fun little easter egg!
We also found a directory called /invoice/ with what appears to be an administrative login; this must be important. No SQLi or XSS; we attempted some simple brute force with no luck. However, if you login in with your name (not your email, even though it asks for email) and your password you get an error saying “Wrong Level”. Ok, this must be the userlevel column on the users table that we found from the contact form. (save this for later)
Let’s go back to the product page and see if we can find anything juicy there. After adding an item to our cart, we started playing with the update feature which sends the following post data:
At first this doesn’t seem injectable, but after some tinkering we found that the parameter key “qty1″ is in fact injectable. The PHP script extracts the number following the “qty” and place it inside the SQL query.
Query failed ... near '\'' at line 1 SQL: SELECT * FROM books WHERE id = 1\' in
Perfect, our first attack may begin. We quickly found out that the PHP script was escaping all single and double quotes, and whenever we used commas it split the query into two different queries. Now this has gotten tricky… we can’t use quotes or commas at all with this SQLi. We also can’t use Sqlmap… its too unique. We can try a UNION query but we first need to f ind a table with the same amount of columns.
If you then try to purchase the item in your cart, you will then find another little SQLi using the same concept as before. Lucky for us, the application is printing out the SQL error and we get the following information:
Query failed ... near '\'10)' at line 1 SQL: INSERT INTO salerow(saleid,bookid,qty) VALUES(151570,1,1\'10) ...
We don’t know it yet, but we just found a perfect match for our UNION query. We found out from trial and error that the ‘books’ table has 3 columns, all integers, and here we see that salerow has 3 columns which are all integers. Lets union them together to make sure. First before I go into this attack payload, I want to share a little technique I used to make exploitation easier on this unique SQLi situation. Instead of having to modify the parameter key in each POST request to perform the exploit I created a PHP script that would do this form me. This PHP script takes a supplied GET variable (?here=) and crafts it into a POST request (plus a little extra magic) to the challenge.appsecusa.org site using cURL. Here is the PHP source, you can skip over this if you like doing it the hard and tedious way; feel free to ask me any questions about this script in the comments section.
<?php // Set random cookie session_write_close(); $cookieval = uniqid(); // SQLi information $param = str_replace(" ","\n",$_GET['here']); $url1 = "http://challenge.appsecusa.org/cart.php?action=add&id=1"; $url2 = "http://challenge.appsecusa.org/cart.php?action=update"; $cookie = "PHPSESSID=".$cookieval; $payload = "qty".urlencode($param)."=1"; // First Add Initial Item $ch = curl_init(); curl_setopt($ch,CURLOPT_URL,$url1); curl_setopt($ch,CURLOPT_FOLLOWLOCATION,0); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); curl_setopt($ch,CURLOPT_COOKIE,$cookie); $result = curl_exec($ch); curl_close($ch); // Send SQLi Request $ch = curl_init(); curl_setopt($ch,CURLOPT_URL,$url2); curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1); curl_setopt($ch,CURLOPT_RETURNTRANSFER,0); curl_setopt($ch,CURLOPT_COOKIE,$cookie); curl_setopt($ch,CURLOPT_POST,2); curl_setopt($ch,CURLOPT_POSTFIELDS,$payload); $result = curl_exec($ch); curl_close($ch); ?>
Now, using our PHP script we can pass the following query:
?here=UNION SELECT * FROM salerow ORDER BY id DESC LIMIT 1 OFFSET 1
You can enumerate through the entire salerow table by incrementing the OFFSET value until you receive an error. After enumerating a large portion of the salerow, we determined there is nothing juicy here. But all is not lost… we can use this to extract some very nice data. Remember how the /invoice/ section said we had the wrong userlevel? Lets use this to find out who has the correct userlevel to access that page:
?here=(SELECT COUNT(id) FROM users WHERE userlevel=1)
This query returns the count of users with the supplied user level… pay attention to what gets added to your cart; if its an error than it returned 0, if it added an item, look at the items ID and that will be the number of users that fit the query. With userlevel=1 we found 1 user. Lets find out how many characters are in their username:
?here=(SELECT COUNT(id) FROM users WHERE userlevel=1 AND LENGTH(name)=1
Keep incrementing the length value until the query returns true (no error). Looks like the username has 5 characters. Lets figure out what the first letter of the username is:
?here=(SELECT COUNT(id) FROM users WHERE userlevel=1 AND ORD(name)=97
The MySQL ORD() function returns the character code from the leftmost character; in this case 97 = a. So its five letters and starts with ‘a’… my guess is ‘admin’. Here is a curve ball thought, HINT #2 says “To go in the right direction, think about what person usually handles an invoice.” This means ‘admin’ probably isn’t what we want, lets look and see if there is another user level:
?here=(SELECT COUNT(id) FROM users WHERE userlevel=2)
Yep! There is one user with the userlevel=2. Rinse and repeat the queries above and we find out that the username has 5 characters and starts with ‘s’. My guess is ‘sales’! (which is correct). Ok, we know who to login as, and to cut to the chase I’m going to show you how to extract their hashed password:
Use the ‘purchase’ SQLi to inject the value of the hash into the salerow table then use the ‘update’ SQLi to retrieve the values. Remember these are only integer columns so we need to HEX the value. Also, after trial and error we couldn’t do the entire HASH at once; instead we do 1 character at a time. The hashes are MD5, so that means 32 characters. Here we go:
(SELECT HEX(SUBSTR(password FROM 1 FOR 1)) WHERE userlevel=2)
Increment the FOR 1 value all the way up to 32 and you will have the entire hash:
Run it through Google and you will find out that the password is: ‘guessable’. Nice password! Adding that to my bruteforce list. Go ahead and login to the /invoice/ section of the site. Access Granted, awesome!
I’m not going to go into depth of what we attempted in the /invoice/ section because we spun our wheels a lot. You have two main options in this section: ‘View Invoice’ and ‘Process Sale’. If you manipulate the values on either of these requests you get a very nice SQLi, which you can now run Sqlmap on. Pwned, thats the entire database structure/data dumped to our hard-drive. Another important note is to know that the PDFs are created and stored with the following naming convention:
Where is the key though!? Not in the database or our PDF invoices, we looked all over. It wasn’t until they released the final hint that we found the key: “MY invoice will get you there, not YOURS!”.
Ok, look through the users table and try to find HIS user, not YOURS! Our options are ‘admin’(id=1), ‘sales’(id=2), ‘AppSecUSA’(id=3), or ‘myname’(id=4 which is steven’s account) … obviously we chose the three wrong options first ‘admin’, ‘sales’, and ‘myname’. We enumerated all their invoices with the “OWASP AppSecUSA Ticket” item using this query and found nothing.
?here=SELECT s.date FROM sales s, salerow sr WHERE s.id=sr.saleid AND s.userid=1 AND sr.bookid=10
We then chose to enumerate the ‘AppSecUSA’ (id=3) and found one invoice with the ticket in it:
Tango DOWN! We found the registration key! Submitted it to the challenge and the game was over!
(Base64 of “1 OWASP AppSecUSA 2011 Ticket”)
Thanks again for the awesome challenge OWASP! I learned a lot throughout the process and hope that others can learn from it as well.