From 7d0724843f580e2a785e95c396abc34a1d1e2da1 Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Mon, 17 Nov 2008 17:41:02 +0000 Subject: [PATCH] Major enhancement to the engine to parse XML files and matches on DBMS banner and HTTP response headers. Initial web application technology fingerprint (for the moment based only on X-Powered-By HTTP response header and not shown yet to the user). Minor layout adjustments. --- lib/core/common.py | 51 ++++++++++++------- lib/core/option.py | 1 + lib/parse/banner.py | 79 +++++++++++++++-------------- lib/parse/headers.py | 90 +++++++++++++++++++++++++++++----- plugins/dbms/mssqlserver.py | 22 ++++----- plugins/dbms/mysql.py | 21 +++----- plugins/dbms/oracle.py | 18 +++---- plugins/dbms/postgresql.py | 18 +++---- plugins/generic/enumeration.py | 8 +++ xml/banner/generic.xml | 20 ++------ xml/banner/mysql.xml | 3 ++ xml/banner/postgresql.xml | 1 + xml/banner/x-powered-by.xml | 23 +++++++++ 13 files changed, 222 insertions(+), 133 deletions(-) create mode 100644 xml/banner/x-powered-by.xml diff --git a/lib/core/common.py b/lib/core/common.py index 68f127800..b2a862c95 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -130,7 +130,11 @@ def formatDBMSfp(versions=None): return "%s %s" % (kb.dbms, " and ".join([version for version in versions])) -def formatOSfp(info): +def __formatOSfpString(values): + return " or ".join([v for v in values]) + + +def formatOSfp(): """ This function format the back-end operating system fingerprint value and return its values formatted as a human readable string. @@ -142,31 +146,40 @@ def formatOSfp(info): infoStr = "" - # Example of 'info' dictionary: + # Examples of kb.bannerFp dictionary: + # # { - # 'distrib': 'Ubuntu', - # 'release': '8.10', - # 'codename': 'Intrepid', - # 'version': '5.0.67', - # 'type': 'Linux' + # "distrib": set(["2000"]), + # "dbmsVersion": "8.00.194", + # "dbmsRelease": "2000", + # "dbmsServicePack": "0", + # "type": set(["Windows"]) + # } + # + # { + # "distrib": set(["Ubuntu"]), + # "release": set(["8.10"]), + # "codename": set(["Intrepid"]), + # "version": "5.0.67", + # "type": set(["Linux"]) # } - if not info or 'type' not in info: + if not kb.bannerFp or "type" not in kb.bannerFp: return infoStr - elif info['type'] != "None": - infoStr += "back-end DBMS operating system: %s" % info['type'] + else: + infoStr += "back-end DBMS operating system: %s" % __formatOSfpString(kb.bannerFp["type"]) - if 'distrib' in info and info['distrib'] != "None": - infoStr += " %s" % info['distrib'] + if "distrib" in kb.bannerFp: + infoStr += " %s" % __formatOSfpString(kb.bannerFp["distrib"]) - if 'release' in info and info['release'] != "None": - infoStr += " %s" % info['release'] + if "release" in kb.bannerFp: + infoStr += " %s" % __formatOSfpString(kb.bannerFp["release"]) - if 'sp' in info and info['sp'] != "None": - infoStr += " %s" % info['sp'] + if "sp" in kb.bannerFp: + infoStr += " %s" % __formatOSfpString(kb.bannerFp["sp"]) - if 'codename' in info and info['codename'] != "None": - infoStr += " (%s)" % info['codename'] + if "codename" in kb.bannerFp: + infoStr += " (%s)" % __formatOSfpString(kb.bannerFp["codename"]) return infoStr @@ -248,7 +261,7 @@ def getDirectories(): if kb.docRoot: directories.add(kb.docRoot) - pagePath = re.search('^/(.*)/', conf.path) + pagePath = re.search("^/(.*)/", conf.path) if kb.docRoot and pagePath: pagePath = pagePath.groups()[0] diff --git a/lib/core/option.py b/lib/core/option.py index 89b992868..da8099fd0 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -453,6 +453,7 @@ def __setKnowledgeBaseAttributes(): kb.dbms = None kb.dbmsDetected = False kb.dbmsVersion = None + kb.bannerFp = {} kb.headersFp = {} kb.htmlFp = [] kb.injParameter = None diff --git a/lib/parse/banner.py b/lib/parse/banner.py index 71fc1da9b..7b4f0ea87 100644 --- a/lib/parse/banner.py +++ b/lib/parse/banner.py @@ -44,11 +44,24 @@ class BannerHandler(ContentHandler): def __init__(self, banner): self.__banner = sanitizeStr(banner) - self.__regexp = None - self.__match = None - self.__position = None + self.__regexp = None + self.__match = None + self.__version = None - self.info = {} + + def __feedInfo(self, key, value): + value = sanitizeStr(value) + + if value in ( None, "None" ): + return + + if key == "version": + kb.bannerFp[key] = value + else: + if key not in kb.bannerFp.keys(): + kb.bannerFp[key] = set() + + kb.bannerFp[key].add(value) def startElement(self, name, attrs): @@ -57,22 +70,23 @@ class BannerHandler(ContentHandler): 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.__feedInfo("type", attrs.get("type")) + self.__feedInfo("distrib", attrs.get("distrib")) + self.__feedInfo("release", attrs.get("release")) + self.__feedInfo("codename", attrs.get("codename")) + + self.__version = 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.__version.isdigit(): + self.__feedInfo("version", self.__match.group(int(self.__version))) if self.__sp.isdigit(): - self.info['sp'] = "Service Pack %s" % self.__match.group(int(self.__sp)) + self.__feedInfo("sp", "Service Pack %s" % self.__match.group(int(self.__sp))) - self.__match = None - self.__position = None + self.__regexp = None + self.__match = None + self.__version = None class MSSQLBannerHandler(ContentHandler): @@ -90,7 +104,14 @@ class MSSQLBannerHandler(ContentHandler): self.__version = "" self.__servicePack = "" - self.info = {} + + def __feedInfo(self, key, value): + value = sanitizeStr(value) + + if value in ( None, "None" ): + return + + kb.bannerFp[key] = value def startElement(self, name, attrs): @@ -114,9 +135,9 @@ class MSSQLBannerHandler(ContentHandler): def endElement(self, name): if name == "signature": if re.search(" %s[\.\ ]+" % self.__version, self.__banner): - self.info['dbmsRelease'] = self.__release - self.info['dbmsVersion'] = self.__version - self.info['dbmsServicePack'] = self.__servicePack + self.__feedInfo("dbmsRelease", self.__release) + self.__feedInfo("dbmsVersion", self.__version) + self.__feedInfo("dbmsServicePack", self.__servicePack) self.__version = "" self.__servicePack = "" @@ -137,9 +158,6 @@ def bannerParser(banner): DBMS banner based upon the data in XML file """ - banner = sanitizeStr(banner) - info = {} - if kb.dbms == "Microsoft SQL Server": xmlfile = paths.MSSQL_XML elif kb.dbms == "MySQL": @@ -154,24 +172,9 @@ def bannerParser(banner): 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 + parse(paths.GENERIC_XML, handler) diff --git a/lib/parse/headers.py b/lib/parse/headers.py index 00fa2dae8..f3c8dc25c 100644 --- a/lib/parse/headers.py +++ b/lib/parse/headers.py @@ -26,10 +26,68 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import re +from xml.sax import parse +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 -from lib.parse.banner import BannerHandler + + +class HeadersHandler(ContentHandler): + """ + This class defines methods to parse and extract information from + the given HTTP header based upon the data in XML file + """ + + def __init__(self, header): + self.__header = sanitizeStr(header) + + self.__regexp = None + self.__match = None + self.__techVersion = None + + + def __feedInfo(self, key, value): + value = sanitizeStr(value) + + if value in ( None, "None" ): + return + + if key == "techVersion": + kb.headersFp[key] = value + else: + if key not in kb.headersFp.keys(): + kb.headersFp[key] = set() + + kb.headersFp[key].add(value) + + + def startElement(self, name, attrs): + if name == "regexp": + self.__regexp = sanitizeStr(attrs.get("value")) + self.__match = re.search(self.__regexp, self.__header, re.I | re.M) + + if name == "info" and self.__match: + self.__feedInfo("type", attrs.get("type")) + self.__feedInfo("distrib", attrs.get("distrib")) + self.__feedInfo("release", attrs.get("release")) + self.__feedInfo("codename", attrs.get("codename")) + self.__feedInfo("technology", attrs.get("codename")) + + self.__techVersion = sanitizeStr(attrs.get("tech_version")) + self.__sp = sanitizeStr(attrs.get("sp")) + + if self.__techVersion.isdigit(): + self.__feedInfo("techVersion", self.__match.group(int(self.__techVersion))) + + if self.__sp.isdigit(): + self.__feedInfo("sp", "Service Pack %s" % self.__match.group(int(self.__sp))) + + self.__regexp = None + self.__match = None + self.__techVersion = None def headersParser(headers): @@ -39,17 +97,23 @@ def headersParser(headers): and the web application technology """ - topHeaders = ( - "cookie", - "microsoftsharepointteamservices", - "server", - "servlet-engine", - "www-authenticate", - "x-aspnet-version", - "x-powered-by", - ) + # TODO: ahead here + topHeaders = { + #"cookie": "%s/cookie.xml" % paths.SQLMAP_XML_BANNER_PATH, + #"microsoftsharepointteamservices": "%s/microsoftsharepointteamservices.xml" % paths.SQLMAP_XML_BANNER_PATH, + #"server": "%s/server.xml" % paths.SQLMAP_XML_BANNER_PATH, + #"servlet-engine": "%s/servlet-engine.xml" % paths.SQLMAP_XML_BANNER_PATH, + #"set-cookie": "%s/cookie.xml" % paths.SQLMAP_XML_BANNER_PATH, + #"www-authenticate": "%s/www-authenticate.xml" % paths.SQLMAP_XML_BANNER_PATH, + #"x-aspnet-version": "%s/x-aspnet-version.xml" % paths.SQLMAP_XML_BANNER_PATH, + "x-powered-by": "%s/x-powered-by.xml" % paths.SQLMAP_XML_BANNER_PATH, + } for header in headers: - if header in topHeaders: - # TODO: fill me - pass + if header in topHeaders.keys(): + value = headers[header] + xmlfile = topHeaders[header] + checkFile(xmlfile) + handler = HeadersHandler(value) + parse(xmlfile, handler) + parse(paths.GENERIC_XML, handler) diff --git a/plugins/dbms/mssqlserver.py b/plugins/dbms/mssqlserver.py index 9be2dab7c..32deb7cad 100644 --- a/plugins/dbms/mssqlserver.py +++ b/plugins/dbms/mssqlserver.py @@ -124,15 +124,13 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): value = "" - info = None formatInfo = None if self.banner: - info = bannerParser(self.banner) - formatInfo = formatOSfp(info) + formatInfo = formatOSfp() - if formatInfo: - value += "%s\n" % formatInfo + if formatInfo: + value += "%s\n" % formatInfo value += "back-end DBMS: " actVer = formatDBMSfp() @@ -145,10 +143,10 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): formatInfo = None value += "active fingerprint: %s" % actVer - if info: - release = info["dbmsRelease"] - version = info["dbmsVersion"] - servicepack = info["dbmsServicePack"] + if kb.bannerFp: + release = kb.bannerFp["dbmsRelease"] + version = kb.bannerFp["dbmsVersion"] + servicepack = kb.bannerFp["dbmsServicePack"] if release and version and servicepack: banVer = "Microsoft SQL Server %s " % release @@ -169,8 +167,7 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): if conf.dbms in MSSQL_ALIASES and kb.dbmsVersion and kb.dbmsVersion[0].isdigit(): setDbms("Microsoft SQL Server %s" % kb.dbmsVersion[0]) - if conf.getBanner: - self.banner = inject.getValue("@@VERSION") + self.getPrematureBanner("@@VERSION") if not conf.extensiveFp: return True @@ -197,8 +194,7 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): else: setDbms("Microsoft SQL Server") - if conf.getBanner: - self.banner = inject.getValue("@@VERSION") + self.getPrematureBanner("@@VERSION") return True else: diff --git a/plugins/dbms/mysql.py b/plugins/dbms/mysql.py index 05752ef96..768f9f0d4 100644 --- a/plugins/dbms/mysql.py +++ b/plugins/dbms/mysql.py @@ -182,15 +182,13 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): value = "" - info = None formatInfo = None if self.banner: - info = bannerParser(self.banner) - formatInfo = formatOSfp(info) + formatInfo = formatOSfp() - if formatInfo: - value += "%s\n" % formatInfo + if formatInfo: + value += "%s\n" % formatInfo value += "back-end DBMS: " actVer = formatDBMSfp() @@ -208,9 +206,9 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): comVer = formatDBMSfp([comVer]) value += "\n%scomment injection fingerprint: %s" % (blank, comVer) - if info: + if kb.bannerFp: # TODO: move to the XML banner file - banVer = info['version'] + banVer = kb.bannerFp['version'] if re.search("-log$", self.banner): banVer += ", logging enabled" @@ -241,8 +239,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): if int(kb.dbmsVersion[0]) >= 5: self.has_information_schema = True - if conf.getBanner: - self.banner = inject.getValue("VERSION()") + self.getPrematureBanner("VERSION()") if not conf.extensiveFp: return True @@ -270,8 +267,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): setDbms("MySQL 5") self.has_information_schema = True - if conf.getBanner: - self.banner = inject.getValue("VERSION()") + self.getPrematureBanner("VERSION()") if not conf.extensiveFp: kb.dbmsVersion = [">= 5.0.0"] @@ -318,8 +314,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): setDbms("MySQL 4") kb.dbmsVersion = ["< 5.0.0"] - if conf.getBanner: - self.banner = inject.getValue("VERSION()") + self.getPrematureBanner("VERSION()") if not conf.extensiveFp: return True diff --git a/plugins/dbms/oracle.py b/plugins/dbms/oracle.py index b72ba78c4..4a78a19a4 100644 --- a/plugins/dbms/oracle.py +++ b/plugins/dbms/oracle.py @@ -118,15 +118,13 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): value = "" - info = None formatInfo = None if self.banner: - info = bannerParser(self.banner) - formatInfo = formatOSfp(info) + formatInfo = formatOSfp() - if formatInfo: - value += "%s\n" % formatInfo + if formatInfo: + value += "%s\n" % formatInfo value += "back-end DBMS: " @@ -139,8 +137,8 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): formatInfo = None value += "active fingerprint: %s" % actVer - if info: - banVer = info['version'] + if kb.bannerFp: + banVer = kb.bannerFp['version'] banVer = formatDBMSfp([banVer]) value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) @@ -156,8 +154,7 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): if conf.dbms in ORACLE_ALIASES: setDbms("Oracle") - if conf.getBanner: - self.banner = inject.getValue("SELECT banner FROM v$version WHERE ROWNUM=1") + self.getPrematureBanner("SELECT banner FROM v$version WHERE ROWNUM=1") if not conf.extensiveFp: return True @@ -183,8 +180,7 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): setDbms("Oracle") - if conf.getBanner: - self.banner = inject.getValue("SELECT banner FROM v$version WHERE ROWNUM=1") + self.getPrematureBanner("SELECT banner FROM v$version WHERE ROWNUM=1") if not conf.extensiveFp: return True diff --git a/plugins/dbms/postgresql.py b/plugins/dbms/postgresql.py index bc24774b0..0155ff0b9 100644 --- a/plugins/dbms/postgresql.py +++ b/plugins/dbms/postgresql.py @@ -118,15 +118,13 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): value = "" - info = None formatInfo = None if self.banner: - info = bannerParser(self.banner) - formatInfo = formatOSfp(info) + formatInfo = formatOSfp() - if formatInfo: - value += "%s\n" % formatInfo + if formatInfo: + value += "%s\n" % formatInfo value += "back-end DBMS: " @@ -139,8 +137,8 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): formatInfo = None value += "active fingerprint: %s" % actVer - if info: - banVer = info['version'] + if kb.bannerFp: + banVer = kb.bannerFp['version'] banVer = formatDBMSfp([banVer]) value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) @@ -160,8 +158,7 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): if conf.dbms in PGSQL_ALIASES: setDbms("PostgreSQL") - if conf.getBanner: - self.banner = inject.getValue("VERSION()") + self.getPrematureBanner("VERSION()") if not conf.extensiveFp: return True @@ -186,8 +183,7 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): setDbms("PostgreSQL") - if conf.getBanner: - self.banner = inject.getValue("VERSION()") + self.getPrematureBanner("VERSION()") if not conf.extensiveFp: return True diff --git a/plugins/generic/enumeration.py b/plugins/generic/enumeration.py index fe903e089..91f75756d 100644 --- a/plugins/generic/enumeration.py +++ b/plugins/generic/enumeration.py @@ -41,6 +41,7 @@ from lib.core.exception import sqlmapUndefinedMethod from lib.core.exception import sqlmapUnsupportedFeatureException 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 @@ -72,6 +73,13 @@ class Enumeration: pass + def getPrematureBanner(self, query): + if conf.getBanner: + self.banner = inject.getValue(query) + + bannerParser(self.banner) + + def getBanner(self): infoMsg = "fetching banner" logger.info(infoMsg) diff --git a/xml/banner/generic.xml b/xml/banner/generic.xml index 4e06fc708..950d9715d 100644 --- a/xml/banner/generic.xml +++ b/xml/banner/generic.xml @@ -6,37 +6,26 @@ - - + + - - + + - - - - - - - - - - - @@ -83,6 +72,7 @@ + diff --git a/xml/banner/mysql.xml b/xml/banner/mysql.xml index 1c21ec89c..1d806f963 100644 --- a/xml/banner/mysql.xml +++ b/xml/banner/mysql.xml @@ -6,11 +6,13 @@ + + @@ -36,6 +38,7 @@ + diff --git a/xml/banner/postgresql.xml b/xml/banner/postgresql.xml index 79cde445b..aad23bfee 100644 --- a/xml/banner/postgresql.xml +++ b/xml/banner/postgresql.xml @@ -6,6 +6,7 @@ + diff --git a/xml/banner/x-powered-by.xml b/xml/banner/x-powered-by.xml new file mode 100644 index 000000000..d6d5e6a69 --- /dev/null +++ b/xml/banner/x-powered-by.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + +