From f316e722c10a8c45eecb67a842fcfdbae01423ee Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Sat, 9 Jan 2010 00:05:00 +0000 Subject: [PATCH] sqlmap 0.8-rc4: --dump option now can also accept only -C: user can provide a string column and sqlmap will enumerate all databases, tables and columns that contain the 'provided_string' or '%provided_string%' then ask the user to dump the entries of only those columns. --columns now accepts also -C option: user can provide a string column and sqlmap will enumerate all columns of a specific table like '%provided_string%'. Minor enhancements. Minor bug fixes. --- lib/core/dump.py | 74 +++++-- lib/core/settings.py | 4 +- lib/parse/queriesfile.py | 13 +- plugins/generic/enumeration.py | 370 ++++++++++++++++++++++++++++++--- xml/queries.xml | 25 ++- 5 files changed, 430 insertions(+), 56 deletions(-) diff --git a/lib/core/dump.py b/lib/core/dump.py index 6c8c6c2d0..d0f838511 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -123,7 +123,32 @@ class Dump: for setting in settings: self.__write(" %s: %s" % (subHeader, setting)) print - + + def dbColumns(self, dbColumns, colConsider, dbs): + for column, dbTables in dbColumns.items(): + if colConsider == "1": + colConsiderStr = "s like '" + column + "' were" + else: + colConsiderStr = " '%s' was" % column + + msg = "Column%s found in the " % colConsiderStr + msg += "following databases:" + self.__write(msg) + + printDbs = {} + + for db, tblData in dbs.items(): + for tbl, colData in tblData.items(): + for col in colData: + if column in col: + if db in printDbs: + printDbs[db][tbl] = colData + else: + printDbs[db] = { tbl: colData } + break + + self.dbTableColumns(printDbs) + def dbTables(self, dbTables): if not isinstance(dbTables, dict): self.string("tables", dbTables) @@ -155,7 +180,7 @@ class Dump: self.__write("| %s%s |" % (table, blank)) self.__write("+%s+\n" % lines) - + def dbTableColumns(self, tableColumns): for db, tables in tableColumns.items(): if not db: @@ -171,12 +196,16 @@ class Dump: for column in colList: colType = columns[column] maxlength1 = max(maxlength1, len(column)) - maxlength2 = max(maxlength2, len(colType)) + + if colType is not None: + maxlength2 = max(maxlength2, len(colType)) maxlength1 = max(maxlength1, len("COLUMN")) - maxlength2 = max(maxlength2, len("TYPE")) lines1 = "-" * (int(maxlength1) + 2) - lines2 = "-" * (int(maxlength2) + 2) + + if colType is not None: + maxlength2 = max(maxlength2, len("TYPE")) + lines2 = "-" * (int(maxlength2) + 2) self.__write("Database: %s\nTable: %s" % (db, table)) @@ -185,23 +214,42 @@ class Dump: else: self.__write("[%d columns]" % len(columns)) - self.__write("+%s+%s+" % (lines1, lines2)) + if colType is not None: + self.__write("+%s+%s+" % (lines1, lines2)) + else: + self.__write("+%s+" % lines1) blank1 = " " * (maxlength1 - len("COLUMN")) - blank2 = " " * (maxlength2 - len("TYPE")) - self.__write("| Column%s | Type%s |" % (blank1, blank2)) - self.__write("+%s+%s+" % (lines1, lines2)) + if colType is not None: + blank2 = " " * (maxlength2 - len("TYPE")) + + if colType is not None: + self.__write("| Column%s | Type%s |" % (blank1, blank2)) + self.__write("+%s+%s+" % (lines1, lines2)) + else: + self.__write("| Column%s |" % blank1) + self.__write("+%s+" % lines1) for column in colList: colType = columns[column] blank1 = " " * (maxlength1 - len(column)) - blank2 = " " * (maxlength2 - len(colType)) - self.__write("| %s%s | %s%s |" % (column, blank1, colType, blank2)) - self.__write("+%s+%s+\n" % (lines1, lines2)) - + if colType is not None: + blank2 = " " * (maxlength2 - len(colType)) + self.__write("| %s%s | %s%s |" % (column, blank1, colType, blank2)) + else: + self.__write("| %s%s |" % (column, blank1)) + + if colType is not None: + self.__write("+%s+%s+\n" % (lines1, lines2)) + else: + self.__write("+%s+\n" % lines1) + def dbTableValues(self, tableValues): + if tableValues is None: + return + db = tableValues["__infos__"]["db"] if not db: db = "All" diff --git a/lib/core/settings.py b/lib/core/settings.py index 4238da9c0..b4c16c100 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -27,7 +27,7 @@ import subprocess import sys # sqlmap version and site -VERSION = "0.8-rc3" +VERSION = "0.8-rc4" VERSION_STRING = "sqlmap/%s" % VERSION SITE = "http://sqlmap.sourceforge.net" @@ -58,7 +58,7 @@ SQLMAP_SOURCE_URL = "http://downloads.sourceforge.net/sqlmap/sqlmap-%s.zip" # Database managemen system specific variables MSSQL_SYSTEM_DBS = ( "Northwind", "model", "msdb", "pubs", "tempdb" ) MYSQL_SYSTEM_DBS = ( "information_schema", "mysql" ) # Before MySQL 5.0 only "mysql" -PGSQL_SYSTEM_DBS = ( "information_schema", "pg_catalog" ) +PGSQL_SYSTEM_DBS = ( "information_schema", "pg_catalog", "pg_toast" ) ORACLE_SYSTEM_DBS = ( "SYSTEM", "SYSAUX" ) # These are TABLESPACE_NAME MSSQL_ALIASES = [ "microsoft sql server", "mssqlserver", "mssql", "ms" ] diff --git a/lib/parse/queriesfile.py b/lib/parse/queriesfile.py index 074972bfc..38830c67e 100644 --- a/lib/parse/queriesfile.py +++ b/lib/parse/queriesfile.py @@ -145,6 +145,8 @@ class queriesHandler(ContentHandler): self.__blind2 = sanitizeStr(attrs.get("query2")) self.__count = sanitizeStr(attrs.get("count")) self.__count2 = sanitizeStr(attrs.get("count2")) + self.__condition = sanitizeStr(attrs.get("condition")) + self.__condition2 = sanitizeStr(attrs.get("condition2")) def endElement(self, name): if name == "dbms": @@ -192,11 +194,18 @@ class queriesHandler(ContentHandler): elif name == "columns": self.__columns = {} - self.__columns["inband"] = { "query": self.__inband } - self.__columns["blind"] = { "query": self.__blind, "query2": self.__blind2, "count": self.__count } + self.__columns["inband"] = { "query": self.__inband, "condition": self.__condition } + self.__columns["blind"] = { "query": self.__blind, "query2": self.__blind2, "count": self.__count, "condition": self.__condition } self.__queries.columns = self.__columns + elif name == "dump_column": + self.__dumpColumn = {} + self.__dumpColumn["inband"] = { "query": self.__inband, "query2": self.__inband2, "condition": self.__condition, "condition2": self.__condition2 } + self.__dumpColumn["blind"] = { "query": self.__blind, "query2": self.__blind2, "count": self.__count, "count2": self.__count2, "condition": self.__condition, "condition2": self.__condition2 } + + self.__queries.dumpColumn = self.__dumpColumn + elif name == "dump_table": self.__dumpTable = {} self.__dumpTable["inband"] = { "query": self.__inband } diff --git a/plugins/generic/enumeration.py b/plugins/generic/enumeration.py index 05c1c921c..06e5f88ee 100644 --- a/plugins/generic/enumeration.py +++ b/plugins/generic/enumeration.py @@ -190,7 +190,11 @@ class Enumeration: errMsg = "unable to retrieve the number of database users" raise sqlmapNoneDataException, errMsg - indexRange = getRange(count) + if kb.dbms == "Oracle": + plusOne = True + else: + plusOne = False + indexRange = getRange(count, plusOne=plusOne) for index in indexRange: if condition: @@ -299,7 +303,12 @@ class Enumeration: logger.info(infoMsg) passwords = [] - indexRange = getRange(count) + + if kb.dbms == "Oracle": + plusOne = True + else: + plusOne = False + indexRange = getRange(count, plusOne=plusOne) for index in indexRange: if kb.dbms == "Microsoft SQL Server": @@ -543,7 +552,12 @@ class Enumeration: logger.info(infoMsg) privileges = set() - indexRange = getRange(count) + + if kb.dbms == "Oracle": + plusOne = True + else: + plusOne = False + indexRange = getRange(count, plusOne=plusOne) for index in indexRange: if kb.dbms == "MySQL" and not kb.data.has_information_schema: @@ -742,7 +756,12 @@ class Enumeration: continue tables = [] - indexRange = getRange(count) + + if kb.dbms in ( "Microsoft SQL Server", "Oracle" ): + plusOne = True + else: + plusOne = False + indexRange = getRange(count, plusOne=plusOne) for index in indexRange: query = rootQuery["blind"]["query"] % (db, index) @@ -785,31 +804,46 @@ class Enumeration: conf.db = self.getCurrentDb() - infoMsg = "fetching columns " + rootQuery = queries[kb.dbms].columns + + infoMsg = "fetching columns " + + if conf.col: + if kb.dbms == "Oracle": + conf.col = conf.col.upper() + colList = conf.col.split(",") + condition = rootQuery["blind"]["condition"] + condQuery = " AND (" + " OR ".join("%s LIKE '%s'" % (condition, "%" + col + "%") for col in colList) + ")" + infoMsg += "like '%s' " % ", ".join(col for col in colList) + else: + condQuery = "" + infoMsg += "for table '%s' " % conf.tbl infoMsg += "on database '%s'" % conf.db logger.info(infoMsg) - rootQuery = queries[kb.dbms].columns - if kb.unionPosition: if kb.dbms in ( "MySQL", "PostgreSQL" ): query = rootQuery["inband"]["query"] % (conf.tbl, conf.db) elif kb.dbms == "Oracle": query = rootQuery["inband"]["query"] % conf.tbl.upper() elif kb.dbms == "Microsoft SQL Server": + # TODO: adjust with condQuery query = rootQuery["inband"]["query"] % (conf.db, conf.db, conf.db, conf.db, conf.db, conf.db, conf.db, conf.tbl) + query += condQuery value = inject.getValue(query, blind=False) if value: table = {} columns = {} + for column, colType in value: columns[column] = colType + table[conf.tbl] = columns kb.data.cachedColumns[conf.db] = table @@ -824,8 +858,10 @@ class Enumeration: elif kb.dbms == "Oracle": query = rootQuery["blind"]["count"] % conf.tbl.upper() elif kb.dbms == "Microsoft SQL Server": + # TODO: adjust with condQuery query = rootQuery["blind"]["count"] % (conf.db, conf.db, conf.tbl) + query += condQuery count = inject.getValue(query, inband=False, expected="int", charsetType=2) if not count.isdigit() or not len(count) or count == "0": @@ -834,24 +870,27 @@ class Enumeration: errMsg += "on database '%s'" % conf.db raise sqlmapNoneDataException, errMsg + table = {} + columns = {} + if kb.dbms == "Microsoft SQL Server": plusOne = True else: plusOne = False - - table = {} - columns = {} indexRange = getRange(count, plusOne=plusOne) for index in indexRange: if kb.dbms in ( "MySQL", "PostgreSQL" ): - query = rootQuery["blind"]["query"] % (conf.tbl, conf.db, index) + query = rootQuery["blind"]["query"] % (conf.tbl, conf.db) elif kb.dbms == "Oracle": - query = rootQuery["blind"]["query"] % (conf.tbl.upper(), index) + query = rootQuery["blind"]["query"] % (conf.tbl.upper()) elif kb.dbms == "Microsoft SQL Server": + # TODO: adjust with condQuery query = rootQuery["blind"]["query"] % (index, conf.db, conf.db, conf.tbl) + query += condQuery + query = agent.limitQuery(index, query) column = inject.getValue(query, inband=False) if not onlyColNames: @@ -881,11 +920,275 @@ class Enumeration: return kb.data.cachedColumns - def dumpTable(self): - if not conf.tbl: - errMsg = "missing table parameter" + def dumpColumn(self): + # TODO: adjust for MSSQL + + if kb.dbms == "MySQL" and not kb.data.has_information_schema: + errMsg = "information_schema not available, " + errMsg += "back-end DBMS is MySQL < 5.0" + raise sqlmapUnsupportedFeatureException, errMsg + + if not conf.col: + errMsg = "missing column parameter" raise sqlmapMissingMandatoryOptionException, errMsg + rootQuery = queries[kb.dbms].dumpColumn + foundCols = {} + dbs = {} + colList = conf.col.split(",") + colCond = rootQuery["inband"]["condition"] + dbCond = rootQuery["inband"]["condition2"] + + message = "do you want sqlmap to consider provided column(s):\n" + message += "[1] as LIKE column names (default)\n" + message += "[2] as exact column names" + colConsider = readInput(message, default="1") + + if not colConsider or colConsider.isdigit() and colConsider == "1": + colConsider = "1" + colCondParam = " LIKE '%%%s%%'" + elif colConsider.isdigit() and colConsider == "2": + colCondParam = "='%s'" + else: + errMsg = "invalid value" + raise sqlmapNoneDataException, errMsg + + if kb.dbms == "Microsoft SQL Server": + plusOne = True + else: + plusOne = False + + for column in colList: + if kb.dbms == "Oracle": + column = column.upper() + conf.db = "USERS" + + foundCols[column] = {} + + if conf.db: + for db in conf.db.split(","): + dbs[db] = {} + foundCols[column][db] = [] + + continue + + infoMsg = "fetching databases with tables containing column" + if colConsider == "1": + infoMsg += "s like" + infoMsg += " '%s'" % column + logger.info(infoMsg) + + if conf.excludeSysDbs and kb.dbms != "Oracle": + dbsQuery = "".join(" AND '%s' != %s" % (db, dbCond) for db in self.excludeDbsList) + infoMsg = "skipping system databases '%s'" % ", ".join(db for db in self.excludeDbsList) + logger.info(infoMsg) + else: + dbsQuery = "" + + colQuery = "%s%s" % (colCond, colCondParam) + colQuery = colQuery % column + + if kb.unionPosition: + query = rootQuery["inband"]["query"] + query += colQuery + query += dbsQuery + values = inject.getValue(query, blind=False) + + if values: + if isinstance(values, str): + values = [ values ] + + for value in values: + dbs[value] = {} + foundCols[column][value] = [] + else: + infoMsg = "fetching number of databases with tables containing column" + if colConsider == "1": + infoMsg += "s like" + infoMsg += " '%s'" % column + logger.info(infoMsg) + + query = rootQuery["blind"]["count"] + query += colQuery + query += dbsQuery + count = inject.getValue(query, inband=False, expected="int", charsetType=2) + + if not count.isdigit() or not len(count) or count == "0": + warnMsg = "no databases have tables containing column" + if colConsider == "1": + warnMsg += "s like" + warnMsg += " '%s'" % column + logger.warn(warnMsg) + + continue + + indexRange = getRange(count, plusOne=plusOne) + + for index in indexRange: + query = rootQuery["blind"]["query"] + query += colQuery + query += dbsQuery + query = agent.limitQuery(index, query) + db = inject.getValue(query, inband=False) + dbs[db] = {} + foundCols[column][db] = [] + + for column, dbData in foundCols.items(): + colQuery = "%s%s" % (colCond, colCondParam) + colQuery = colQuery % column + + for db in dbData: + infoMsg = "fetching tables containing column" + if colConsider == "1": + infoMsg += "s like" + infoMsg += " '%s' in database '%s'" % (column, db) + logger.info(infoMsg) + + if kb.unionPosition: + query = rootQuery["inband"]["query2"] + if kb.dbms == "Oracle": + query += " WHERE %s" % colQuery + else: + query = query % db + query += " AND %s" % colQuery + values = inject.getValue(query, blind=False) + + if values: + if isinstance(values, str): + values = [ values ] + + for value in values: + if value not in dbs[db]: + dbs[db][value] = {} + + dbs[db][value][column] = None + foundCols[column][db].append(value) + else: + infoMsg = "fetching number of tables containing column" + if colConsider == "1": + infoMsg += "s like" + infoMsg += " '%s' in database '%s'" % (column, db) + logger.info(infoMsg) + + query = rootQuery["blind"]["count2"] + if kb.dbms == "Oracle": + query += " WHERE %s" % colQuery + else: + query = query % db + query += " AND %s" % colQuery + count = inject.getValue(query, inband=False, expected="int", charsetType=2) + + if not count.isdigit() or not len(count) or count == "0": + warnMsg = "no tables contain column" + if colConsider == "1": + warnMsg += "s like" + warnMsg += " '%s'" % column + warnMsg += "in database '%s'" % db + logger.warn(warnMsg) + + continue + + indexRange = getRange(count, plusOne=plusOne) + + for index in indexRange: + query = rootQuery["blind"]["query2"] + if kb.dbms == "Oracle": + query += " WHERE %s" % colQuery + else: + query = query % db + query += " AND %s" % colQuery + query = agent.limitQuery(index, query) + tbl = inject.getValue(query, inband=False) + + if tbl not in dbs[db]: + dbs[db][tbl] = {} + + dbs[db][tbl][column] = None + foundCols[column][db].append(tbl) + + if colConsider == "1": + okDbs = {} + + for db, tableData in dbs.items(): + conf.db = db + okDbs[db] = {} + + for tbl, columns in tableData.items(): + conf.tbl = tbl + + for column in columns: + conf.col = column + + self.getColumns(onlyColNames=True) + + if tbl in okDbs[db]: + okDbs[db][tbl].update(kb.data.cachedColumns[db][tbl]) + else: + okDbs[db][tbl] = kb.data.cachedColumns[db][tbl] + + kb.data.cachedColumns = {} + + dbs = okDbs + + if not dbs: + warnMsg = "no databases have tables containing any of the " + warnMsg += "provided columns" + logger.warn(warnMsg) + return + + dumper.dbColumns(foundCols, colConsider, dbs) + + message = "do you want to dump entries? [Y/n] " + output = readInput(message, default="Y") + + if output not in ("y", "Y"): + return + + dumpFromDbs = [] + message = "which database?\n[a]ll (default)\n" + + for db in dbs: + message += "[%s]\n" % db + + message += "[q]uit" + test = readInput(message, default="a") + + if not test or test[0] in ("a", "A"): + dumpFromDbs = dbs.keys() + + elif test[0] in ("q", "Q"): + return + + else: + dumpFromDbs = test.replace(" ", "").split(",") + + for db, tblData in dbs.items(): + if db not in dumpFromDbs: + continue + + conf.db = db + + for table, columns in tblData.items(): + conf.tbl = table + conf.col = ",".join(column for column in columns) + kb.data.cachedColumns = {} + kb.data.dumpedTable = {} + + data = self.dumpTable() + + if data: + dumper.dbTableValues(data) + + def dumpTable(self): + if not conf.tbl and not conf.col: + errMsg = "missing both table and column parameters, please " + errMsg += "provide at least one of them" + raise sqlmapMissingMandatoryOptionException, errMsg + + if conf.col and not conf.tbl: + self.dumpColumn() + return + if "." in conf.tbl: conf.db, conf.tbl = conf.tbl.split(".") @@ -926,6 +1229,8 @@ class Enumeration: infoMsg += " on database '%s'" % conf.db logger.info(infoMsg) + entriesCount = 0 + if kb.unionPosition: if kb.dbms == "Oracle": query = rootQuery["inband"]["query"] % (colString, conf.tbl.upper()) @@ -934,6 +1239,9 @@ class Enumeration: entries = inject.getValue(query, blind=False) if entries: + if isinstance(entries, str): + entries = [ entries ] + entriesCount = len(entries) index = 0 @@ -974,17 +1282,15 @@ class Enumeration: count = inject.getValue(query, inband=False, expected="int", charsetType=2) if not count.isdigit() or not len(count) or count == "0": - errMsg = "unable to retrieve the number of " + warnMsg = "unable to retrieve the number of " if conf.col: - errMsg += "columns '%s' " % colString - errMsg += "entries for table '%s' " % conf.tbl - errMsg += "on database '%s'" % conf.db + warnMsg += "columns '%s' " % colString + warnMsg += "entries for table '%s' " % conf.tbl + warnMsg += "on database '%s'" % conf.db - if conf.dumpAll: - logger.warn(errMsg) - return None - else: - raise sqlmapNoneDataException, errMsg + logger.warn(warnMsg) + + return None lengths = {} entries = {} @@ -1036,17 +1342,15 @@ class Enumeration: "db": conf.db } else: - errMsg = "unable to retrieve the entries of " + warnMsg = "unable to retrieve the entries of " if conf.col: - errMsg += "columns '%s' " % colString - errMsg += "for table '%s' " % conf.tbl - errMsg += "on database '%s'" % conf.db + warnMsg += "columns '%s' " % colString + warnMsg += "for table '%s' " % conf.tbl + warnMsg += "on database '%s'" % conf.db - if conf.dumpAll: - logger.warn(errMsg) - return None - else: - raise sqlmapNoneDataException, errMsg + logger.warn(warnMsg) + + return None return kb.data.dumpedTable diff --git a/xml/queries.xml b/xml/queries.xml index bcdcfa8d8..af67f33ca 100644 --- a/xml/queries.xml +++ b/xml/queries.xml @@ -51,9 +51,13 @@ - - + + + + + + @@ -102,9 +106,13 @@ - - + + + + + + @@ -161,9 +169,13 @@ - - + + + + + + @@ -214,6 +226,7 @@ +