From 727664aea72f8ac4df8ea7646730d1ebe35b4895 Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Tue, 18 Nov 2008 17:42:46 +0000 Subject: [PATCH] Minor enhancement to fingerprint the web server operating system and the web application technology by parsing also HTTP response Server header. Refactor libraries and plugins that parses XML to fingerprint and show on standard output the information. Updated changelog. --- doc/ChangeLog | 14 +- lib/core/common.py | 66 ++++--- lib/core/option.py | 1 + lib/parse/banner.py | 60 +----- lib/parse/handler.py | 95 ++++++++++ lib/parse/headers.py | 67 +------ plugins/dbms/mssqlserver.py | 17 +- plugins/dbms/mysql.py | 19 +- plugins/dbms/oracle.py | 19 +- plugins/dbms/postgresql.py | 19 +- xml/banner/generic.xml | 18 +- xml/banner/mysql.xml | 22 +-- xml/banner/oracle.xml | 3 +- xml/banner/postgresql.xml | 6 +- xml/banner/server.xml | 369 ++++++++++++++++++++++++++++++++++++ 15 files changed, 588 insertions(+), 207 deletions(-) create mode 100644 lib/parse/handler.py create mode 100644 xml/banner/server.xml diff --git a/doc/ChangeLog b/doc/ChangeLog index 4ecc84a71..704593cbe 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,11 +1,14 @@ 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; + * Major enhancement to support stacked queries when the web application + supports it which will be used in the long run by takeover + functionality; + * Minor enhancement to test if the injectable parameter is affected by + a time based blind SQL injection technique; + * Minor enhancement to fingerprint the web server operating system and + the web application technology by parsing some HTTP response headers; * Minor enhancement to fingerprint the back-end DBMS operating system by - parsing the DBMS banner value when both -f and -b are provided; + parsing the DBMS banner value when -b option is provided; * Minor enhancement to be able to specify the number of seconds to wait between each HTTP request providing option --delay #; * Minor enhancement to be able to enumerate table columns and dump table @@ -16,6 +19,7 @@ sqlmap (0.6.3-1) stable; urgency=low HTTP headers (Accept, Accept-Encoding, etc); * Minor improvements to sqlmap Debian package files: sqlmap uploaded to official Debian project repository; + * Major bug fix to correctly handle httplib.BadStatusLine exception; * Minor bug fix to handle session.error and session.timeout in HTTP requests; * Minor bug fix so that when the user provide a SELECT statement to be diff --git a/lib/core/common.py b/lib/core/common.py index b2a862c95..48c0e1b4b 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -130,15 +130,34 @@ def formatDBMSfp(versions=None): return "%s %s" % (kb.dbms, " and ".join([version for version in versions])) -def __formatOSfpString(values): - return " or ".join([v for v in values]) +def __formatFingerprintString(values, chain="or"): + string = "|".join([v for v in values]) + return string.replace("|", " %s " % chain) -def formatOSfp(): +def formatFingerprint(target, info): """ This function format the back-end operating system fingerprint value and return its values formatted as a human readable string. + Examples of info dictionary: + + { + "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"]) + } + @return: detected back-end operating system based upon fingerprint techniques. @rtype: C{str} @@ -146,40 +165,25 @@ def formatOSfp(): infoStr = "" - # Examples of kb.bannerFp dictionary: - # - # { - # "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 kb.bannerFp or "type" not in kb.bannerFp: + if not info or "type" not in info: return infoStr else: - infoStr += "back-end DBMS operating system: %s" % __formatOSfpString(kb.bannerFp["type"]) + infoStr += "%s operating system: %s" % (target, __formatFingerprintString(info["type"])) - if "distrib" in kb.bannerFp: - infoStr += " %s" % __formatOSfpString(kb.bannerFp["distrib"]) + if "distrib" in info: + infoStr += " %s" % __formatFingerprintString(info["distrib"]) - if "release" in kb.bannerFp: - infoStr += " %s" % __formatOSfpString(kb.bannerFp["release"]) + if "release" in info: + infoStr += " %s" % __formatFingerprintString(info["release"]) - if "sp" in kb.bannerFp: - infoStr += " %s" % __formatOSfpString(kb.bannerFp["sp"]) + if "sp" in info: + infoStr += " %s" % __formatFingerprintString(info["sp"]) - if "codename" in kb.bannerFp: - infoStr += " (%s)" % __formatOSfpString(kb.bannerFp["codename"]) + if "codename" in info: + infoStr += " (%s)" % __formatFingerprintString(info["codename"]) + + if "technology" in info: + infoStr += "\nweb application technology: %s" % __formatFingerprintString(info["technology"], "and") return infoStr diff --git a/lib/core/option.py b/lib/core/option.py index da8099fd0..44114cd3f 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -454,6 +454,7 @@ def __setKnowledgeBaseAttributes(): kb.dbmsDetected = False kb.dbmsVersion = None kb.bannerFp = {} + kb.headersCount = 0 kb.headersFp = {} kb.htmlFp = [] kb.injParameter = None diff --git a/lib/parse/banner.py b/lib/parse/banner.py index 7b4f0ea87..57afe1669 100644 --- a/lib/parse/banner.py +++ b/lib/parse/banner.py @@ -33,60 +33,7 @@ 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): - """ - 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.__version = None - - - 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): - 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.__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")) - - if self.__version.isdigit(): - self.__feedInfo("version", self.__match.group(int(self.__version))) - - if self.__sp.isdigit(): - self.__feedInfo("sp", "Service Pack %s" % self.__match.group(int(self.__sp))) - - self.__regexp = None - self.__match = None - self.__version = None +from lib.parse.handler import FingerprintHandler class MSSQLBannerHandler(ContentHandler): @@ -172,9 +119,10 @@ def bannerParser(banner): if kb.dbms == "Microsoft SQL Server": handler = MSSQLBannerHandler(banner) parse(xmlfile, handler) - handler = BannerHandler(banner) + + handler = FingerprintHandler(banner, kb.bannerFp) parse(paths.GENERIC_XML, handler) else: - handler = BannerHandler(banner) + handler = FingerprintHandler(banner, kb.bannerFp) parse(xmlfile, handler) parse(paths.GENERIC_XML, handler) diff --git a/lib/parse/handler.py b/lib/parse/handler.py new file mode 100644 index 000000000..eae2bc8f1 --- /dev/null +++ b/lib/parse/handler.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +""" +$Id$ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. + and Daniele Bellucci + +sqlmap is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation version 2 of the License. + +sqlmap is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along +with sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from xml.sax.handler import ContentHandler + +from lib.core.common import sanitizeStr +from lib.core.data import kb + + +class FingerprintHandler(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, info): + self.__banner = sanitizeStr(banner) + + self.__regexp = None + self.__match = None + self.__dbmsVersion = None + self.__techVersion = None + self.__info = info + + + def __feedInfo(self, key, value): + value = sanitizeStr(value) + + if value in ( None, "None" ): + return + + if key in ( "dbmsVersion" ): + self.__info[key] = value + else: + if key not in self.__info.keys(): + self.__info[key] = set() + + self.__info[key].add(value) + + + 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.__feedInfo("type", attrs.get("type")) + self.__feedInfo("distrib", attrs.get("distrib")) + self.__feedInfo("release", attrs.get("release")) + self.__feedInfo("codename", attrs.get("codename")) + + self.__dbmsVersion = sanitizeStr(attrs.get("dbms_version")) + self.__techVersion = sanitizeStr(attrs.get("tech_version")) + self.__sp = sanitizeStr(attrs.get("sp")) + + if self.__dbmsVersion.isdigit(): + self.__feedInfo("dbmsVersion", self.__match.group(int(self.__dbmsVersion))) + + if self.__techVersion.isdigit(): + self.__feedInfo("technology", "%s %s" % (attrs.get("technology"), self.__match.group(int(self.__techVersion)))) + else: + self.__feedInfo("technology", attrs.get("technology")) + + if self.__sp.isdigit(): + self.__feedInfo("sp", "Service Pack %s" % self.__match.group(int(self.__sp))) + + self.__regexp = None + self.__match = None + self.__dbmsVersion = None + self.__techVersion = None diff --git a/lib/parse/headers.py b/lib/parse/headers.py index f3c8dc25c..524d453b8 100644 --- a/lib/parse/headers.py +++ b/lib/parse/headers.py @@ -27,67 +27,11 @@ 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 - - -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 +from lib.parse.handler import FingerprintHandler def headersParser(headers): @@ -97,11 +41,16 @@ def headersParser(headers): and the web application technology """ + if kb.headersCount > 3: + return + + kb.headersCount += 1 + # 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, + "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, @@ -114,6 +63,6 @@ def headersParser(headers): value = headers[header] xmlfile = topHeaders[header] checkFile(xmlfile) - handler = HeadersHandler(value) + handler = FingerprintHandler(value, kb.headersFp) parse(xmlfile, handler) parse(paths.GENERIC_XML, handler) diff --git a/plugins/dbms/mssqlserver.py b/plugins/dbms/mssqlserver.py index 32deb7cad..150e85577 100644 --- a/plugins/dbms/mssqlserver.py +++ b/plugins/dbms/mssqlserver.py @@ -29,7 +29,7 @@ import time from lib.core.agent import agent from lib.core.common import dataToStdout from lib.core.common import formatDBMSfp -from lib.core.common import formatOSfp +from lib.core.common import formatFingerprint from lib.core.common import getHtmlErrorFp from lib.core.common import randomInt from lib.core.common import readInput @@ -43,7 +43,6 @@ from lib.core.session import setDbms from lib.core.settings import MSSQL_ALIASES from lib.core.settings import MSSQL_SYSTEM_DBS 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 @@ -123,14 +122,17 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): - value = "" - formatInfo = None + value = "" + wsOsFp = formatFingerprint("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp if self.banner: - formatInfo = formatOSfp() + dbmsOsFp = formatFingerprint("back-end DBMS", kb.bannerFp) - if formatInfo: - value += "%s\n" % formatInfo + if dbmsOsFp: + value += "%s\n" % dbmsOsFp value += "back-end DBMS: " actVer = formatDBMSfp() @@ -140,7 +142,6 @@ class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): return value blank = " " * 15 - formatInfo = None value += "active fingerprint: %s" % actVer if kb.bannerFp: diff --git a/plugins/dbms/mysql.py b/plugins/dbms/mysql.py index 768f9f0d4..ddf3330a0 100644 --- a/plugins/dbms/mysql.py +++ b/plugins/dbms/mysql.py @@ -29,7 +29,7 @@ import re from lib.core.agent import agent from lib.core.common import fileToStr from lib.core.common import formatDBMSfp -from lib.core.common import formatOSfp +from lib.core.common import formatFingerprint from lib.core.common import getDirectories from lib.core.common import getHtmlErrorFp from lib.core.common import randomInt @@ -44,7 +44,6 @@ 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 @@ -181,14 +180,17 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): - value = "" - formatInfo = None + value = "" + wsOsFp = formatFingerprint("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp if self.banner: - formatInfo = formatOSfp() + dbmsOsFp = formatFingerprint("back-end DBMS", kb.bannerFp) - if formatInfo: - value += "%s\n" % formatInfo + if dbmsOsFp: + value += "%s\n" % dbmsOsFp value += "back-end DBMS: " actVer = formatDBMSfp() @@ -199,7 +201,6 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): comVer = self.__commentCheck() blank = " " * 15 - formatInfo = None value += "active fingerprint: %s" % actVer if comVer: @@ -208,7 +209,7 @@ class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): if kb.bannerFp: # TODO: move to the XML banner file - banVer = kb.bannerFp['version'] + banVer = kb.bannerFp["dbmsVersion"] if re.search("-log$", self.banner): banVer += ", logging enabled" diff --git a/plugins/dbms/oracle.py b/plugins/dbms/oracle.py index 4a78a19a4..e39b76d33 100644 --- a/plugins/dbms/oracle.py +++ b/plugins/dbms/oracle.py @@ -27,7 +27,7 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import re from lib.core.common import formatDBMSfp -from lib.core.common import formatOSfp +from lib.core.common import formatFingerprint from lib.core.common import getHtmlErrorFp from lib.core.data import conf from lib.core.data import kb @@ -37,7 +37,6 @@ 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 plugins.generic.enumeration import Enumeration @@ -117,14 +116,17 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): - value = "" - formatInfo = None + value = "" + wsOsFp = formatFingerprint("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp if self.banner: - formatInfo = formatOSfp() + dbmsOsFp = formatFingerprint("back-end DBMS", kb.bannerFp) - if formatInfo: - value += "%s\n" % formatInfo + if dbmsOsFp: + value += "%s\n" % dbmsOsFp value += "back-end DBMS: " @@ -134,11 +136,10 @@ class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): actVer = formatDBMSfp() blank = " " * 15 - formatInfo = None value += "active fingerprint: %s" % actVer if kb.bannerFp: - banVer = kb.bannerFp['version'] + banVer = kb.bannerFp["dbmsVersion"] banVer = formatDBMSfp([banVer]) value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) diff --git a/plugins/dbms/postgresql.py b/plugins/dbms/postgresql.py index 0155ff0b9..903cfcf6e 100644 --- a/plugins/dbms/postgresql.py +++ b/plugins/dbms/postgresql.py @@ -27,7 +27,7 @@ Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import re from lib.core.common import formatDBMSfp -from lib.core.common import formatOSfp +from lib.core.common import formatFingerprint from lib.core.common import getHtmlErrorFp from lib.core.common import randomInt from lib.core.data import conf @@ -38,7 +38,6 @@ 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 plugins.generic.enumeration import Enumeration @@ -117,14 +116,17 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): def getFingerprint(self): - value = "" - formatInfo = None + value = "" + wsOsFp = formatFingerprint("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp if self.banner: - formatInfo = formatOSfp() + dbmsOsFp = formatFingerprint("back-end DBMS", kb.bannerFp) - if formatInfo: - value += "%s\n" % formatInfo + if dbmsOsFp: + value += "%s\n" % dbmsOsFp value += "back-end DBMS: " @@ -134,11 +136,10 @@ class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): actVer = formatDBMSfp() blank = " " * 15 - formatInfo = None value += "active fingerprint: %s" % actVer if kb.bannerFp: - banVer = kb.bannerFp['version'] + banVer = kb.bannerFp["dbmsVersion"] banVer = formatDBMSfp([banVer]) value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) diff --git a/xml/banner/generic.xml b/xml/banner/generic.xml index 950d9715d..8b0d3fe1c 100644 --- a/xml/banner/generic.xml +++ b/xml/banner/generic.xml @@ -26,12 +26,27 @@ + + + + + + + + + + + + + + + @@ -61,7 +76,7 @@ - + @@ -72,7 +87,6 @@ - diff --git a/xml/banner/mysql.xml b/xml/banner/mysql.xml index 1d806f963..c51c0296f 100644 --- a/xml/banner/mysql.xml +++ b/xml/banner/mysql.xml @@ -1,46 +1,42 @@ - - + - - + - - + - + - + - + - + - + - - + diff --git a/xml/banner/oracle.xml b/xml/banner/oracle.xml index ad8bc36d9..61ce9948c 100644 --- a/xml/banner/oracle.xml +++ b/xml/banner/oracle.xml @@ -1,8 +1,7 @@ - - + diff --git a/xml/banner/postgresql.xml b/xml/banner/postgresql.xml index aad23bfee..3f58f101c 100644 --- a/xml/banner/postgresql.xml +++ b/xml/banner/postgresql.xml @@ -1,14 +1,12 @@ - - + - - + diff --git a/xml/banner/server.xml b/xml/banner/server.xml new file mode 100644 index 000000000..d0cb771a7 --- /dev/null +++ b/xml/banner/server.xml @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +