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)
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_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)
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)