The Web 3.0 category was almost entirely an exercise in SQL injection.
There were a few gotcha's, and the last one was pretty interesting. While
the "lessons" for each stage are important, the solutions to each stage
seemed slightly less than real-world in their feel.
For testing for SQL injection vulnerabilities, you usually just put in a single
quote, and hit "Submit". This immediately made the service blow up with a
500-series "internal server error". The next step to SQL injection is to
attempt to "OR" your way into a "TRUE" SQL select statement. To ditch
the remainder of the SQL statement, the easiest method is to comment out the
rest with the "--" comment syntax. This results in:
' OR 1=1 --
Using this for the username, the key is displayed.
Generally, these sorts of programming mistakes come from a style of
"user authentication" that only tests if the SQL statement returns a row, but
doesn't check the results. For example:
$result = sql_query("SELECT id FROM users WHERE username='$username' AND password='$password'")
if (rows($result)>0) {
// let them in
}
The "' OR 1=1 --" attack comments out the password match, and kicks back an
empty single-row SQL result (NULL id). This, unfortunately, is a very
common mistake in SQL-based attempts at authentication.
As in 200, it's immediately obvious that it is vulnerable to SQL injection
attacks because it blows up with a username of "'". However, this time, we
get fancy error tracing. How handy! Clicking on the "..." on the right
side, we can examine the SQL statement, and various code snippets.
At first glance, it seems the solution for 200 would work here as well.
After trying it, though, we're greeted with an error from the script itself,
saying "Invalid characters in username". This means filtering is happening
before passing the username variable to the SQL statement. Since we know it's
not the "'" character, we check on the "-" character. As it turns out, this
is also an invalid character. So, it seems we must construct a query without
commenting out the rest of the SQL.
We try to make up a stand-alone "OR" test in what would otherwise be a valid
query. A quick attempt:
' OR 1=1 OR username='
We're greeted again with the "Invalid characters" message, and we now consider
that spaces are not allowed. No worries! Either use the other style
of SQL commenting to separate your statement elements, or just don't use
spaces at all. Both of these solutions do the trick:
'OR''=''OR''='
'/**/OR/**/1=1/**/OR/**/username='
For this challenge, since we were being given the actual source to the script,
it would have been more "real-world appropriate" to have to defeat a more
robust check of the SQL result. To verify that a valid row was returned
(rather than an all-NULL row) a test of getting back a valid username from
the database might have been fun. This would have required a slightly more
advanced SQL injection:
'/**/OR/**/username/**/like/**/'%'/**/OR/**/''='
This would have returned the first user in the table.
Since one should never forget to read the source, we notice a hidden FORM
variable named "sessKey". Curiously close to 31337, we play with it a little,
but without any immediate results. We do notice, however, that it is being
incremented with each new HTTP request. Saving this for later, we move on.
The "'" username test shows another vulnerable script, but this time without the helpful
error tracing. Trying "' OR 1=1 --" blows it up, though. Suspecting that
the query may require a more valid result than an all-NULL row, assuming
none of the database fields changed, we try what should be a valid query:
' AND username='
This does NOT blow up the server, but (of course) doesn't let us in.
Now assuming that the
results of the SQL query are being examined more closely this time, we move
on to the next SQL injection trick: "UNION SELECT". With this syntax, the goal
is to produce NO rows in the SQL preceeding the injection, and then
manufacture a row within the injection. Assuming the same query as 300
("SELECT username, password") we try:
' UNION SELECT 1,2 --
And discover a new error, which says that session is already in use. Sounds
like we made it past the SQL test! Moving back to the hidden variable, we
set it to 31337 and try again. The key greets us.
As with 300, a more "real-world" implementation maybe would have checked
the password. In that case, (again assuming the same style of password
authentication from 300: md5) we could have built an md5 string, and passed
the matching clear-text in the "password" variable, with the injection:
' UNION SELECT 'pwnd','758d31f351ce11bf27c522f516cb4c20' --
This URL immediately greeted us with a cookie named "source_ip". Ignoring
this for a moment, we find the script is SQL injectable, and with:
' OR 1=1 --
We get a new error message that says we can only log in from 127.0.0.1.
Assuming that this test is being done on the cookie rather than, say,
looking at the socket address or some web server environment variable, we
examine the cookie more closely.
Connecting from the same IP address, we get the same cookie value.
Connecting from a different IP address, we get a slightly different cookie.
So, we conclude that the cookie value holds an encoded IP address. The
strings are the same length as the IP address strings, so we assume this
is a character scrambling of some kind. Seeing that repeated characters
in the IP address do not repeat in the scrambled version, we further conclude
this is like a position-based scrambling. (i.e. In the string "127.0.0.1",
the "0" in position 5 could be scrambled to "A", but "0" in position 7 uses
a different scrambling, and could be "H".) Seeing that the same characters
in the same position are always the same, we're pretty sure it's not some
kind of "real" encryption that would depend on prior characters in the
string, etc.
Now, if you're like team 1@stPlace, you've already done a bunch of the
other lower-level challenges in all the other categories at this point
in the game.
We decided we needed to sample an IP address that matched "127.0.0.1" as
closely as possible. ;) We shelled
into kenshoto.allyourboxarebelongto.us with one of our Pwnage exploits,
bound to localhost, and talked to the server, care of netcat:
(echo "GET / HTTP/1.0"; echo ""; sleep 5) | nc -s 127.0.0.1 localhost 8500
This gave us the needed cookie string, and we were on our way. :) However,
it seems this was not the intended approach, and this route was closed after
Kenshoto realized what had happened. D'oh.
The "correct" solution is to sample as many IP addresses as you can. A
pattern emerges that shows each character in the string has been rotated,
sometimes with some weird skips, reversals, etc.
For example in the first character position, "1" is "\010", "2" is "\013", "6"
is "\017". After the prequals, we checked our IP samples, and found
that we had enough character matches to produce almost the entire cookie
string. We were only missing the second zero and the terminal "1":
\010\012K\231\256R0\0371
We had samples for the 9th character of "3" and "2" being "\037" and "\036",
so we assumed "1" would be "\035". For the 7th character "0", we only had
a single sample of "1" being "\332", so we guessed the direction and found "0"
at "\333". Passing this to "curl", the key was found:
curl -b 'source_ip="\010\012K\231\256R\333\037\035"' -F username="' OR 1=1 --" -F password= http://kenshoto.allyourboxarebelongto.us:8500/fa926fa448eba493b863d035d1a19597
This challenge was fun both times we solved it. The first time was
ninja-fun, and the second time was fun because it was effectively a scavenger
hunt for controlled IP address sources. Very cool problem.
UPDATE: Thanks to Sk3wlMaster for pointing out that the scrambling used was
actually just XOR, not a rotation cipher. The IP string was just XOR'd
against the string "\x39\x38\x7C\xB7\x9e\x7C\xEB\x31\x2C\x16\x7E\x92\x59\x36".
UPDATED UPDATE: Invisigoth has clarified that the cipher string used
for the XORing is actually an RC4 stream, seeded with "kenshoto".