From fa0507ab39a7a82cafa7f5aa8f5f8397f26a4c0d Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Sat, 15 Nov 2008 23:41:31 +0000 Subject: [PATCH] Minor enhancement to fingerprint the back-end DBMS operating system (type, version, release, distribution, codename and service pack) by parsing the DBMS banner value when both -f and -b are provided: adapted the code and added XML files defining regular expressions for matching. Example of the -f -b output now on MySQL 5.0.67 running on latest Ubuntu: --8<-- back-end DBMS: active fingerprint: MySQL >= 5.0.38 and < 5.1.2 comment injection fingerprint: MySQL 5.0.67 banner parsing fingerprint: MySQL 5.0.67 html error message fingerprint: MySQL back-end DBMS operating system: Linux Ubuntu 8.10 (Intrepid) --8<-- --- doc/ChangeLog | 9 ++-- lib/controller/handler.py | 2 +- lib/core/common.py | 72 ++++++++++++++++++++++----- lib/parse/banner.py | 99 ++++++++++++++++++++++++++++++++----- lib/parse/cmdline.py | 2 +- plugins/dbms/mssqlserver.py | 20 +++++--- plugins/dbms/mysql.py | 27 ++++++---- plugins/dbms/oracle.py | 35 +++++++------ plugins/dbms/postgresql.py | 21 +++++--- xml/banner/generic.xml | 86 ++++++++++++++++++++++++++++++++ xml/{ => banner}/mssql.xml | 0 xml/banner/mysql.xml | 43 ++++++++++++++++ xml/banner/oracle.xml | 8 +++ xml/banner/postgresql.xml | 13 +++++ xml/queries.xml | 4 ++ 15 files changed, 372 insertions(+), 69 deletions(-) create mode 100644 xml/banner/generic.xml rename xml/{ => banner}/mssql.xml (100%) create mode 100644 xml/banner/mysql.xml create mode 100644 xml/banner/oracle.xml create mode 100644 xml/banner/postgresql.xml diff --git a/doc/ChangeLog b/doc/ChangeLog index 827b7d7ed..2420943dc 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,14 +1,17 @@ sqlmap (0.6.3-1) stable; urgency=low + * Major bug fix to correctly handle httplib.BadStatusLine exception; * Minor enhancement to support stacked queries which will be used sometimes by takeover functionality and time based blind SQL injection technique; * Minor enhancement to be able to specify the number of seconds to wait between each HTTP request; * Minor enhancement to be able to enumerate table columns and dump table - entries also if the database name is not provided by using the current - database on MySQL and MSSQL, the 'public' scheme on PostgreSQL and the - 'USERS' TABLESPACE_NAME on Oracle; + entries, also when the database name is not provided, by using the + current database on MySQL and Microsoft SQL Server, the 'public' + scheme on PostgreSQL and the 'USERS' TABLESPACE_NAME on Oracle; + * Minor improvement to set by default in all HTTP requests the standard + HTTP headers (Accept, Accept-Encoding, etc); * Minor improvements to sqlmap Debian package files: sqlmap uploaded to official Debian project repository; * Minor bug fix to handle session.error and session.timeout in HTTP diff --git a/lib/controller/handler.py b/lib/controller/handler.py index 25d7dea1c..5ee86fa25 100644 --- a/lib/controller/handler.py +++ b/lib/controller/handler.py @@ -55,7 +55,7 @@ def setHandler(): for dbmsAliases, dbmsEntry in dbmsMap: if conf.dbms and conf.dbms not in dbmsAliases: - debugMsg = "skipping to test for %s" % dbmsNames[count] + debugMsg = "skipping test for %s" % dbmsNames[count] logger.debug(debugMsg) count += 1 continue diff --git a/lib/core/common.py b/lib/core/common.py index 736b20ea8..82f2c5dfb 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -112,7 +112,7 @@ def paramToDict(place, parameters=None): return testableParameters -def formatFingerprint(versions=None): +def formatDBMSfp(versions=None): """ This function format the back-end DBMS fingerprint value and return its values formatted as a human readable string. @@ -130,6 +130,47 @@ def formatFingerprint(versions=None): return "%s %s" % (kb.dbms, " and ".join([version for version in versions])) +def formatOSfp(info): + """ + This function format the back-end operating system fingerprint value + and return its values formatted as a human readable string. + + @return: detected back-end operating system based upon fingerprint + techniques. + @rtype: C{str} + """ + + infoStr = "" + + # Example of 'info' dictionary: + # { + # 'distrib': 'Ubuntu', + # 'release': '8.10', + # 'codename': 'Intrepid', + # 'version': '5.0.67', + # 'type': 'Linux' + # } + + if not info or 'type' not in info: + return infoStr + elif info['type'] != "None": + infoStr += "back-end DBMS operating system: %s" % info['type'] + + if 'distrib' in info and info['distrib'] != "None": + infoStr += " %s" % info['distrib'] + + if 'release' in info and info['release'] != "None": + infoStr += " %s" % info['release'] + + if 'sp' in info and info['sp'] != "None": + infoStr += " %s" % info['sp'] + + if 'codename' in info and info['codename'] != "None": + infoStr += " (%s)" % info['codename'] + + return infoStr + + def getHtmlErrorFp(): """ This function parses the knowledge base htmlFp list and return its @@ -442,20 +483,25 @@ def cleanQuery(query): def setPaths(): # sqlmap paths - paths.SQLMAP_SHELL_PATH = "%s/shell" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_TXT_PATH = "%s/txt" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_XML_PATH = "%s/xml" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_OUTPUT_PATH = "%s/output" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_DUMP_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/dump" - paths.SQLMAP_FILES_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/files" + paths.SQLMAP_SHELL_PATH = "%s/shell" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_TXT_PATH = "%s/txt" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_XML_PATH = "%s/xml" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_XML_BANNER_PATH = "%s/banner" % paths.SQLMAP_XML_PATH + paths.SQLMAP_OUTPUT_PATH = "%s/output" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_DUMP_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/dump" + paths.SQLMAP_FILES_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/files" # sqlmap files - paths.SQLMAP_HISTORY = "%s/.sqlmap_history" % paths.SQLMAP_ROOT_PATH - paths.SQLMAP_CONFIG = "%s/sqlmap-%s.conf" % (paths.SQLMAP_ROOT_PATH, randomStr()) - paths.FUZZ_VECTORS = "%s/fuzz_vectors.txt" % paths.SQLMAP_TXT_PATH - paths.ERRORS_XML = "%s/errors.xml" % paths.SQLMAP_XML_PATH - paths.MSSQL_XML = "%s/mssql.xml" % paths.SQLMAP_XML_PATH - paths.QUERIES_XML = "%s/queries.xml" % paths.SQLMAP_XML_PATH + paths.SQLMAP_HISTORY = "%s/.sqlmap_history" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_CONFIG = "%s/sqlmap-%s.conf" % (paths.SQLMAP_ROOT_PATH, randomStr()) + paths.FUZZ_VECTORS = "%s/fuzz_vectors.txt" % paths.SQLMAP_TXT_PATH + paths.ERRORS_XML = "%s/errors.xml" % paths.SQLMAP_XML_PATH + paths.QUERIES_XML = "%s/queries.xml" % paths.SQLMAP_XML_PATH + paths.GENERIC_XML = "%s/generic.xml" % paths.SQLMAP_XML_BANNER_PATH + paths.MSSQL_XML = "%s/mssql.xml" % paths.SQLMAP_XML_BANNER_PATH + paths.MYSQL_XML = "%s/mysql.xml" % paths.SQLMAP_XML_BANNER_PATH + paths.ORACLE_XML = "%s/oracle.xml" % paths.SQLMAP_XML_BANNER_PATH + paths.PGSQL_XML = "%s/postgresql.xml" % paths.SQLMAP_XML_BANNER_PATH def weAreFrozen(): diff --git a/lib/parse/banner.py b/lib/parse/banner.py index 04b7dcef8..71fc1da9b 100644 --- a/lib/parse/banner.py +++ b/lib/parse/banner.py @@ -31,25 +31,67 @@ from xml.sax.handler import ContentHandler from lib.core.common import checkFile from lib.core.common import sanitizeStr +from lib.core.data import kb +from lib.core.data import paths -class bannerHandler(ContentHandler): +class BannerHandler(ContentHandler): """ This class defines methods to parse and extract information from the given DBMS banner based upon the data in XML file """ + def __init__(self, banner): + self.__banner = sanitizeStr(banner) + + self.__regexp = None + self.__match = None + self.__position = None + + self.info = {} + + + def startElement(self, name, attrs): + if name == "regexp": + self.__regexp = sanitizeStr(attrs.get("value")) + self.__match = re.search(self.__regexp, self.__banner, re.I | re.M) + + if name == "info" and self.__match: + self.__position = sanitizeStr(attrs.get("version")) + self.__sp = sanitizeStr(attrs.get("sp")) + + self.info['type'] = sanitizeStr(attrs.get("type")) + self.info['distrib'] = sanitizeStr(attrs.get("distrib")) + self.info['release'] = sanitizeStr(attrs.get("release")) + self.info['codename'] = sanitizeStr(attrs.get("codename")) + + if self.__position.isdigit(): + self.info['version'] = self.__match.group(int(self.__position)) + + if self.__sp.isdigit(): + self.info['sp'] = "Service Pack %s" % self.__match.group(int(self.__sp)) + + self.__match = None + self.__position = None + + +class MSSQLBannerHandler(ContentHandler): + """ + This class defines methods to parse and extract information from the + given Microsoft SQL Server banner based upon the data in XML file + """ + def __init__(self, banner): self.__banner = sanitizeStr(banner) - self.release = None - self.version = None - self.servicePack = None + self.__inVersion = False self.__inServicePack = False self.__release = None self.__version = "" self.__servicePack = "" + self.info = {} + def startElement(self, name, attrs): if name == "signatures": @@ -72,9 +114,9 @@ class bannerHandler(ContentHandler): def endElement(self, name): if name == "signature": if re.search(" %s[\.\ ]+" % self.__version, self.__banner): - self.release = self.__release - self.version = self.__version - self.servicePack = self.__servicePack + self.info['dbmsRelease'] = self.__release + self.info['dbmsVersion'] = self.__version + self.info['dbmsServicePack'] = self.__servicePack self.__version = "" self.__servicePack = "" @@ -89,16 +131,47 @@ class bannerHandler(ContentHandler): self.__servicePack = self.__servicePack.replace(" ", "") - -def bannerParser(banner, xmlfile): +def bannerParser(banner): """ This function calls a class to extract information from the given DBMS banner based upon the data in XML file """ - checkFile(xmlfile) banner = sanitizeStr(banner) - handler = bannerHandler(banner) - parse(xmlfile, handler) + info = {} - return handler.release, handler.version, handler.servicePack + if kb.dbms == "Microsoft SQL Server": + xmlfile = paths.MSSQL_XML + elif kb.dbms == "MySQL": + xmlfile = paths.MYSQL_XML + elif kb.dbms == "Oracle": + xmlfile = paths.ORACLE_XML + elif kb.dbms == "PostgreSQL": + xmlfile = paths.PGSQL_XML + + checkFile(xmlfile) + + if kb.dbms == "Microsoft SQL Server": + handler = MSSQLBannerHandler(banner) + parse(xmlfile, handler) + info = handler.info + + handler = BannerHandler(banner) + parse(paths.GENERIC_XML, handler) + + for title, value in handler.info.items(): + info[title] = value + else: + handler = BannerHandler(banner) + parse(xmlfile, handler) + info = handler.info + + if "type" not in info or info["type"] == "None": + parse(paths.GENERIC_XML, handler) + info["type"] = handler.info["type"] + + if "distrib" not in info or info["distrib"] == "None": + parse(paths.GENERIC_XML, handler) + info["distrib"] = handler.info["distrib"] + + return info diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index b759efac9..24b4120c7 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -129,7 +129,7 @@ def cmdLineParser(): fingerprint.add_option("-f", "--fingerprint", dest="extensiveFp", action="store_true", - help="Perform an extensive database fingerprint") + help="Perform an extensive DBMS version fingerprint") # Enumeration options enumeration = OptionGroup(parser, "Enumeration", "These options can " diff --git a/plugins/dbms/mssqlserver.py b/plugins/dbms/mssqlserver.py index 524c2efb8..cff2cfd88 100644 --- a/plugins/dbms/mssqlserver.py +++ b/plugins/dbms/mssqlserver.py @@ -28,14 +28,14 @@ import time from lib.core.agent import agent from lib.core.common import dataToStdout -from lib.core.common import formatFingerprint +from lib.core.common import formatDBMSfp +from lib.core.common import formatOSfp from lib.core.common import getHtmlErrorFp from lib.core.common import randomInt 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 paths from lib.core.data import queries from lib.core.exception import sqlmapNoneDataException from lib.core.exception import sqlmapSyntaxException @@ -124,16 +124,21 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): - actVer = formatFingerprint() + actVer = formatDBMSfp() if not conf.extensiveFp: return actVer - blank = " " * 16 - value = "active fingerprint: %s" % actVer + blank = " " * 16 + formatInfo = None + value = "active fingerprint: %s" % actVer if self.banner: - release, version, servicepack = bannerParser(self.banner, paths.MSSQL_XML) + info = bannerParser(self.banner) + release = info["dbmsRelease"] + version = info["dbmsVersion"] + servicepack = info["dbmsServicePack"] + formatInfo = formatOSfp(info) if release and version and servicepack: banVer = "Microsoft SQL Server %s " % release @@ -148,6 +153,9 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): if htmlParsed: value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + if formatInfo: + value += "\n%s" % formatInfo + return value diff --git a/plugins/dbms/mysql.py b/plugins/dbms/mysql.py index ecfa1fa54..004b100eb 100644 --- a/plugins/dbms/mysql.py +++ b/plugins/dbms/mysql.py @@ -28,7 +28,8 @@ import re from lib.core.agent import agent from lib.core.common import fileToStr -from lib.core.common import formatFingerprint +from lib.core.common import formatDBMSfp +from lib.core.common import formatOSfp from lib.core.common import getDirectories from lib.core.common import getHtmlErrorFp from lib.core.common import randomInt @@ -43,6 +44,7 @@ from lib.core.settings import MYSQL_ALIASES from lib.core.settings import MYSQL_SYSTEM_DBS from lib.core.shell import autoCompletion from lib.core.unescaper import unescaper +from lib.parse.banner import bannerParser from lib.request import inject from lib.request.connect import Connect as Request #from lib.utils.fuzzer import passiveFuzzing @@ -180,26 +182,28 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): - actVer = formatFingerprint() + actVer = formatDBMSfp() if not conf.extensiveFp: return actVer - blank = " " * 16 - value = "active fingerprint: %s" % actVer - comVer = self.__commentCheck() + comVer = self.__commentCheck() + blank = " " * 16 + formatInfo = None + value = "active fingerprint: %s" % actVer if comVer: - comVer = formatFingerprint([comVer]) + comVer = formatDBMSfp([comVer]) value += "\n%scomment injection fingerprint: %s" % (blank, comVer) if self.banner: - banVer = re.search("^([\d\.]+)", self.banner) - banVer = banVer.groups()[0] + info = bannerParser(self.banner) + formatInfo = formatOSfp(info) + + banVer = info['version'] if re.search("-log$", self.banner): banVer += ", logging enabled" - banVer = formatFingerprint([banVer]) - + banVer = formatDBMSfp([banVer]) value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) #passiveFuzzing() @@ -208,6 +212,9 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): if htmlParsed: value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + if formatInfo: + value += "\n%s" % formatInfo + return value diff --git a/plugins/dbms/oracle.py b/plugins/dbms/oracle.py index fc134af0f..4922f606d 100644 --- a/plugins/dbms/oracle.py +++ b/plugins/dbms/oracle.py @@ -26,7 +26,8 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import re -from lib.core.common import formatFingerprint +from lib.core.common import formatDBMSfp +from lib.core.common import formatOSfp from lib.core.common import getHtmlErrorFp from lib.core.data import conf from lib.core.data import kb @@ -36,6 +37,7 @@ from lib.core.session import setDbms from lib.core.settings import ORACLE_ALIASES from lib.core.settings import ORACLE_SYSTEM_DBS from lib.core.unescaper import unescaper +from lib.parse.banner import bannerParser from lib.request import inject #from lib.utils.fuzzer import passiveFuzzing @@ -119,19 +121,19 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): if not conf.extensiveFp: return "Oracle" - actVer = formatFingerprint() + actVer = formatDBMSfp() - blank = " " * 16 - value = "active fingerprint: %s" % actVer + blank = " " * 16 + formatInfo = None + value = "active fingerprint: %s" % actVer if self.banner: - banVer = re.search("^Oracle .*Release ([\d\.]+) ", self.banner) + info = bannerParser(self.banner) + formatInfo = formatOSfp(info) - if banVer: - banVer = banVer.groups()[0] - banVer = formatFingerprint([banVer]) - - value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + banVer = info['version'] + banVer = formatDBMSfp([banVer]) + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) #passiveFuzzing() htmlParsed = getHtmlErrorFp() @@ -139,6 +141,9 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): if htmlParsed: value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + if formatInfo: + value += "\n%s" % formatInfo + return value @@ -159,7 +164,7 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): logMsg = "confirming Oracle" logger.info(logMsg) - query = "SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1" + query = "SELECT SUBSTR((VERSION), 1, 2) FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1" version = inject.getValue(query) if not version: @@ -173,13 +178,13 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): if not conf.extensiveFp: return True - if re.search("^11\.", version): + if re.search("^11", version): kb.dbmsVersion = ["11i"] - elif re.search("^10\.", version): + elif re.search("^10", version): kb.dbmsVersion = ["10g"] - elif re.search("^9\.", version): + elif re.search("^9", version): kb.dbmsVersion = ["9i"] - elif re.search("^8\.", version): + elif re.search("^8", version): kb.dbmsVersion = ["8i"] if conf.getBanner: diff --git a/plugins/dbms/postgresql.py b/plugins/dbms/postgresql.py index ca1ad85f0..dfe1a11be 100644 --- a/plugins/dbms/postgresql.py +++ b/plugins/dbms/postgresql.py @@ -26,7 +26,8 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import re -from lib.core.common import formatFingerprint +from lib.core.common import formatDBMSfp +from lib.core.common import formatOSfp from lib.core.common import getHtmlErrorFp from lib.core.common import randomInt from lib.core.data import conf @@ -37,6 +38,7 @@ from lib.core.session import setDbms from lib.core.settings import PGSQL_ALIASES from lib.core.settings import PGSQL_SYSTEM_DBS from lib.core.unescaper import unescaper +from lib.parse.banner import bannerParser from lib.request import inject #from lib.utils.fuzzer import passiveFuzzing @@ -119,16 +121,18 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): if not conf.extensiveFp: return "PostgreSQL" - actVer = formatFingerprint() + actVer = formatDBMSfp() - blank = " " * 16 - value = "active fingerprint: %s" % actVer + blank = " " * 16 + formatInfo = None + value = "active fingerprint: %s" % actVer if self.banner: - banVer = re.search("^PostgreSQL ([\d\.]+)", self.banner) - banVer = banVer.groups()[0] - banVer = formatFingerprint([banVer]) + info = bannerParser(self.banner) + formatInfo = formatOSfp(info) + banVer = info['version'] + banVer = formatDBMSfp([banVer]) value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) #passiveFuzzing() @@ -137,6 +141,9 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): if htmlParsed: value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + if formatInfo: + value += "\n%s" % formatInfo + return value diff --git a/xml/banner/generic.xml b/xml/banner/generic.xml new file mode 100644 index 000000000..afc762df9 --- /dev/null +++ b/xml/banner/generic.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xml/mssql.xml b/xml/banner/mssql.xml similarity index 100% rename from xml/mssql.xml rename to xml/banner/mssql.xml diff --git a/xml/banner/mysql.xml b/xml/banner/mysql.xml new file mode 100644 index 000000000..1c21ec89c --- /dev/null +++ b/xml/banner/mysql.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xml/banner/oracle.xml b/xml/banner/oracle.xml new file mode 100644 index 000000000..ad8bc36d9 --- /dev/null +++ b/xml/banner/oracle.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/xml/banner/postgresql.xml b/xml/banner/postgresql.xml new file mode 100644 index 000000000..79cde445b --- /dev/null +++ b/xml/banner/postgresql.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/xml/queries.xml b/xml/queries.xml index 14d766a8c..84d7ba66f 100644 --- a/xml/queries.xml +++ b/xml/queries.xml @@ -75,6 +75,10 @@ +