Exemple #1
0
    def is_URL_in_windows(self, main_url):
        """
        Detect if platform is Windows or \*NIX. To do this, get the first link, in scope, and
        does two resquest. If are the same response, then, platform are Windows. Else are \*NIX.

        :returns: True, if the remote host is a Windows system. False is \*NIX or None if unknown.
        :rtype: bool
        """
        m_forbidden = (
            "logout",
            "logoff",
            "exit",
            "sigout",
            "signout",
        )

        # Get the main web page
        m_r = download(main_url, callback=self.check_download)
        if not m_r or not m_r.raw_data:
            return None
        discard_data(m_r)

        # Get the first link
        m_links = None
        try:
            if m_r.information_type == Information.INFORMATION_HTML:
                m_links = extract_from_html(m_r.raw_data, main_url)
            else:
                m_links = extract_from_text(m_r.raw_data, main_url)
        except TypeError,e:
            Logger.log_error_more_verbose("Plugin error: %s" % format_exc())
            return None
    def is_URL_in_windows(self, main_url):
        """
        Detect if platform is Windows or \*NIX. To do this, get the first link, in scope, and
        does two resquest. If are the same response, then, platform are Windows. Else are \*NIX.

        :returns: True, if the remote host is a Windows system. False is \*NIX or None if unknown.
        :rtype: bool
        """
        m_forbidden = (
            "logout",
            "logoff",
            "exit",
            "sigout",
            "signout",
        )

        # Get the main web page
        m_r = download(main_url, callback=self.check_download)
        if not m_r or not m_r.raw_data:
            return None
        discard_data(m_r)

        # Get the first link
        m_links = None
        try:
            if m_r.information_type == Information.INFORMATION_HTML:
                m_links = extract_from_html(m_r.raw_data, main_url)
            else:
                m_links = extract_from_text(m_r.raw_data, main_url)
        except TypeError, e:
            Logger.log_error_more_verbose("Plugin error: %s" % format_exc())
            return None
def process_url(risk_level, method, matcher, updater_func, total_urls, url):
    """
    Checks if an URL exits.

    :param risk_level: risk level of the tested URL, if discovered.
    :type risk_level: int

    :param method: string with HTTP method used.
    :type method: str

    :param matcher: instance of MatchingAnalyzer object.
    :type matcher: `MatchingAnalyzer`

    :param updater_func: update_status function to send updates
    :type updater_func: update_status

    :param total_urls: total number of URL to globally process.
    :type total_urls: int

    :param url: a tuple with data: (index, the URL to process)
    :type url: tuple(int, str)
    """
    i, url = url

    updater_func((float(i) * 100.0) / float(total_urls))
    # Logger.log_more_verbose("Trying to discover URL %s" % url)

    # Get URL
    p = None
    try:
        p = HTTP.get_url(url, use_cache=False, method=method)
        if p:
            discard_data(p)
    except Exception, e:
        Logger.log_error_more_verbose("Error while processing: '%s': %s" % (url, str(e)))
Exemple #4
0
    def __detect_wordpress_installation(self, url, wordpress_urls):
        """
        Try to detect a wordpress instalation in the current path.

        :param url: URL where try to find the WordPress installation.
        :type url: str

        :param wordpress_urls: string with wordlist name with WordPress URLs.
        :type wordpress_urls: str

        :return: True if wordpress installation found. False otherwise.
        :rtype: bool
        """
        Logger.log_more_verbose(
            "Detecting Wordpress instalation in URI: '%s'." % url)
        total_urls = 0
        urls_found = 0

        error_page = get_error_page(url).raw_data

        for u in WordListLoader.get_wordlist(wordpress_urls):
            total_urls += 1
            tmp_url = urljoin(url, u)

            r = HTTP.get_url(tmp_url, use_cache=False)
            if r.status == "200":

                # Try to detect non-default error pages
                ratio = get_diff_ratio(r.raw_response, error_page)
                if ratio < 0.35:
                    urls_found += 1

            discard_data(r)

        # If Oks > 85% continue
        if (urls_found / float(total_urls)) < 0.85:

            # If all fails, make another last test
            url_wp_admin = urljoin(url, "wp-admin/")

            try:
                p = HTTP.get_url(url_wp_admin,
                                 use_cache=False,
                                 allow_redirects=False)
                if p:
                    discard_data(p)
            except Exception, e:
                return False

            if p.status == "302" and "wp-login.php?redirect_to=" in p.headers.get(
                    "Location", ""):
                return True
            else:
                return False
Exemple #5
0
    def __detect_wordpress_installation(self, url, wordpress_urls):
        """
        Try to detect a wordpress instalation in the current path.

        :param url: URL where try to find the WordPress installation.
        :type url: str

        :param wordpress_urls: string with wordlist name with WordPress URLs.
        :type wordpress_urls: str

        :return: True if wordpress installation found. False otherwise.
        :rtype: bool
        """
        Logger.log_more_verbose("Detecting Wordpress instalation in URI: '%s'." % url)
        total_urls = 0
        urls_found = 0

        error_page = get_error_page(url).raw_data

        for u in WordListLoader.get_wordlist(wordpress_urls):
            total_urls += 1
            tmp_url = urljoin(url, u)

            r = HTTP.get_url(tmp_url, use_cache=False)
            if r.status == "200":

                # Try to detect non-default error pages
                ratio = get_diff_ratio(r.raw_response, error_page)
                if ratio < 0.35:
                    urls_found += 1

            discard_data(r)

        # If Oks > 85% continue
        if (urls_found / float(total_urls)) < 0.85:

            # If all fails, make another last test
            url_wp_admin = urljoin(url, "wp-admin/")

            try:
                p = HTTP.get_url(url_wp_admin, use_cache=False, allow_redirects=False)
                if p:
                    discard_data(p)
            except Exception, e:
                return False

            if p.status == "302" and "wp-login.php?redirect_to=" in p.headers.get("Location", ""):
                return True
            else:
                return False
def get_http_method(url):
    """
    This function determinates if the method HEAD is available. To do that, compare between two responses:
    - One with GET method
    - One with HEAD method

    If both are seem more than 90%, the response are the same and HEAD method are not allowed.
    """

    m_head_response = HTTP.get_url(url, method="HEAD")  # FIXME handle exceptions!
    discard_data(m_head_response)

    m_get_response  = HTTP.get_url(url)  # FIXME handle exceptions!
    discard_data(m_get_response)

    # Check if HEAD reponse is different that GET response, to ensure that results are valids
    return "HEAD" if HTTP_response_headers_analyzer(m_head_response.headers, m_get_response.headers) < 0.90 else "GET"
Exemple #7
0
def get_http_method(url):
    """
    This function determinates if the method HEAD is available. To do that, compare between two responses:
    - One with GET method
    - One with HEAD method

    If both are seem more than 90%, the response are the same and HEAD method are not allowed.
    """

    m_head_response = HTTP.get_url(url, method="HEAD")  # FIXME handle exceptions!
    discard_data(m_head_response)

    m_get_response  = HTTP.get_url(url)  # FIXME handle exceptions!
    discard_data(m_get_response)

    # Check if HEAD reponse is different that GET response, to ensure that results are valids
    return "HEAD" if HTTP_response_headers_analyzer(m_head_response.headers, m_get_response.headers) < 0.90 else "GET"
def get_error_page(url):
    """
    Generates an error page an get their content.

    :param url: string with the base Url.
    :type url: str

    :return: a string with the content of response.
    :rtype: str
    """

    #
    # Generate an error in server to get an error page, using a random string
    #
    # Make the URL
    m_error_url      = "%s%s" % (url, generate_random_string())

    # Get the request
    m_error_response = HTTP.get_url(m_error_url)  # FIXME handle exceptions!
    discard_data(m_error_response)
    m_error_response = m_error_response.data
Exemple #9
0
def get_error_page(url):
    """
    Generates an error page an get their content.

    :param url: string with the base Url.
    :type url: str

    :return: a string with the content of response.
    :rtype: str
    """

    #
    # Generate an error in server to get an error page, using a random string
    #
    # Make the URL
    m_error_url      = "%s%s" % (url, generate_random_string())

    # Get the request
    m_error_response = HTTP.get_url(m_error_url)  # FIXME handle exceptions!
    discard_data(m_error_response)
    m_error_response = m_error_response.data
Exemple #10
0
    def recv_info(self, info):

        # Get the response page.
        response = HTTP.get_url(info.url, callback=self.check_response)
        if response:

            try:

                # Look for a match.
                page_text = response.data

                total = float(len(signatures))
                for step, (server_name,
                           server_page) in enumerate(signatures.iteritems()):

                    # Update status
                    progress = float(step) / total
                    self.update_status(progress=progress)

                    level = get_diff_ratio(page_text, server_page)

                    if level > 0.95:  # magic number :)

                        # Match found.
                        vulnerability = DefaultErrorPage(info, server_name)
                        vulnerability.add_information(response)
                        return [vulnerability, response]

                # Discard the response if no match was found.
                discard_data(response)

            except Exception:

                # Discard the response on error.
                discard_data(response)

                raise
    def recv_info(self, info):

        # Get the response page.
        response = HTTP.get_url(info.url, callback = self.check_response)
        if response:

            try:

                # Look for a match.
                page_text = response.data

                total = float(len(signatures))
                for step, (server_name, server_page) in enumerate(signatures.iteritems()):

                    # Update status
                    progress = float(step) / total
                    self.update_status(progress=progress)

                    level = get_diff_ratio(page_text, server_page)

                    if level > 0.95:  # magic number :)

                        # Match found.
                        vulnerability = DefaultErrorPage(info, server_name)
                        vulnerability.add_information(response)
                        return [vulnerability, response]

                # Discard the response if no match was found.
                discard_data(response)

            except Exception:

                # Discard the response on error.
                discard_data(response)

                raise
Exemple #12
0
def process_url(risk_level, method, matcher, updater_func, total_urls, url):
    """
    Checks if an URL exits.

    :param risk_level: risk level of the tested URL, if discovered.
    :type risk_level: int

    :param method: string with HTTP method used.
    :type method: str

    :param matcher: instance of MatchingAnalyzer object.
    :type matcher: `MatchingAnalyzer`

    :param updater_func: update_status function to send updates
    :type updater_func: update_status

    :param total_urls: total number of URL to globally process.
    :type total_urls: int

    :param url: a tuple with data: (index, the URL to process)
    :type url: tuple(int, str)
    """
    i, url = url

    updater_func((float(i) * 100.0) / float(total_urls))
    # Logger.log_more_verbose("Trying to discover URL %s" % url)

    # Get URL
    p = None
    try:
        p = HTTP.get_url(url, use_cache=False, method=method)
        if p:
            discard_data(p)
    except Exception, e:
        Logger.log_error_more_verbose("Error while processing: '%s': %s" %
                                      (url, str(e)))
Exemple #13
0
class HarvesterPlugin(TestingPlugin):
    """
    Integration with `theHarvester <https://code.google.com/p/theharvester/>`_.
    """

    # Supported theHarvester modules.
    SUPPORTED = (
        "google",
        "bing",
        "pgp",
        "exalead",  # "yandex",
    )

    #--------------------------------------------------------------------------
    def get_accepted_info(self):
        return [Domain]

    #--------------------------------------------------------------------------
    def recv_info(self, info):

        # Get the search parameters.
        word = info.hostname
        limit = 100
        try:
            limit = int(Config.plugin_config.get("limit", str(limit)), 0)
        except ValueError:
            pass

        # Search every supported engine.
        total = float(len(self.SUPPORTED))
        all_emails, all_hosts = set(), set()
        for step, engine in enumerate(self.SUPPORTED):
            try:
                Logger.log_verbose("Searching keyword %r in %s" %
                                   (word, engine))
                self.update_status(progress=float(step * 80) / total)
                emails, hosts = self.search(engine, word, limit)
            except Exception, e:
                t = traceback.format_exc()
                m = "theHarvester raised an exception: %s\n%s"
                warnings.warn(m % (e, t))
                continue
            all_emails.update(address.lower() for address in emails if address)
            all_hosts.update(name.lower() for name in hosts if name)
        self.update_status(progress=80)
        Logger.log_more_verbose("Search complete for keyword %r" % word)

        # Adapt the data into our model.
        results = []

        # Email addresses.
        for address in all_emails:
            if "..." in address:  # known bug in theHarvester
                continue
            while address and not address[0].isalnum(
            ):  # known bug in theHarvester
                address = address[1:]
            while address and not address[-1].isalnum():
                address = address[:-1]
            if not address:
                continue
            try:
                data = Email(address)
            except Exception, e:
                warnings.warn("Cannot parse email address: %r" % address)
                continue
            with warnings.catch_warnings():
                warnings.filterwarnings("ignore")
                in_scope = data.is_in_scope()
            if in_scope:
                data.add_resource(info)
                results.append(data)
                all_hosts.add(data.hostname)
            else:
                Logger.log_more_verbose("Email address out of scope: %s" %
                                        address)
                discard_data(data)
def http_analyzers(main_url, update_status_func, number_of_entries=4):
    """
    Analyze HTTP headers for detect the web server. Return a list with most possible web servers.

    :param main_url: Base url to test.
    :type main_url: str

    :param update_status_func: function used to update the status of the process
    :type update_status_func: function

    :param number_of_entries: number of resutls tu return for most probable web servers detected.
    :type number_of_entries: int

    :return: Web server family, Web server version, Web server complete description, related web servers (as a dict('SERVER_RELATED' : set(RELATED_NAMES))), others web server with their probabilities as a dict(CONCRETE_WEB_SERVER, PROBABILITY)
    """

    # Load wordlist directly related with a HTTP fields.
    # { HTTP_HEADER_FIELD : [wordlists] }
    m_wordlists_HTTP_fields = {
        "Accept-Ranges"              : "accept-range",
        "Server"                     : "banner",
        "Cache-Control"              : "cache-control",
        "Connection"                 : "connection",
        "Content-Type"               : "content-type",
        "WWW-Authenticate"           : "htaccess-realm",
        "Pragma"                     : "pragma",
        "X-Powered-By"               : "x-powered-by"
    }

    m_actions = {
        'GET'        : { 'wordlist' : 'Wordlist_get'            , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/' },
        'LONG_GET'   : { 'wordlist' : 'Wordlist_get_long'       , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/%s' % ('a' * 200) },
        'NOT_FOUND'  : { 'wordlist' : 'Wordlist_get_notfound'   , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/404_NOFOUND__X02KAS' },
        'HEAD'       : { 'wordlist' : 'Wordlist_head'           , 'weight' : 3 , 'protocol' : 'HTTP/1.1', 'method' : 'HEAD'     , 'payload': '/' },
        'OPTIONS'    : { 'wordlist' : 'Wordlist_options'        , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'OPTIONS'  , 'payload': '/' },
        'DELETE'     : { 'wordlist' : 'Wordlist_delete'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'DELETE'   , 'payload': '/' },
        'TEST'       : { 'wordlist' : 'Wordlist_attack'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'TEST'     , 'payload': '/' },
        'INVALID'    : { 'wordlist' : 'Wordlist_wrong_method'   , 'weight' : 5 , 'protocol' : 'HTTP/9.8', 'method' : 'GET'      , 'payload': '/' },
        'ATTACK'     : { 'wordlist' : 'Wordlist_wrong_version'  , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': "/etc/passwd?format=%%%%&xss=\x22><script>alert('xss');</script>&traversal=../../&sql='%20OR%201;"}
    }


    # Store results for others HTTP params
    m_d                   = ParsedURL(main_url)
    m_hostname            = m_d.hostname
    m_port                = m_d.port
    m_debug               = False # Only for develop

    # Counter of banners. Used when others methods fails.
    m_banners_counter     = Counter()

    # Score counter
    m_counters = HTTPAnalyzer(debug=m_debug)

    # Var used to update the status
    m_data_len = len(m_actions)
    i          = 1 # element in process


    for l_action, v in m_actions.iteritems():
        if m_debug:
            print "###########"
        l_method      = v["method"]
        l_payload     = v["payload"]
        l_proto       = v["protocol"]
        l_wordlist    = v["wordlist"]

        # Each type of probe hast different weight.
        #
        # Weights go from 0 - 5
        #
        l_weight      = v["weight"]

        # Make the URL
        l_url         = urljoin(main_url, l_payload)

        # Make the raw request
        #l_raw_request = "%(method)s %(payload)s %(protocol)s\r\nHost: %(host)s:%(port)s\r\nConnection: Close\r\n\r\n" % (
        l_raw_request = "%(method)s %(payload)s %(protocol)s\r\nHost: %(host)s\r\n\r\n" % (
            {
                "method"     : l_method,
                "payload"    : l_payload,
                "protocol"   : l_proto,
                "host"       : m_hostname,
                "port"       : m_port
            }
        )
        if m_debug:
            print "REQUEST"
            print l_raw_request

        # Do the connection
        l_response = None
        try:
            m_raw_request = HTTP_Raw_Request(l_raw_request)
            discard_data(m_raw_request)
            l_response = HTTP.make_raw_request(
                host        = m_hostname,
                port        = m_port,
                raw_request = m_raw_request,
                callback    = check_raw_response)
            if l_response:
                discard_data(l_response)
        except NetworkException,e:
            Logger.log_error_more_verbose("Server-Fingerprint plugin: No response for URL (%s) '%s'. Message: %s" % (l_method, l_url, str(e)))
            continue

        if not l_response:
            Logger.log_error_more_verbose("No response for URL '%s'." % l_url)
            continue

        if m_debug:
            print "RESPONSE"
            print l_response.raw_headers


        # Update the status
        update_status_func((float(i) * 100.0) / float(m_data_len))
        Logger.log_more_verbose("Making '%s' test." % (l_wordlist))
        i += 1

        # Analyze for each wordlist
        #
        # Store the server banner
        try:
            m_banners_counter[l_response.headers["Server"]] += l_weight
        except KeyError:
            pass

        #
        # =====================
        # HTTP directly related
        # =====================
        #
        #
        for l_http_header_name, l_header_wordlist in m_wordlists_HTTP_fields.iteritems():

            # Check if HTTP header field is in response
            if l_http_header_name not in l_response.headers:
                continue

            l_curr_header_value = l_response.headers[l_http_header_name]

            # Generate concrete wordlist name
            l_wordlist_path     = Config.plugin_extra_config[l_wordlist][l_header_wordlist]

            # Load words for the wordlist
            l_wordlist_instance = WordListLoader.get_wordlist_as_dict(l_wordlist_path)
            # Looking for matches
            l_matches           = l_wordlist_instance.matches_by_value(l_curr_header_value)

            m_counters.inc(l_matches, l_action, l_weight, l_http_header_name, message="HTTP field: " + l_curr_header_value)

        #
        # =======================
        # HTTP INdirectly related
        # =======================
        #
        #

        #
        # Status code
        # ===========
        #
        l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["statuscode"])
        # Looking for matches
        l_matches           = l_wordlist_instance.matches_by_value(l_response.status)

        m_counters.inc(l_matches, l_action, l_weight, "statuscode", message="Status code: " + l_response.status)


        #
        # Status text
        # ===========
        #
        l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["statustext"])
        # Looking for matches
        l_matches           = l_wordlist_instance.matches_by_value(l_response.reason)

        m_counters.inc(l_matches, l_action, l_weight, "statustext", message="Status text: " + l_response.reason)


        #
        # Header space
        # ============
        #
        # Count the number of spaces between HTTP field name and their value, for example:
        # -> Server: Apache 1
        # The number of spaces are: 1
        #
        # -> Server:Apache 1
        # The number of spaces are: 0
        #
        l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["header-space"])
        # Looking for matches
        try:
            l_http_value        = l_response.headers[0] # get the value of first HTTP field
            l_spaces_num        = str(abs(len(l_http_value) - len(l_http_value.lstrip())))
            l_matches           = l_wordlist_instance.matches_by_value(l_spaces_num)

            m_counters.inc(l_matches, l_action, l_weight, "header-space", message="Header space: " + l_spaces_num)

        except IndexError:
            print "index error header space"
            pass


        #
        # Header capitalafterdash
        # =======================
        #
        # Look for non capitalized first letter of field name, for example:
        # -> Content-type: ....
        # Instead of:
        # -> Content-Type: ....
        #
        l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["header-capitalafterdash"])
        # Looking for matches
        l_valid_fields     = [x for x in l_response.headers.iterkeys() if "-" in x]

        if l_valid_fields:

            l_h = l_valid_fields[0]

            l_value = l_h.split("-")[1] # Get the second value: Content-type => type
            l_dush  = None

            if l_value[0].isupper(): # Check first letter is lower
                l_dush = 1
            else:
                l_dush = 0

            l_matches           = l_wordlist_instance.matches_by_value(l_dush)
            m_counters.inc(l_matches, l_action, l_weight, "header-capitalizedafterdush", message="Capital after dash: %s" % str(l_dush))

        #
        # Header order
        # ============
        #
        l_header_order  = ','.join(l_response.headers.iterkeys())

        l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["header-order"])
        l_matches           = l_wordlist_instance.matches_by_value(l_header_order)

        m_counters.inc(l_matches, l_action, l_weight, "header-order", message="Header order: " + l_header_order)


        #
        # Protocol name
        # ============
        #
        # For a response like:
        # -> HTTP/1.0 200 OK
        #    ....
        #
        # Get the 'HTTP' value.
        #
        try:
            l_proto             = l_response.protocol # Get the 'HTTP' text from response, if available
            if l_proto:
                l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["protocol-name"])
                l_matches           = l_wordlist_instance.matches_by_value(l_proto)

                m_counters.inc(l_matches, l_action, l_weight, "proto-name", message="Proto name: " + l_proto)

        except IndexError:
            print "index error protocol name"
            pass


        #
        # Protocol version
        # ================
        #
        # For a response like:
        # -> HTTP/1.0 200 OK
        #    ....
        #
        # Get the '1.0' value.
        #
        try:
            l_version           = l_response.version # Get the '1.0' text from response, if available
            if l_version:
                l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["protocol-version"])
                l_matches           = l_wordlist_instance.matches_by_value(l_version)

                m_counters.inc(l_matches, l_action, l_weight, "proto-version", message="Proto version: " + l_version)

        except IndexError:
            print "index error protocol version"
            pass



        if "ETag" in l_response.headers:
            l_etag_header       = l_response.headers["ETag"]
            #
            # ETag length
            # ================
            #
            l_etag_len          = len(l_etag_header)
            l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["etag-legth"])
            l_matches           = l_wordlist_instance.matches_by_value(l_etag_len)

            m_counters.inc(l_matches, l_action, l_weight, "etag-length", message="ETag length: " + str(l_etag_len))


            #
            # ETag Quotes
            # ================
            #
            l_etag_striped          = l_etag_header.strip()
            if l_etag_striped.startswith("\"") or l_etag_striped.startswith("'"):
                l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["etag-quotes"])
                l_matches           = l_wordlist_instance.matches_by_value(l_etag_striped[0])

                m_counters.inc(l_matches, l_action, l_weight, "etag-quotes", message="Etag quotes: " + l_etag_striped[0])

        if "Vary" in l_response.headers:
            l_vary_header       = l_response.headers["Vary"]
            #
            # Vary delimiter
            # ================
            #
            # Checks if Vary header delimiter is something like this:
            # -> Vary: Accept-Encoding,User-Agent
            # Or this:
            # -> Vary: Accept-Encoding, User-Agent
            #
            l_var_delimiter     = ", " if l_vary_header.find(", ") else ","
            l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["vary-delimiter"])
            l_matches           = l_wordlist_instance.matches_by_value(l_var_delimiter)

            m_counters.inc(l_matches, l_action, l_weight, "vary-delimiter", message="Vary delimiter: " + l_var_delimiter)

            #
            # Vary capitalizer
            # ================
            #
            # Checks if Vary header delimiter is something like this:
            # -> Vary: Accept-Encoding,user-Agent
            # Or this:
            # -> Vary: accept-encoding,user-agent
            #
            l_vary_capitalizer  = str(0 if l_vary_header == l_vary_header.lower() else 1)
            l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["vary-capitalize"])
            l_matches           = l_wordlist_instance.matches_by_value(l_vary_capitalizer)

            m_counters.inc(l_matches, l_action, l_weight, "vary-capitalize", message="Vary capitalizer: " + l_vary_capitalizer)


            #
            # Vary order
            # ================
            #
            # Checks order between vary values:
            # -> Vary: Accept-Encoding,user-Agent
            # Or this:
            # -> Vary: User-Agent,Accept-Encoding
            #
            l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["vary-order"])
            l_matches           = l_wordlist_instance.matches_by_value(l_vary_header)

            m_counters.inc(l_matches, l_action, l_weight, "vary-order", message="Vary order: " + l_vary_header)


        #
        # =====================
        # HTTP specific options
        # =====================
        #
        #
        if l_action == "HEAD":
            #
            # HEAD Options
            # ============
            #
            l_option            = l_response.headers.get("Allow")
            if l_option:
                l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-public"])
                # Looking for matches
                l_matches           = l_wordlist_instance.matches_by_value(l_option)

                m_counters.inc(l_matches, l_action, l_weight, "options-allow", message="HEAD option: " + l_option)


        if l_action == "OPTIONS" or l_action == "INVALID" or l_action == "DELETE":
            if "Allow" in l_response.headers:
                #
                # Options allow
                # =============
                #
                l_option            = l_response.headers.get("Allow")
                if l_option:
                    l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-public"])
                    # Looking for matches
                    l_matches           = l_wordlist_instance.matches_by_value(l_option)

                    m_counters.inc(l_matches, l_action, l_weight, "options-allow", message="OPTIONS allow: "  + l_action + " # " + l_option)


                #
                # Allow delimiter
                # ===============
                #
                l_option            = l_response.headers.get("Allow")
                if l_option:
                    l_var_delimiter     = ", " if l_option.find(", ") else ","
                    l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-delimited"])
                    # Looking for matches
                    l_matches           = l_wordlist_instance.matches_by_value(l_var_delimiter)

                    m_counters.inc(l_matches, l_action, l_weight, "options-delimiter", message="OPTION allow delimiter " + l_action + " # " + l_option)


            if "Public" in l_response.headers:
                #
                # Public response
                # ===============
                #
                l_option            = l_response.headers.get("Public")
                if l_option:
                    l_wordlist_instance = WordListLoader.get_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-public"])
                    # Looking for matches
                    l_matches           = l_wordlist_instance.matches_by_value(l_option)

                    m_counters.inc(l_matches, l_action, l_weight, "options-public", message="Public response: " + l_action + " # " + l_option)
def http_simple_analyzer(main_url, update_status_func, number_of_entries=4):
    """Simple method to get fingerprint server info

    :param main_url: Base url to test.
    :type main_url: str

    :param update_status_func: function used to update the status of the process
    :type update_status_func: function

    :param number_of_entries: number of resutls tu return for most probable web servers detected.
    :type number_of_entries: int

    :return: a typle as format: Web server family, Web server version, Web server complete description, related web servers (as a dict('SERVER_RELATED' : set(RELATED_NAMES))), others web server with their probabilities as a dict(CONCRETE_WEB_SERVER, PROBABILITY)
    """

    m_actions = {
        'GET'        : { 'wordlist' : 'Wordlist_get'            , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/' },
        'LONG_GET'   : { 'wordlist' : 'Wordlist_get_long'       , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/%s' % ('a' * 200) },
        'NOT_FOUND'  : { 'wordlist' : 'Wordlist_get_notfound'   , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/404_NOFOUND__X02KAS' },
        'HEAD'       : { 'wordlist' : 'Wordlist_head'           , 'weight' : 3 , 'protocol' : 'HTTP/1.1', 'method' : 'HEAD'     , 'payload': '/' },
        'OPTIONS'    : { 'wordlist' : 'Wordlist_options'        , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'OPTIONS'  , 'payload': '/' },
        'DELETE'     : { 'wordlist' : 'Wordlist_delete'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'DELETE'   , 'payload': '/' },
        'TEST'       : { 'wordlist' : 'Wordlist_attack'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'TEST'     , 'payload': '/' },
        'INVALID'    : { 'wordlist' : 'Wordlist_wrong_method'   , 'weight' : 5 , 'protocol' : 'HTTP/9.8', 'method' : 'GET'      , 'payload': '/' },
        'ATTACK'     : { 'wordlist' : 'Wordlist_wrong_version'  , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': "/etc/passwd?format=%%%%&xss=\x22><script>alert('xss');</script>&traversal=../../&sql='%20OR%201;"}
    }

    m_d                   = ParsedURL(main_url)
    m_hostname            = m_d.hostname
    m_port                = m_d.port
    m_scheme              = m_d.scheme
    m_debug               = False # Only for develop
    i                     = 0
    m_counters            = HTTPAnalyzer()
    m_data_len            = len(m_actions) # Var used to update the status
    m_banners_counter     = Counter()

    for l_action, v in m_actions.iteritems():
        if m_debug:
            print "###########"
        l_method      = v["method"]
        l_payload     = v["payload"]
        l_proto       = v["protocol"]
        #l_wordlist    = v["wordlist"]

        # Each type of probe hast different weight.
        #
        # Weights go from 0 - 5
        #
        l_weight      = v["weight"]

        # Make the raw request
        l_raw_request = "%(method)s %(payload)s %(protocol)s\r\nHost: %(host)s\r\n\r\n" % (
            {
                "method"     : l_method,
                "payload"    : l_payload,
                "protocol"   : l_proto,
                "host"       : m_hostname,
                "port"       : m_port
            }
        )
        if m_debug:
            print "REQUEST"
            print l_raw_request

        # Do the connection
        l_response = None
        try:
            m_raw_request = HTTP_Raw_Request(l_raw_request)
            discard_data(m_raw_request)
            l_response = HTTP.make_raw_request(
                host        = m_hostname,
                port        = m_port,
                proto       = m_scheme,
                raw_request = m_raw_request)
                #callback    = check_raw_response)
            if l_response:
                discard_data(l_response)
        except NetworkException,e:
            Logger.log_error_more_verbose("Server-Fingerprint plugin: No response for host '%s:%d' with method '%s'. Message: %s" % (m_hostname, m_port, l_method, str(e)))
            continue

        if not l_response:
            Logger.log_error_more_verbose("No response for host '%s:%d' with method '%s'." % (m_hostname, m_port, l_method))
            continue

        if m_debug:
            print "RESPONSE"
            print l_response.raw_headers


        # Update the status
        update_status_func((float(i) * 100.0) / float(m_data_len))
        Logger.log_more_verbose("Making '%s' test." % l_method)
        i += 1

        # Analyze for each wordlist
        #
        # Store the server banner
        try:
            m_banners_counter[l_response.headers["Server"]] += l_weight
        except KeyError:
            pass

        l_server_name = None
        try:
            l_server_name = l_response.headers["Server"]
        except KeyError:
            continue

        m_counters.simple_inc(l_server_name, l_method, l_weight)
Exemple #16
0
    def run(self, info):

        # If it's an URL...
        if info.is_instance(BaseURL):
            target = URL(info.url)
            discard_data(target)

            # Get the hostname to test.
            hostname = info.hostname

            # If it's HTTPS, use the port number from the URL.
            if info.is_https:
                port = info.parsed_url.port

            # Otherwise, assume the port is 443.
            else:
                port = 443

            # Test this port.
            is_vulnerable = self.test(hostname, port)

        # If it's a service fingerprint...
        elif info.is_instance(Relationship(IP, ServiceFingerprint)):
            ip, fp = info.instances
            target = ip
            port = fp.port
            starttls = False

            # Ignore if the port does not respond directly to SSL...
            if fp.protocol != "SSL":

                # If it's SMTP, we need to issue a STARTTLS command first.
                if fp.name == "smtp":
                    starttls = True

                # Ignore if the port does not support SSL.
                else:
                    Logger.log_more_verbose(
                        "No SSL services found in fingerprint [%s] for IP %s,"
                        " aborting." % (fp, ip))
                    return

            # Test this port.
            is_vulnerable = self.test(ip.address, port, starttls=starttls)

        # Internal error!
        else:
            assert False, "Unexpected data type received: %s" % type(info)

        # If it's vulnerable, report the vulnerability.
        if is_vulnerable:
            title = "OpenSSL Heartbleed Vulnerability"
            description = "An unpatched OpenSSL service was found that's" \
                          " vulnerable to the Heartbleed vulnerability" \
                          " (CVE-2014-0162). This vulnerability allows an" \
                          " attacker to dump the memory contents of the" \
                          " service running the flawed version of the" \
                          " OpenSSL library, potentially compromising" \
                          " usernames, passwords, private keys and other" \
                          " sensitive data."
            references = ["http://heartbleed.com/"]
            cve = [
                "CVE-2014-0162",
            ],
            if target.is_instance(IP):
                vuln = VulnerableService(
                    target=target,
                    port=port,
                    protocol="TCP",
                    title=title,
                    description=description,
                    references=references,
                    cve=cve,
                )
            elif target.is_instance(URL):
                vuln = VulnerableWebApp(
                    target=target,
                    title=title,
                    description=description,
                    references=references,
                    cve=cve,
                )
            else:
                assert False, "Internal error!"
            return vuln
Exemple #17
0
    def __get_wordpress_version(self, url):
        """
        This function get the current version of wordpress and the last version
        available for download.

        :param url: URL fo target.
        :type url: str.

        :return: a tuple with (CURRENT_VERSION, LAST_AVAILABLE_VERSION)
        :type: tuple(str, str)
        """
        url_version = {
            # Generic
            "wp-login.php": r"(;ver=)([0-9\.]+)([\-a-z]*)",

            # For WordPress 3.8
            "wp-admin/css/wp-admin-rtl.css": r"(Version[\s]+)([0-9\.]+)",
            "wp-admin/css/wp-admin.css": r"(Version[\s]+)([0-9\.]+)"
        }

        #
        # Get current version
        #

        # URL to find wordpress version
        url_current_version = urljoin(url, "readme.html")
        current_version_content_1 = download(url_current_version)

        if isinstance(current_version_content_1, HTML):
            current_version_method1 = re.search(r"(<br/>[\s]*[vV]ersion[\s]*)([0-9\.]*)", current_version_content_1.raw_data)
            if current_version_method1 is None:
                current_version_method1 = None
            else:
                if len(current_version_method1.groups()) != 2:
                    current_version_method1 = None
                else:
                    current_version_method1 = current_version_method1.group(2)
        else:
            current_version_method1 = None

        # Try to find the version into HTML meta value

        # Get content of main page
        current_version_content_2 = download(url)

        # Try to find the info
        current_version_method2 = re.search(r"(<meta name=\"generator\" content=\"WordPress[\s]+)([0-9\.]+)",
                                            current_version_content_2.raw_data)
        if current_version_method2 is None:
            current_version_method2 = None
        else:
            if len(current_version_method2.groups()) != 2:
                current_version_method2 = None
            else:
                current_version_method2 = current_version_method2.group(2)

        # Match versions of the diffentents methods
        current_version = "unknown"
        if current_version_method1 is None and current_version_method2 is None:
            current_version = "unknown"
        elif current_version_method1 is None and current_version_method2 is not None:
            current_version = current_version_method2
        elif current_version_method1 is not None and current_version_method2 is None:
            current_version = current_version_method1
        elif current_version_method1 is not None and current_version_method2 is not None:
            if current_version_method1 != current_version_method2:
                current_version = current_version_method2
            else:
                current_version = current_version_method1
        else:
            current_version = "unknown"

        # If Current version not found
        if current_version == "unknown":
            for url_pre, regex in url_version.iteritems():
                # URL to find wordpress version
                url_current_version = urljoin(url, url_pre)
                current_version_content = download(url_current_version)
                discard_data(current_version_content)

                # Find the version
                tmp_version = re.search(regex, current_version_content.raw_data)

                if tmp_version is not None:
                    current_version = tmp_version.group(2)
                    break  # Found -> stop search

        #
        # Get last version
        #

        # URL to get last version of WordPress available
        url_last_version = "http://wordpress.org/download/"
        last_version_content = download(url_last_version, allow_out_of_scope=True)

        if isinstance(last_version_content, HTML):
            last_version = re.search("(WordPress&nbsp;)([0-9\.]*)", last_version_content.raw_data)

            if last_version is None:
                last_version = "unknown"
            else:
                if len(last_version.groups()) != 2:
                    last_version = "unknown"
                else:
                    last_version = last_version.group(2)
        else:
            last_version = "unknown"

        # Discard unused data
        discard_data(current_version_content_2)
        discard_data(current_version_content_1)
        discard_data(last_version_content)

        return current_version, last_version
Exemple #18
0
    def recv_info(self, info):

        m_domain = info.root

        # Skips localhost
        if m_domain == "localhost":
            return

        m_return = None

        # Checks if the hostname has been already processed
        if not self.state.check(m_domain):

            #
            # Looking for
            #
            m_subdomains = WordListLoader.get_advanced_wordlist_as_list("subs_small.txt")

            # Run in parallel
            self.base_domain = m_domain
            self.completed = Counter(0)
            self.total = len(m_subdomains)
            r = pmap(self.get_subdomains_bruteforcer, m_subdomains, pool_size=10)

            #
            # Remove repeated
            #

            # The results
            m_domains                  = set()
            m_domains_add              = m_domains.add
            m_domains_already          = []
            m_domains_already_append   = m_domains_already.append

            m_ips                      = set()
            m_ips_add                  = m_ips.add
            m_ips_already              = []
            m_ips_already_append       = m_ips_already.append

            if r:
                for doms in r:
                    for dom in doms:
                        # Domains
                        if dom.type == "CNAME":
                            if not dom.target in m_domains_already:
                                m_domains_already_append(dom.target)
                                if dom.target in Config.audit_scope:
                                    m_domains_add(dom)
                                else:
                                    discard_data(dom)

                        # IPs
                        if dom.type == "A":
                            if dom.address not in m_ips_already:
                                m_ips_already_append(dom.address)
                                m_ips_add(dom)

                # Unify
                m_domains.update(m_ips)

                m_return = m_domains


                # Add the information to the host
                map(info.add_information, m_return)

            # Set the domain as processed
            self.state.set(m_domain, True)

            Logger.log_verbose("DNS analyzer plugin found %d subdomains" % len(m_return))


            # Write the info as more user friendly
            if Logger.MORE_VERBOSE:
                m_tmp        = []
                m_tmp_append = m_tmp.append
                for x in m_return:
                    if getattr(x, "address", False):
                        m_tmp_append("%s (%s)" % (getattr(x, "address"), str(x)))
                    elif getattr(x, "target", False):
                        m_tmp_append("%s (%s)" % (getattr(x, "target"), str(x)))
                    else:
                        m_tmp_append(str(x))

                Logger.log_more_verbose("Subdomains found: \n\t+ %s" % "\n\t+ ".join(m_tmp))

        return m_return
Exemple #19
0
    def __get_wordpress_version(self, url):
        """
        This function get the current version of wordpress and the last version
        available for download.

        :param url: URL fo target.
        :type url: str.

        :return: a tuple with (CURRENT_VERSION, LAST_AVAILABLE_VERSION)
        :type: tuple(str, str)
        """
        url_version = {
            # Generic
            "wp-login.php": r"(;ver=)([0-9\.]+)([\-a-z]*)",

            # For WordPress 3.8
            "wp-admin/css/wp-admin-rtl.css": r"(Version[\s]+)([0-9\.]+)",
            "wp-admin/css/wp-admin.css": r"(Version[\s]+)([0-9\.]+)"
        }

        #
        # Get current version
        #

        # URL to find wordpress version
        url_current_version = urljoin(url, "readme.html")
        current_version_content_1 = download(url_current_version)

        if isinstance(current_version_content_1, HTML):
            current_version_method1 = re.search(
                r"(<br/>[\s]*[vV]ersion[\s]*)([0-9\.]*)",
                current_version_content_1.raw_data)
            if current_version_method1 is None:
                current_version_method1 = None
            else:
                if len(current_version_method1.groups()) != 2:
                    current_version_method1 = None
                else:
                    current_version_method1 = current_version_method1.group(2)
        else:
            current_version_method1 = None

        # Try to find the version into HTML meta value

        # Get content of main page
        current_version_content_2 = download(url)

        # Try to find the info
        current_version_method2 = re.search(
            r"(<meta name=\"generator\" content=\"WordPress[\s]+)([0-9\.]+)",
            current_version_content_2.raw_data)
        if current_version_method2 is None:
            current_version_method2 = None
        else:
            if len(current_version_method2.groups()) != 2:
                current_version_method2 = None
            else:
                current_version_method2 = current_version_method2.group(2)

        # Match versions of the diffentents methods
        current_version = "unknown"
        if current_version_method1 is None and current_version_method2 is None:
            current_version = "unknown"
        elif current_version_method1 is None and current_version_method2 is not None:
            current_version = current_version_method2
        elif current_version_method1 is not None and current_version_method2 is None:
            current_version = current_version_method1
        elif current_version_method1 is not None and current_version_method2 is not None:
            if current_version_method1 != current_version_method2:
                current_version = current_version_method2
            else:
                current_version = current_version_method1
        else:
            current_version = "unknown"

        # If Current version not found
        if current_version == "unknown":
            for url_pre, regex in url_version.iteritems():
                # URL to find wordpress version
                url_current_version = urljoin(url, url_pre)
                current_version_content = download(url_current_version)
                discard_data(current_version_content)

                # Find the version
                tmp_version = re.search(regex,
                                        current_version_content.raw_data)

                if tmp_version is not None:
                    current_version = tmp_version.group(2)
                    break  # Found -> stop search

        #
        # Get last version
        #

        # URL to get last version of WordPress available
        url_last_version = "http://wordpress.org/download/"
        last_version_content = download(url_last_version,
                                        allow_out_of_scope=True)

        if isinstance(last_version_content, HTML):
            last_version = re.search("(WordPress&nbsp;)([0-9\.]*)",
                                     last_version_content.raw_data)

            if last_version is None:
                last_version = "unknown"
            else:
                if len(last_version.groups()) != 2:
                    last_version = "unknown"
                else:
                    last_version = last_version.group(2)
        else:
            last_version = "unknown"

        # Discard unused data
        discard_data(current_version_content_2)
        discard_data(current_version_content_1)
        discard_data(last_version_content)

        return current_version, last_version
def http_analyzers(main_url, update_status_func, number_of_entries=4):
    """
    Analyze HTTP headers for detect the web server. Return a list with most possible web servers.

    :param main_url: Base url to test.
    :type main_url: str

    :param update_status_func: function used to update the status of the process
    :type update_status_func: function

    :param number_of_entries: number of resutls tu return for most probable web servers detected.
    :type number_of_entries: int

    :return: Web server family, Web server version, Web server complete description, related web servers (as a dict('SERVER_RELATED' : set(RELATED_NAMES))), others web server with their probabilities as a dict(CONCRETE_WEB_SERVER, PROBABILITY)
    """

    # Load wordlist directly related with a HTTP fields.
    # { HTTP_HEADER_FIELD : [wordlists] }
    m_wordlists_HTTP_fields = {
        "Accept-Ranges"              : "accept-range",
        "Server"                     : "banner",
        "Cache-Control"              : "cache-control",
        "Connection"                 : "connection",
        "Content-Type"               : "content-type",
        "WWW-Authenticate"           : "htaccess-realm",
        "Pragma"                     : "pragma",
        "X-Powered-By"               : "x-powered-by"
    }

    m_actions = {
        'GET'        : { 'wordlist' : 'Wordlist_get'            , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/' },
        'LONG_GET'   : { 'wordlist' : 'Wordlist_get_long'       , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/%s' % ('a' * 200) },
        'NOT_FOUND'  : { 'wordlist' : 'Wordlist_get_notfound'   , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/404_NOFOUND__X02KAS' },
        'HEAD'       : { 'wordlist' : 'Wordlist_head'           , 'weight' : 3 , 'protocol' : 'HTTP/1.1', 'method' : 'HEAD'     , 'payload': '/' },
        'OPTIONS'    : { 'wordlist' : 'Wordlist_options'        , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'OPTIONS'  , 'payload': '/' },
        'DELETE'     : { 'wordlist' : 'Wordlist_delete'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'DELETE'   , 'payload': '/' },
        'TEST'       : { 'wordlist' : 'Wordlist_attack'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'TEST'     , 'payload': '/' },
        'INVALID'    : { 'wordlist' : 'Wordlist_wrong_method'   , 'weight' : 5 , 'protocol' : 'HTTP/9.8', 'method' : 'GET'      , 'payload': '/' },
        'ATTACK'     : { 'wordlist' : 'Wordlist_wrong_version'  , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': "/etc/passwd?format=%%%%&xss=\x22><script>alert('xss');</script>&traversal=../../&sql='%20OR%201;"}
    }


    # Store results for others HTTP params
    m_d                   = ParsedURL(main_url)
    m_hostname            = m_d.hostname
    m_port                = m_d.port
    m_debug               = False # Only for develop

    # Counter of banners. Used when others methods fails.
    m_banners_counter     = Counter()

    # Score counter
    m_counters = HTTPAnalyzer(debug=m_debug)

    # Var used to update the status
    m_data_len = len(m_actions)
    i          = 1 # element in process


    for l_action, v in m_actions.iteritems():
        if m_debug:
            print "###########"
        l_method      = v["method"]
        l_payload     = v["payload"]
        l_proto       = v["protocol"]
        l_wordlist    = v["wordlist"]

        # Each type of probe hast different weight.
        #
        # Weights go from 0 - 5
        #
        l_weight      = v["weight"]

        # Make the URL
        l_url         = urljoin(main_url, l_payload)

        # Make the raw request
        #l_raw_request = "%(method)s %(payload)s %(protocol)s\r\nHost: %(host)s:%(port)s\r\nConnection: Close\r\n\r\n" % (
        l_raw_request = "%(method)s %(payload)s %(protocol)s\r\nHost: %(host)s\r\n\r\n" % (
            {
                "method"     : l_method,
                "payload"    : l_payload,
                "protocol"   : l_proto,
                "host"       : m_hostname,
                "port"       : m_port
            }
        )
        if m_debug:
            print "REQUEST"
            print l_raw_request

        # Do the connection
        l_response = None
        try:
            m_raw_request = HTTP_Raw_Request(l_raw_request)
            discard_data(m_raw_request)
            l_response = HTTP.make_raw_request(
                host        = m_hostname,
                port        = m_port,
                raw_request = m_raw_request,
                callback    = check_raw_response)
            if l_response:
                discard_data(l_response)
        except NetworkException,e:
            Logger.log_error_more_verbose("Server-Fingerprint plugin: No response for URL (%s) '%s'. Message: %s" % (l_method, l_url, str(e)))
            continue

        if not l_response:
            Logger.log_error_more_verbose("No response for URL '%s'." % l_url)
            continue

        if m_debug:
            print "RESPONSE"
            print l_response.raw_headers


        # Update the status
        update_status_func((float(i) * 100.0) / float(m_data_len))
        Logger.log_more_verbose("Making '%s' test." % (l_wordlist))
        i += 1

        # Analyze for each wordlist
        #
        # Store the server banner
        try:
            m_banners_counter[l_response.headers["Server"]] += l_weight
        except KeyError:
            pass

        #
        # =====================
        # HTTP directly related
        # =====================
        #
        #
        for l_http_header_name, l_header_wordlist in m_wordlists_HTTP_fields.iteritems():

            # Check if HTTP header field is in response
            if l_http_header_name not in l_response.headers:
                continue

            l_curr_header_value = l_response.headers[l_http_header_name]

            # Generate concrete wordlist name
            l_wordlist_path     = Config.plugin_extra_config[l_wordlist][l_header_wordlist]

            # Load words for the wordlist
            l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(l_wordlist_path)
            # Looking for matches
            l_matches           = l_wordlist_instance.matches_by_value(l_curr_header_value)

            m_counters.inc(l_matches, l_action, l_weight, l_http_header_name, message="HTTP field: " + l_curr_header_value)

        #
        # =======================
        # HTTP INdirectly related
        # =======================
        #
        #

        #
        # Status code
        # ===========
        #
        l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["statuscode"])
        # Looking for matches
        l_matches           = l_wordlist_instance.matches_by_value(l_response.status)

        m_counters.inc(l_matches, l_action, l_weight, "statuscode", message="Status code: " + l_response.status)


        #
        # Status text
        # ===========
        #
        l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["statustext"])
        # Looking for matches
        l_matches           = l_wordlist_instance.matches_by_value(l_response.reason)

        m_counters.inc(l_matches, l_action, l_weight, "statustext", message="Status text: " + l_response.reason)


        #
        # Header space
        # ============
        #
        # Count the number of spaces between HTTP field name and their value, for example:
        # -> Server: Apache 1
        # The number of spaces are: 1
        #
        # -> Server:Apache 1
        # The number of spaces are: 0
        #
        l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["header-space"])
        # Looking for matches
        try:
            l_http_value        = l_response.headers[0] # get the value of first HTTP field
            l_spaces_num        = str(abs(len(l_http_value) - len(l_http_value.lstrip())))
            l_matches           = l_wordlist_instance.matches_by_value(l_spaces_num)

            m_counters.inc(l_matches, l_action, l_weight, "header-space", message="Header space: " + l_spaces_num)

        except IndexError:
            print "index error header space"
            pass


        #
        # Header capitalafterdash
        # =======================
        #
        # Look for non capitalized first letter of field name, for example:
        # -> Content-type: ....
        # Instead of:
        # -> Content-Type: ....
        #
        l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["header-capitalafterdash"])
        # Looking for matches
        l_valid_fields     = [x for x in l_response.headers.iterkeys() if "-" in x]

        if l_valid_fields:

            l_h = l_valid_fields[0]

            l_value = l_h.split("-")[1] # Get the second value: Content-type => type
            l_dush  = None

            if l_value[0].isupper(): # Check first letter is lower
                l_dush = 1
            else:
                l_dush = 0

            l_matches           = l_wordlist_instance.matches_by_value(l_dush)
            m_counters.inc(l_matches, l_action, l_weight, "header-capitalizedafterdush", message="Capital after dash: %s" % str(l_dush))

        #
        # Header order
        # ============
        #
        l_header_order  = ','.join(l_response.headers.iterkeys())

        l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["header-order"])
        l_matches           = l_wordlist_instance.matches_by_value(l_header_order)

        m_counters.inc(l_matches, l_action, l_weight, "header-order", message="Header order: " + l_header_order)


        #
        # Protocol name
        # ============
        #
        # For a response like:
        # -> HTTP/1.0 200 OK
        #    ....
        #
        # Get the 'HTTP' value.
        #
        try:
            l_proto             = l_response.protocol # Get the 'HTTP' text from response, if available
            if l_proto:
                l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["protocol-name"])
                l_matches           = l_wordlist_instance.matches_by_value(l_proto)

                m_counters.inc(l_matches, l_action, l_weight, "proto-name", message="Proto name: " + l_proto)

        except IndexError:
            print "index error protocol name"
            pass


        #
        # Protocol version
        # ================
        #
        # For a response like:
        # -> HTTP/1.0 200 OK
        #    ....
        #
        # Get the '1.0' value.
        #
        try:
            l_version           = l_response.version # Get the '1.0' text from response, if available
            if l_version:
                l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["protocol-version"])
                l_matches           = l_wordlist_instance.matches_by_value(l_version)

                m_counters.inc(l_matches, l_action, l_weight, "proto-version", message="Proto version: " + l_version)

        except IndexError:
            print "index error protocol version"
            pass



        if "ETag" in l_response.headers:
            l_etag_header       = l_response.headers["ETag"]
            #
            # ETag length
            # ================
            #
            l_etag_len          = len(l_etag_header)
            l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["etag-legth"])
            l_matches           = l_wordlist_instance.matches_by_value(l_etag_len)

            m_counters.inc(l_matches, l_action, l_weight, "etag-length", message="ETag length: " + str(l_etag_len))


            #
            # ETag Quotes
            # ================
            #
            l_etag_striped          = l_etag_header.strip()
            if l_etag_striped.startswith("\"") or l_etag_striped.startswith("'"):
                l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["etag-quotes"])
                l_matches           = l_wordlist_instance.matches_by_value(l_etag_striped[0])

                m_counters.inc(l_matches, l_action, l_weight, "etag-quotes", message="Etag quotes: " + l_etag_striped[0])

        if "Vary" in l_response.headers:
            l_vary_header       = l_response.headers["Vary"]
            #
            # Vary delimiter
            # ================
            #
            # Checks if Vary header delimiter is something like this:
            # -> Vary: Accept-Encoding,User-Agent
            # Or this:
            # -> Vary: Accept-Encoding, User-Agent
            #
            l_var_delimiter     = ", " if l_vary_header.find(", ") else ","
            l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["vary-delimiter"])
            l_matches           = l_wordlist_instance.matches_by_value(l_var_delimiter)

            m_counters.inc(l_matches, l_action, l_weight, "vary-delimiter", message="Vary delimiter: " + l_var_delimiter)

            #
            # Vary capitalizer
            # ================
            #
            # Checks if Vary header delimiter is something like this:
            # -> Vary: Accept-Encoding,user-Agent
            # Or this:
            # -> Vary: accept-encoding,user-agent
            #
            l_vary_capitalizer  = str(0 if l_vary_header == l_vary_header.lower() else 1)
            l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["vary-capitalize"])
            l_matches           = l_wordlist_instance.matches_by_value(l_vary_capitalizer)

            m_counters.inc(l_matches, l_action, l_weight, "vary-capitalize", message="Vary capitalizer: " + l_vary_capitalizer)


            #
            # Vary order
            # ================
            #
            # Checks order between vary values:
            # -> Vary: Accept-Encoding,user-Agent
            # Or this:
            # -> Vary: User-Agent,Accept-Encoding
            #
            l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["vary-order"])
            l_matches           = l_wordlist_instance.matches_by_value(l_vary_header)

            m_counters.inc(l_matches, l_action, l_weight, "vary-order", message="Vary order: " + l_vary_header)


        #
        # =====================
        # HTTP specific options
        # =====================
        #
        #
        if l_action == "HEAD":
            #
            # HEAD Options
            # ============
            #
            l_option            = l_response.headers.get("Allow")
            if l_option:
                l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-public"])
                # Looking for matches
                l_matches           = l_wordlist_instance.matches_by_value(l_option)

                m_counters.inc(l_matches, l_action, l_weight, "options-allow", message="HEAD option: " + l_option)


        if l_action == "OPTIONS" or l_action == "INVALID" or l_action == "DELETE":
            if "Allow" in l_response.headers:
                #
                # Options allow
                # =============
                #
                l_option            = l_response.headers.get("Allow")
                if l_option:
                    l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-public"])
                    # Looking for matches
                    l_matches           = l_wordlist_instance.matches_by_value(l_option)

                    m_counters.inc(l_matches, l_action, l_weight, "options-allow", message="OPTIONS allow: "  + l_action + " # " + l_option)


                #
                # Allow delimiter
                # ===============
                #
                l_option            = l_response.headers.get("Allow")
                if l_option:
                    l_var_delimiter     = ", " if l_option.find(", ") else ","
                    l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-delimited"])
                    # Looking for matches
                    l_matches           = l_wordlist_instance.matches_by_value(l_var_delimiter)

                    m_counters.inc(l_matches, l_action, l_weight, "options-delimiter", message="OPTION allow delimiter " + l_action + " # " + l_option)


            if "Public" in l_response.headers:
                #
                # Public response
                # ===============
                #
                l_option            = l_response.headers.get("Public")
                if l_option:
                    l_wordlist_instance = WordListLoader.get_advanced_wordlist_as_dict(Config.plugin_extra_config[l_wordlist]["options-public"])
                    # Looking for matches
                    l_matches           = l_wordlist_instance.matches_by_value(l_option)

                    m_counters.inc(l_matches, l_action, l_weight, "options-public", message="Public response: " + l_action + " # " + l_option)
Exemple #21
0
            m_analyzer = MatchingAnalyzer(m_response_error_page.data)
            m_total = len(m_discovered_urls)
            for m_step, l_url in enumerate(m_discovered_urls):

                # Update only odd iterations
                if m_step % 2:
                    progress = (float(m_step * 100) / m_total)
                    self.update_status(progress=progress)
                l_url = fix_url(l_url, m_url)
                if l_url in Config.audit_scope:
                    l_p = None
                    try:
                        l_p = HTTP.get_url(l_url, callback=self.check_response)  # FIXME handle exceptions!
                    except:
                        if l_p:
                            discard_data(l_p)
                        continue
                    if l_p:
                        if m_analyzer.analyze(l_p.data, url=l_url):
                            match[l_url] = l_p
                        else:
                            discard_data(l_p)

            # Generate results
            for i in m_analyzer.unique_texts:
                l_url    = i.url
                l_p      = match.pop(l_url)
                m_result = Url(l_url, referer=m_url)
                m_result.add_information(l_p)
                m_return.append(m_result)
                m_return.append(l_p)
Exemple #22
0
    def is_URL_in_windows(self, main_url):
        """
        Detect if platform is Windows or \*NIX. To do this, get the first link, in scope, and
        does two resquest. If are the same response, then, platform are Windows. Else are \*NIX.

        :returns: True, if the remote host is a Windows system. False is \*NIX or None if unknown.
        :rtype: bool
        """
        m_forbidden = (
            "logout",
            "logoff",
            "exit",
            "sigout",
            "signout",
        )

        # Get the main web page
        m_r = download(main_url, callback=self.check_download)
        if not m_r:
            return None
        discard_data(m_r)

        # Get the first link
        if m_r.information_type == Information.INFORMATION_HTML:
            m_links = extract_from_html(m_r.raw_data, main_url)
        else:
            m_links = extract_from_text(m_r.raw_data, main_url)

        if not m_links:
            return None

        # Get the first link of the page that's in scope of the audit
        m_first_link = None
        for u in m_links:
            if u in Config.audit_scope and not any(x in u
                                                   for x in m_forbidden):
                m_first_link = u
                break

        if not m_first_link:
            return None

        # Now get two request to the links. One to the original URL and other
        # as upper URL.

        # Original
        m_response_orig = HTTP.get_url(
            m_first_link,
            callback=self.check_response)  # FIXME handle exceptions!
        discard_data(m_response_orig)
        # Uppercase
        m_response_upper = HTTP.get_url(
            m_first_link.upper(),
            callback=self.check_response)  # FIXME handle exceptions!
        discard_data(m_response_upper)
        # Compare them
        m_orig_data = m_response_orig.raw_response if m_response_orig else ""
        m_upper_data = m_response_upper.raw_response if m_response_upper else ""
        m_match_level = get_diff_ratio(m_orig_data, m_upper_data)

        # If the responses are equal by 90%, two URL are the same => Windows; else => *NIX
        m_return = None
        if m_match_level > 0.95:
            m_return = True
        else:
            m_return = False

        return m_return
Exemple #23
0
    def is_URL_in_windows(self, main_url):
        """
        Detect if platform is Windows or \*NIX. To do this, get the first link, in scope, and
        does two resquest. If are the same response, then, platform are Windows. Else are \*NIX.

        :returns: True, if the remote host is a Windows system. False is \*NIX or None if unknown.
        :rtype: bool
        """
        m_forbidden = (
            "logout",
            "logoff",
            "exit",
            "sigout",
            "signout",
        )

        # Get the main web page
        m_r = download(main_url, callback=self.check_download)
        if not m_r:
            return None
        discard_data(m_r)

        # Get the first link
        if m_r.information_type == Information.INFORMATION_HTML:
            m_links = extract_from_html(m_r.raw_data, main_url)
        else:
            m_links = extract_from_text(m_r.raw_data, main_url)

        if not m_links:
            return None

        # Get the first link of the page that's in scope of the audit
        m_first_link = None
        for u in m_links:
            if u in Config.audit_scope and not any(x in u for x in m_forbidden):
                m_first_link = u
                break

        if not m_first_link:
            return None

        # Now get two request to the links. One to the original URL and other
        # as upper URL.

        # Original
        m_response_orig  = HTTP.get_url(m_first_link, callback=self.check_response)  # FIXME handle exceptions!
        discard_data(m_response_orig)
        # Uppercase
        m_response_upper = HTTP.get_url(m_first_link.upper(), callback=self.check_response)  # FIXME handle exceptions!
        discard_data(m_response_upper)
        # Compare them
        m_orig_data      = m_response_orig.raw_response  if m_response_orig  else ""
        m_upper_data     = m_response_upper.raw_response if m_response_upper else ""
        m_match_level    = get_diff_ratio(m_orig_data, m_upper_data)

        # If the responses are equal by 90%, two URL are the same => Windows; else => *NIX
        m_return = None
        if m_match_level > 0.95:
            m_return = True
        else:
            m_return = False

        return m_return
    # Check if the url is acceptable by comparing
    # the result content.
    #
    # If the maching level between the error page
    # and this url is greater than 52%, then it's
    # the same URL and must be discarded.
    #
    if p and p.status == "200":

        # If the method used to get URL was HEAD, get complete URL
        if method != "GET":
            try:
                p = HTTP.get_url(url, use_cache=False, method="GET")
                if p:
                    discard_data(p)
            except Exception, e:
                Logger.log_more_verbose("Error while processing: '%s': %s" % (url, str(e)))

        # Append for analyze and display info if is accepted
        if matcher.analyze(p.raw_response, url=url, risk=risk_level):
            updater_func(text="Discovered partial url: '%s'" % url)


#----------------------------------------------------------------------
#
# Aux functions
#
#----------------------------------------------------------------------
def load_wordlists(wordlists):
    """
Exemple #25
0
    def __find_plugins(self, url, plugins_wordlist, update_func):
        """
        Try to find available plugins

        :param url: base URL to test.
        :type url: str

        :param plugins_wordlist: path to wordlist with plugins lists.
        :type plugins_wordlist: str

        :param update_func: function to update plugin status.
        :type update_func: function

        :return: list of lists as format:
                 list([PLUGIN_NAME, PLUGIN_URL, PLUGIN_INSTALLED_VERSION, PLUGIN_LAST_VERSION, [CVE1, CVE2...]])
        :type: list(list())
        """
        results = []
        urls_to_test = {
            "readme.txt": r"(Stable tag:[\svV]*)([0-9\.]+)",
            "README.txt": r"(Stable tag:[\svV]*)([0-9\.]+)",
        }

        # Generates the error page
        error_response = get_error_page(url).raw_data

        # Load plugins info
        plugins = []
        plugins_append = plugins.append
        with open(plugins_wordlist, "rU") as f:
            for x in f:
                plugins_append(x.replace("\n", ""))

        # Calculate sizes
        total_plugins = len(plugins)

        # Load CSV info
        csv_info = csv.reader(plugins)

        # Process the URLs
        for i, plugin_row in enumerate(csv_info):

            # Plugin properties
            plugin_URI = plugin_row[0]
            plugin_name = plugin_row[1]
            plugin_last_version = plugin_row[2]
            plugin_CVEs = [] if plugin_row[3] == "" else plugin_row[3].split(
                "|")

            # Update status
            update_func((float(i) * 100.0) / float(total_plugins))

            # Make plugin URL
            partial_plugin_url = "%s/%s" % (url, "wp-content/plugins/%s" %
                                            plugin_URI)

            # Test each URL with possible plugin version info
            for target, regex in urls_to_test.iteritems():

                plugin_url = "%s/%s" % (partial_plugin_url, target)

                # Try to get plugin
                p = None
                try:
                    p = HTTP.get_url(plugin_url, use_cache=False)
                    if p:
                        discard_data(p)
                except Exception, e:
                    Logger.log_error_more_verbose(
                        "Error while download: '%s': %s" %
                        (plugin_url, str(e)))
                    continue

                plugin_installed_version = None

                if p.status == "403":  # Installed, but inaccesible
                    plugin_installed_version = "Unknown"

                elif p.status == "200":

                    # Check if page is and non-generic not found page with 404 code
                    if get_diff_ratio(error_response, p.raw_response) < 0.52:

                        # Find the version
                        tmp_version = re.search(regex, p.raw_response)

                        if tmp_version is not None:
                            plugin_installed_version = tmp_version.group(2)

                # Store info
                if plugin_installed_version is not None:
                    Logger.log(
                        "Discovered plugin: '%s (installed version: %s)' (latest version: %s)"
                        % (plugin_name, plugin_installed_version,
                           plugin_last_version))
                    results.append([
                        plugin_name, plugin_url, plugin_installed_version,
                        plugin_last_version, plugin_CVEs
                    ])

                    # Plugin found -> not more URL test for this plugin
                    break
Exemple #26
0
            m_total = len(m_discovered_urls)
            for m_step, l_url in enumerate(m_discovered_urls):

                # Update only odd iterations
                if m_step % 2:
                    progress = (float(m_step * 100) / m_total)
                    self.update_status(progress=progress)
                l_url = fix_url(l_url, m_url)
                if l_url in Config.audit_scope:
                    l_p = None
                    try:
                        l_p = HTTP.get_url(l_url, callback=self.check_response
                                           )  # FIXME handle exceptions!
                    except:
                        if l_p:
                            discard_data(l_p)
                        continue
                    if l_p:
                        if m_analyzer.analyze(l_p.data, url=l_url):
                            match[l_url] = l_p
                        else:
                            discard_data(l_p)

            # Generate results
            for i in m_analyzer.unique_texts:
                l_url = i.url
                l_p = match.pop(l_url)
                m_result = URL(l_url, referer=m_url)
                m_result.add_information(l_p)
                m_return.append(m_result)
                m_return.append(l_p)
Exemple #27
0
    def run(self, info):

        # If it's an URL...
        if info.is_instance(BaseURL):
            target = URL(info.url)
            discard_data(target)

            # Get the hostname to test.
            hostname = info.hostname

            # If it's HTTPS, use the port number from the URL.
            if info.is_https:
                port = info.parsed_url.port

            # Otherwise, assume the port is 443.
            else:
                port = 443

            # Test this port.
            is_vulnerable = self.test(hostname, port)

        # If it's a service fingerprint...
        elif info.is_instance(Relationship(IP, ServiceFingerprint)):
            ip, fp = info.instances
            target = ip
            port = fp.port
            starttls = False

            # Ignore if the port does not respond directly to SSL...
            if fp.protocol != "SSL":

                # If it's SMTP, we need to issue a STARTTLS command first.
                if fp.name == "smtp":
                    starttls = True

                # Ignore if the port does not support SSL.
                else:
                    Logger.log_more_verbose(
                        "No SSL services found in fingerprint [%s] for IP %s,"
                        " aborting." % (fp, ip))
                    return

            # Test this port.
            is_vulnerable = self.test(ip.address, port, starttls=starttls)

        # Internal error!
        else:
            assert False, "Unexpected data type received: %s" % type(info)

        # If it's vulnerable, report the vulnerability.
        if is_vulnerable:
            title = "OpenSSL Heartbleed Vulnerability"
            description = "An unpatched OpenSSL service was found that's" \
                          " vulnerable to the Heartbleed vulnerability" \
                          " (CVE-2014-0162). This vulnerability allows an" \
                          " attacker to dump the memory contents of the" \
                          " service running the flawed version of the" \
                          " OpenSSL library, potentially compromising" \
                          " usernames, passwords, private keys and other" \
                          " sensitive data."
            references = ["http://heartbleed.com/"]
            cve = ["CVE-2014-0162",],
            if target.is_instance(IP):
                vuln = VulnerableService(
                    target      = target,
                    port        = port,
                    protocol    = "TCP",
                    title       = title,
                    description = description,
                    references  = references,
                    cve         = cve,
                )
            elif target.is_instance(URL):
                vuln = VulnerableWebApp(
                    target      = target,
                    title       = title,
                    description = description,
                    references  = references,
                    cve         = cve,
                )
            else:
                assert False, "Internal error!"
            return vuln
def http_simple_analyzer(main_url, update_status_func, number_of_entries=4):
    """Simple method to get fingerprint server info

    :param main_url: Base url to test.
    :type main_url: str

    :param update_status_func: function used to update the status of the process
    :type update_status_func: function

    :param number_of_entries: number of resutls tu return for most probable web servers detected.
    :type number_of_entries: int

    :return: a typle as format: Web server family, Web server version, Web server complete description, related web servers (as a dict('SERVER_RELATED' : set(RELATED_NAMES))), others web server with their probabilities as a dict(CONCRETE_WEB_SERVER, PROBABILITY)
    """

    m_actions = {
        'GET'        : { 'wordlist' : 'Wordlist_get'            , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/' },
        'LONG_GET'   : { 'wordlist' : 'Wordlist_get_long'       , 'weight' : 1 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/%s' % ('a' * 200) },
        'NOT_FOUND'  : { 'wordlist' : 'Wordlist_get_notfound'   , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': '/404_NOFOUND__X02KAS' },
        'HEAD'       : { 'wordlist' : 'Wordlist_head'           , 'weight' : 3 , 'protocol' : 'HTTP/1.1', 'method' : 'HEAD'     , 'payload': '/' },
        'OPTIONS'    : { 'wordlist' : 'Wordlist_options'        , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'OPTIONS'  , 'payload': '/' },
        'DELETE'     : { 'wordlist' : 'Wordlist_delete'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'DELETE'   , 'payload': '/' },
        'TEST'       : { 'wordlist' : 'Wordlist_attack'         , 'weight' : 5 , 'protocol' : 'HTTP/1.1', 'method' : 'TEST'     , 'payload': '/' },
        'INVALID'    : { 'wordlist' : 'Wordlist_wrong_method'   , 'weight' : 5 , 'protocol' : 'HTTP/9.8', 'method' : 'GET'      , 'payload': '/' },
        'ATTACK'     : { 'wordlist' : 'Wordlist_wrong_version'  , 'weight' : 2 , 'protocol' : 'HTTP/1.1', 'method' : 'GET'      , 'payload': "/etc/passwd?format=%%%%&xss=\x22><script>alert('xss');</script>&traversal=../../&sql='%20OR%201;"}
    }

    m_d                   = ParsedURL(main_url)
    m_hostname            = m_d.hostname
    m_port                = m_d.port
    m_debug               = False # Only for develop
    i                     = 0
    m_counters            = HTTPAnalyzer()
    m_data_len            = len(m_actions) # Var used to update the status
    m_banners_counter     = Counter()

    for l_action, v in m_actions.iteritems():
        if m_debug:
            print "###########"
        l_method      = v["method"]
        l_payload     = v["payload"]
        l_proto       = v["protocol"]
        #l_wordlist    = v["wordlist"]

        # Each type of probe hast different weight.
        #
        # Weights go from 0 - 5
        #
        l_weight      = v["weight"]

        # Make the raw request
        l_raw_request = "%(method)s %(payload)s %(protocol)s\r\nHost: %(host)s\r\n\r\n" % (
            {
                "method"     : l_method,
                "payload"    : l_payload,
                "protocol"   : l_proto,
                "host"       : m_hostname,
                "port"       : m_port
            }
        )
        if m_debug:
            print "REQUEST"
            print l_raw_request

        # Do the connection
        l_response = None
        try:
            m_raw_request = HTTP_Raw_Request(l_raw_request)
            discard_data(m_raw_request)
            l_response = HTTP.make_raw_request(
                host        = m_hostname,
                port        = m_port,
                raw_request = m_raw_request,
                callback    = check_raw_response)
            if l_response:
                discard_data(l_response)
        except NetworkException,e:
            Logger.log_error_more_verbose("Server-Fingerprint plugin: No response for URL (%s) with method '%s'. Message: %s" % (m_hostname, l_method, str(e)))
            continue

        if not l_response:
            Logger.log_error_more_verbose("No response for host '%s' with method '%s'." % (m_hostname, l_method))
            continue

        if m_debug:
            print "RESPONSE"
            print l_response.raw_headers


        # Update the status
        update_status_func((float(i) * 100.0) / float(m_data_len))
        Logger.log_more_verbose("Making '%s' test." % l_method)
        i += 1

        # Analyze for each wordlist
        #
        # Store the server banner
        try:
            m_banners_counter[l_response.headers["Server"]] += l_weight
        except KeyError:
            pass

        l_server_name = None
        try:
            l_server_name = l_response.headers["Server"]
        except KeyError:
            continue

        m_counters.simple_inc(l_server_name, l_method, l_weight)
class OSFingerprinting(TestingPlugin):
    """
    Plugin to fingerprint the remote OS.
    """

    #----------------------------------------------------------------------
    def get_accepted_info(self):
        return [IP, BaseUrl]

    #----------------------------------------------------------------------
    def recv_info(self, info):
        """
        Main function for OS fingerprint. Get a domain or IP and return the fingerprint results.

        :param info: Folder URL.
        :type info: FolderUrl

        :return: OS Fingerprint.
        :rtype: OSFingerprint
        """

        #
        # Detection methods and their weights.
        #
        # The weight is a value between 1-5
        #

        FINGERPRINT_METHODS_OS_AND_VERSION = {
            'ttl': {
                'function': self.ttl_platform_detection,
                'weight': 2
            }
        }

        FUNCTIONS = None  # Fingerprint methods to run
        m_host = None

        is_windows = None

        if isinstance(info, IP):
            m_host = info.address
            FUNCTIONS = ['ttl']
        else:  # BaseUrl
            m_host = info.hostname
            FUNCTIONS = ['ttl']

            # Try to detect if remote system is a Windows
            m_windows_host = "%s://%s:%s" % (info.parsed_url.scheme,
                                             info.parsed_url.host,
                                             info.parsed_url.port)
            is_windows = self.is_URL_in_windows(m_windows_host)

        # Logging
        Logger.log_more_verbose(
            "Starting OS fingerprinting plugin for site: %s" % m_host)

        m_counter = Counter()
        # Run functions
        for f in FUNCTIONS:
            l_function = FINGERPRINT_METHODS_OS_AND_VERSION[f]['function']

            ### For future use
            ### l_weight     = FINGERPRINT_METHODS_OS_AND_VERSION[f]['weight']

            # Run
            results = l_function(m_host)

            if results:
                for l_r in results:
                    m_counter[l_r] += 1

        # Return value
        m_return = None

        #
        # Filter the results
        #
        if len(m_counter) > 0:
            # Fooking for a windows system
            if is_windows:  # If Windows is detected
                l_counter = Counter()

                # Extract windows systems
                for x, y in m_counter.iteritems():
                    if "windows" == x:
                        l_counter[x] += y

                # Replace the counter for the new
                m_counter = l_counter

            # Get most common systems
            l_most_common = m_counter.most_common(5)

            # First elemente will be the detected OS
            m_OS_family = l_most_common[0][0][0]
            m_OS_version = l_most_common[0][0][1]

            # Next 4 will be the 'others'
            m_length = float(len(l_most_common))
            m_others = {
                "%s-%s" % (l_most_common[i][0][0], l_most_common[i][0][1]):
                float('{:.2f}'.format(l_most_common[i][1] / m_length))
                for i in xrange(1, len(l_most_common), 1)
            }

            # create the data
            m_return = OSFingerprint(m_OS_family,
                                     m_OS_version,
                                     others=m_others)

        elif is_windows is not None:
            if is_windows:  # Windows system detected
                m_return = OSFingerprint("windows")
            else:  # *NIX system detected
                m_return = OSFingerprint("unix_or_compatible")

        # If there is information, associate it with the resource
        if m_return:
            info.add_information(m_return)

        return m_return

    #----------------------------------------------------------------------
    #
    # Platform detection methods
    #
    #----------------------------------------------------------------------
    def is_URL_in_windows(self, main_url):
        """
        Detect if platform is Windows or \*NIX. To do this, get the first link, in scope, and
        does two resquest. If are the same response, then, platform are Windows. Else are \*NIX.

        :returns: True, if the remote host is a Windows system. False is \*NIX or None if unknown.
        :rtype: bool
        """
        m_forbidden = (
            "logout",
            "logoff",
            "exit",
            "sigout",
            "signout",
        )

        # Get the main web page
        m_r = download(main_url, callback=self.check_download)
        if not m_r or not m_r.raw_data:
            return None
        discard_data(m_r)

        # Get the first link
        m_links = None
        try:
            if m_r.information_type == Information.INFORMATION_HTML:
                m_links = extract_from_html(m_r.raw_data, main_url)
            else:
                m_links = extract_from_text(m_r.raw_data, main_url)
        except TypeError, e:
            Logger.log_error_more_verbose("Plugin error: %s" % format_exc())
            return None

        if not m_links:
            return None

        # Get the first link of the page that's in scope of the audit
        m_first_link = None
        for u in m_links:
            if u in Config.audit_scope and not any(x in u
                                                   for x in m_forbidden):
                m_first_link = u
                break

        if not m_first_link:
            return None

        # Now get two request to the links. One to the original URL and other
        # as upper URL.

        # Original
        m_response_orig = HTTP.get_url(
            m_first_link,
            callback=self.check_response)  # FIXME handle exceptions!
        discard_data(m_response_orig)
        # Uppercase
        m_response_upper = HTTP.get_url(
            m_first_link.upper(),
            callback=self.check_response)  # FIXME handle exceptions!
        discard_data(m_response_upper)
        # Compare them
        m_orig_data = m_response_orig.raw_response if m_response_orig else ""
        m_upper_data = m_response_upper.raw_response if m_response_upper else ""
        m_match_level = get_diff_ratio(m_orig_data, m_upper_data)

        # If the responses are equal by 90%, two URL are the same => Windows; else => *NIX
        m_return = None
        if m_match_level > 0.95:
            m_return = True
        else:
            m_return = False

        return m_return
Exemple #30
0
    # Check if the url is acceptable by comparing
    # the result content.
    #
    # If the maching level between the error page
    # and this url is greater than 52%, then it's
    # the same URL and must be discarded.
    #
    if p and p.status == "200":

        # If the method used to get URL was HEAD, get complete URL
        if method != "GET":
            try:
                p = HTTP.get_url(url, use_cache=False, method="GET")
                if p:
                    discard_data(p)
            except Exception, e:
                Logger.log_error_more_verbose(
                    "Error while processing: '%s': %s" % (url, str(e)))

        # Append for analyze and display info if is accepted
        if matcher.analyze(p.raw_response, url=url, risk=risk_level):
            Logger.log_more_verbose("Discovered partial url: '%s'" % url)


#------------------------------------------------------------------------------
#
# Aux functions
#
#------------------------------------------------------------------------------
def load_wordlists(wordlists):
Exemple #31
0
    def __find_plugins(self, url, plugins_wordlist, update_func):
        """
        Try to find available plugins

        :param url: base URL to test.
        :type url: str

        :param plugins_wordlist: path to wordlist with plugins lists.
        :type plugins_wordlist: str

        :param update_func: function to update plugin status.
        :type update_func: function

        :return: list of lists as format:
                 list([PLUGIN_NAME, PLUGIN_URL, PLUGIN_INSTALLED_VERSION, PLUGIN_LAST_VERSION, [CVE1, CVE2...]])
        :type: list(list())
        """
        results = []
        urls_to_test = {
            "readme.txt": r"(Stable tag:[\svV]*)([0-9\.]+)",
            "README.txt": r"(Stable tag:[\svV]*)([0-9\.]+)",
        }

        # Generates the error page
        error_response = get_error_page(url).raw_data

        # Load plugins info
        plugins = []
        plugins_append = plugins.append
        with open(plugins_wordlist, "rU") as f:
            for x in f:
                plugins_append(x.replace("\n", ""))

        # Calculate sizes
        total_plugins = len(plugins)

        # Load CSV info
        csv_info = csv.reader(plugins)

        # Process the URLs
        for i, plugin_row in enumerate(csv_info):

            # Plugin properties
            plugin_URI = plugin_row[0]
            plugin_name = plugin_row[1]
            plugin_last_version = plugin_row[2]
            plugin_CVEs = [] if plugin_row[3] == "" else plugin_row[3].split("|")

            # Update status
            update_func((float(i) * 100.0) / float(total_plugins))

            # Make plugin URL
            partial_plugin_url = "%s/%s" % (url, "wp-content/plugins/%s" % plugin_URI)

            # Test each URL with possible plugin version info
            for target, regex in urls_to_test.iteritems():

                plugin_url = "%s/%s" % (partial_plugin_url, target)

                # Try to get plugin
                p = None
                try:
                    p = HTTP.get_url(plugin_url, use_cache=False)
                    if p:
                        discard_data(p)
                except Exception, e:
                    Logger.log_error_more_verbose("Error while download: '%s': %s" % (plugin_url, str(e)))
                    continue

                plugin_installed_version = None

                if p.status == "403":  # Installed, but inaccesible
                    plugin_installed_version = "Unknown"

                elif p.status == "200":

                    # Check if page is and non-generic not found page with 404 code
                    if get_diff_ratio(error_response, p.raw_response) < 0.52:

                        # Find the version
                        tmp_version = re.search(regex, p.raw_response)

                        if tmp_version is not None:
                            plugin_installed_version = tmp_version.group(2)

                # Store info
                if plugin_installed_version is not None:
                    Logger.log("Discovered plugin: '%s (installed version: %s)' (latest version: %s)" %
                                (plugin_name, plugin_installed_version, plugin_last_version))
                    results.append([
                        plugin_name,
                        plugin_url,
                        plugin_installed_version,
                        plugin_last_version,
                        plugin_CVEs
                    ])

                    # Plugin found -> not more URL test for this plugin
                    break
Exemple #32
0
class ShodanPlugin(TestingPlugin):
    """
    This plugin tries to perform passive reconnaissance on a target using
    the Shodan web API.
    """


    #--------------------------------------------------------------------------
    def check_params(self):

        # Make sure we have an API key.
        self.get_api_key()


    #--------------------------------------------------------------------------
    def get_accepted_types(self):
        return [IP]


    #--------------------------------------------------------------------------
    def get_api_key(self):
        key = Config.plugin_args.get("apikey", None)
        if not key:
            key = Config.plugin_config.get("apikey", None)
        if not key:
            raise ValueError(
                "Missing API key! Get one at:"
                " http://www.shodanhq.com/api_doc")
        return key


    #--------------------------------------------------------------------------
    def run(self, info):

        # This is where we'll collect the data we'll return.
        results = []

        # Skip unsupported IP addresses.
        if info.version != 4:
            return
        ip = info.address
        parsed = netaddr.IPAddress(ip)
        if parsed.is_loopback() or \
           parsed.is_private()  or \
           parsed.is_link_local():
            return

        # Query Shodan for this host.
        try:
            key = self.get_api_key()
            api = WebAPI(key)
            shodan = api.host(ip)
        except Exception, e:
            tb = traceback.format_exc()
            Logger.log_error("Error querying Shodan for host %s: %s" % (ip, str(e)))
            Logger.log_error_more_verbose(tb)
            return

        # Make sure we got the same IP address we asked for.
        if ip != shodan.get("ip", ip):
            Logger.log_error(
                "Shodan gave us a different IP address... weird!")
            Logger.log_error_verbose(
                "Old IP: %s - New IP: %s" % (ip, shodan["ip"]))
            ip = to_utf8( shodan["ip"] )
            info = IP(ip)
            results.append(info)

        # Extract all hostnames and link them to this IP address.
        # Note: sometimes Shodan sends IP addresses here! (?)
        seen_host = {}
        for hostname in shodan.get("hostnames", []):
            if hostname == ip:
                continue
            if hostname in seen_host:
                domain = seen_host[hostname]
            else:
                try:
                    try:
                        host = IP(hostname)
                    except ValueError:
                        host = Domain(hostname)
                except Exception:
                    tb = traceback.format_exc()
                    Logger.log_error_more_verbose(tb)
                seen_host[hostname] = host
                results.append(host)
                domain = host
            domain.add_resource(info)

        # Get the OS fingerprint, if available.
        os = to_utf8( shodan.get("os") )
        if os:
            Logger.log("Host %s is running %s" % (ip, os))
            pass  # XXX TODO we'll need to reverse lookup the CPE

        # Get the GPS data, if available.
        # Complete any missing data using the default values.
        try:
            latitude  = float( shodan["latitude"]  )
            longitude = float( shodan["longitude"] )
        except Exception:
            latitude  = None
            longitude = None
        if latitude is not None and longitude is not None:
            area_code = shodan.get("area_code")
            if not area_code:
                area_code = None
            else:
                area_code = str(area_code)
            country_code = shodan.get("country_code")
            if not country_code:
                country_code = shodan.get("country_code3")
                if not country_code:
                    country_code = None
                else:
                    country_code = str(country_code)
            else:
                country_code = str(country_code)
            country_name = shodan.get("country_name")
            if not country_name:
                country_name = None
            city = shodan.get("city")
            if not city:
                city = None
            dma_code = shodan.get("dma_code")
            if not dma_code:
                dma_code = None
            else:
                dma_code = str(dma_code)
            postal_code = shodan.get("postal_code")
            if not postal_code:
                postal_code = None
            else:
                postal_code = str(postal_code)
            region_name = shodan.get("region_name")
            if not region_name:
                region_name = None
            geoip = Geolocation(
                latitude, longitude,
                country_code = country_code,
                country_name = country_name,
                region_name = region_name,
                city = city,
                zipcode = postal_code,
                metro_code = dma_code,
                area_code = area_code,
            )
            results.append(geoip)
            geoip.add_resource(info)

        # Go through every result and pick only the latest ones.
        latest = {}
        for data in shodan.get("data", []):
            if (
                not "banner" in data or
                not "ip" in data or
                not "port" in data or
                not "timestamp" in data
            ):
                Logger.log_error("Malformed results from Shodan?")
                from pprint import pformat
                Logger.log_error_more_verbose(pformat(data))
                continue
            key = (
                data["ip"],
                data["port"],
                data["banner"],
            )
            try:
                timestamp = reversed(   # DD.MM.YYYY -> (YYYY, MM, DD)
                    map(int, data["timestamp"].split(".", 2)))
            except Exception:
                continue
            if key not in latest or timestamp > latest[key][0]:
                latest[key] = (timestamp, data)

        # Process the latest results.
        seen_isp_or_org = set()
        seen_html = set()
        for _, data in latest.values():

            # Extract all domains, but don't link them.
            for hostname in data.get("domains", []):
                if hostname not in seen_host:
                    try:
                        domain = Domain(hostname)
                    except Exception:
                        tb = traceback.format_exc()
                        Logger.log_error_more_verbose(tb)
                        continue
                    seen_host[hostname] = domain
                    results.append(domain)

            # We don't have any use for this information yet,
            # but log it so at least the user can see it.
            isp = to_utf8( data.get("isp") )
            org = to_utf8( data.get("org") )
            if org and org not in seen_isp_or_org:
                seen_isp_or_org.add(org)
                Logger.log_verbose(
                    "Host %s belongs to: %s"
                    % (ip, org)
                )
            if isp and (not org or isp != org) and isp not in seen_isp_or_org:
                seen_isp_or_org.add(isp)
                Logger.log_verbose(
                    "IP address %s is provided by ISP: %s"
                    % (ip, isp)
                )

            # Get the HTML content, if available.
            raw_html = to_utf8( data.get("html") )
            if raw_html:
                hash_raw_html = hash(raw_html)
                if hash_raw_html not in seen_html:
                    seen_html.add(hash_raw_html)
                    try:
                        html = HTML(raw_html)
                    except Exception:
                        html = None
                        tb = traceback.format_exc()
                        Logger.log_error_more_verbose(tb)
                    if html:
                        html.add_resource(info)
                        results.append(html)

            # Get the banner, if available.
            raw_banner = to_utf8( data.get("banner") )
            try:
                port = int( data.get("port", "0") )
            except Exception:
                port = 0
            if raw_banner and port:
                try:
                    banner = Banner(info, raw_banner, port)
                except Exception:
                    banner = None
                    tb = traceback.format_exc()
                    Logger.log_error_more_verbose(tb)
                if banner:
                    results.append(banner)

        # Was this host located somewhere else in the past?
        for data in reversed(shodan.get("data", [])):
            try:
                timestamp = reversed(   # DD.MM.YYYY -> (YYYY, MM, DD)
                    map(int, data["timestamp"].split(".", 2)))
                old_location = data.get("location")
                if old_location:
                    old_latitude  = old_location.get("latitude",  latitude)
                    old_longitude = old_location.get("longitude", longitude)
                    if (
                        old_latitude is not None and
                        old_longitude is not None and
                        (old_latitude != latitude or old_longitude != longitude)
                    ):

                        # Get the geoip information.
                        area_code = old_location.get("area_code")
                        if not area_code:
                            area_code = None
                        country_code = old_location.get("country_code")
                        if not country_code:
                            country_code = old_location.get("country_code3")
                            if not country_code:
                                country_code = None
                        country_name = old_location.get("country_name")
                        if not country_name:
                            country_name = None
                        city = old_location.get("city")
                        if not city:
                            city = None
                        postal_code = old_location.get("postal_code")
                        if not postal_code:
                            postal_code = None
                        region_name = old_location.get("region_name")
                        if not region_name:
                            region_name = None
                        geoip = Geolocation(
                            latitude, longitude,
                            country_code = country_code,
                            country_name = country_name,
                            region_name = region_name,
                            city = city,
                            zipcode = postal_code,
                            area_code = area_code,
                        )

                        # If this is the first time we geolocate this IP,
                        # use this information as it if were up to date.
                        if latitude is None or longitude is None:
                            latitude  = old_latitude
                            longitude = old_longitude
                            results.append(geoip)
                            geoip.add_resource(info)

                        # Otherwise, just log the event.
                        else:
                            discard_data(geoip)
                            where = str(geoip)
                            when = datetime.date(*timestamp)
                            msg = "Host %s used to be located at %s on %s."
                            msg %= (ip, where, when.strftime("%B %d, %Y"))
                            Logger.log_verbose(msg)

            except Exception:
                tb = traceback.format_exc()
                Logger.log_error_more_verbose(tb)

        # Return the results.
        return results
Exemple #33
0
    def recv_info(self, info):

        m_domain = info.root

        # Skips localhost
        if m_domain == "localhost":
            return

        m_return = None

        # Checks if the hostname has been already processed
        if not self.state.check(m_domain):

            #
            # Looking for
            #
            m_subdomains = WordListLoader.get_advanced_wordlist_as_list(
                "subs_small.txt")

            # Run in parallel
            self.base_domain = m_domain
            self.completed = Counter(0)
            self.total = len(m_subdomains)
            r = pmap(self.get_subdomains_bruteforcer,
                     m_subdomains,
                     pool_size=10)

            #
            # Remove repeated
            #

            # The results
            m_domains = set()
            m_domains_add = m_domains.add
            m_domains_already = []
            m_domains_already_append = m_domains_already.append

            m_ips = set()
            m_ips_add = m_ips.add
            m_ips_already = []
            m_ips_already_append = m_ips_already.append

            if r:
                for doms in r:
                    for dom in doms:
                        # Domains
                        if dom.type == "CNAME":
                            if not dom.target in m_domains_already:
                                m_domains_already_append(dom.target)
                                if dom.target in Config.audit_scope:
                                    m_domains_add(dom)
                                else:
                                    discard_data(dom)

                        # IPs
                        if dom.type == "A":
                            if dom.address not in m_ips_already:
                                m_ips_already_append(dom.address)
                                m_ips_add(dom)

                # Unify
                m_domains.update(m_ips)

                m_return = m_domains

                # Add the information to the host
                map(info.add_information, m_return)

            # Set the domain as processed
            self.state.set(m_domain, True)

            Logger.log_verbose("DNS analyzer plugin found %d subdomains" %
                               len(m_return))

            # Write the info as more user friendly
            if Logger.MORE_VERBOSE:
                m_tmp = []
                m_tmp_append = m_tmp.append
                for x in m_return:
                    if getattr(x, "address", False):
                        m_tmp_append("%s (%s)" %
                                     (getattr(x, "address"), str(x)))
                    elif getattr(x, "target", False):
                        m_tmp_append("%s (%s)" %
                                     (getattr(x, "target"), str(x)))
                    else:
                        m_tmp_append(str(x))

                Logger.log_more_verbose("Subdomains found: \n\t+ %s" %
                                        "\n\t+ ".join(m_tmp))

        return m_return