def _test_DNS(self, original_response, dns_wildcard_url): ''' Check if http://www.domain.tld/ == http://domain.tld/ ''' headers = Headers([('Host', dns_wildcard_url.get_domain())]) try: modified_response = self._uri_opener.GET( original_response.get_url(), cache=True, headers=headers) except w3afException: return else: if relative_distance_lt(modified_response.get_body(), original_response.get_body(), 0.35): desc = 'The target site has NO DNS wildcard, and the contents' \ ' of "%s" differ from the contents of "%s".' desc = desc % (dns_wildcard_url, original_response.get_url()) i = Info('No DNS wildcard', desc, modified_response.id, self.get_name()) i.set_url(dns_wildcard_url) kb.kb.append(self, 'dns_wildcard', i) om.out.information(i.get_desc()) else: desc = 'The target site has a DNS wildcard configuration, the'\ ' contents of "%s" are equal to the ones of "%s".' desc = desc % (dns_wildcard_url, original_response.get_url()) i = Info('DNS wildcard', desc, modified_response.id, self.get_name()) i.set_url(original_response.get_url()) kb.kb.append(self, 'dns_wildcard', i) om.out.information(i.get_desc())
def _analyze_crossdomain_clientaccesspolicy(self, url, response, file_name): try: dom = xml.dom.minidom.parseString(response.get_body()) except Exception: # Report this, it may be interesting for the final user # not a vulnerability per-se... but... it's information after all if 'allow-access-from' in response.get_body() or \ 'cross-domain-policy' in response.get_body() or \ 'cross-domain-access' in response.get_body(): desc = 'The "%s" file at: "%s" is not a valid XML.' desc = desc % (file_name, response.get_url()) i = Info('Invalid RIA settings file', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'info', i) om.out.information(i.get_desc()) else: if (file_name == 'crossdomain.xml'): url_list = dom.getElementsByTagName("allow-access-from") attribute = 'domain' if (file_name == 'clientaccesspolicy.xml'): url_list = dom.getElementsByTagName("domain") attribute = 'uri' for url in url_list: url = url.getAttribute(attribute) desc = 'The "%s" file at "%s" allows flash/silverlight'\ ' access from any site.' desc = desc % (file_name, response.get_url()) if url == '*': v = Vuln('Insecure RIA settings', desc, severity.LOW, response.id, self.get_name()) v.set_url(response.get_url()) v.set_method('GET') kb.kb.append(self, 'vuln', v) om.out.vulnerability(v.get_desc(), severity=v.get_severity()) else: i = Info('Cross-domain allow ACL', desc, response.id, self.get_name()) i.set_url(response.get_url()) i.set_method('GET') kb.kb.append(self, 'info', i) om.out.information(i.get_desc())
def _analyze_crossdomain_clientaccesspolicy(self, url, response, file_name): try: dom = xml.dom.minidom.parseString(response.get_body()) except Exception: # Report this, it may be interesting for the final user # not a vulnerability per-se... but... it's information after all if 'allow-access-from' in response.get_body() or \ 'cross-domain-policy' in response.get_body() or \ 'cross-domain-access' in response.get_body(): desc = 'The "%s" file at: "%s" is not a valid XML.' desc = desc % (file_name, response.get_url()) i = Info('Invalid RIA settings file', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'info', i) om.out.information(i.get_desc()) else: if(file_name == 'crossdomain.xml'): url_list = dom.getElementsByTagName("allow-access-from") attribute = 'domain' if(file_name == 'clientaccesspolicy.xml'): url_list = dom.getElementsByTagName("domain") attribute = 'uri' for url in url_list: url = url.getAttribute(attribute) desc = 'The "%s" file at "%s" allows flash/silverlight'\ ' access from any site.' desc = desc % (file_name, response.get_url()) if url == '*': v = Vuln('Insecure RIA settings', desc, severity.LOW, response.id, self.get_name()) v.set_url(response.get_url()) v.set_method('GET') kb.kb.append(self, 'vuln', v) om.out.vulnerability(v.get_desc(), severity=v.get_severity()) else: i = Info('Cross-domain allow ACL', desc, response.id, self.get_name()) i.set_url(response.get_url()) i.set_method('GET') kb.kb.append(self, 'info', i) om.out.information(i.get_desc())
def _analyze_author(self, response, frontpage_author): ''' Analyze the author URL. :param response: The http response object for the _vti_inf file. :param frontpage_author: A regex match object. :return: None. All the info is saved to the kb. ''' author_location = response.get_url().get_domain_path().url_join( frontpage_author.group(1)) # Check for anomalies in the location of author.exe if frontpage_author.group(1) != '_vti_bin/_vti_aut/author.exe': name = 'Customized frontpage configuration' desc = 'The FPAuthorScriptUrl is at: "%s" instead of the default'\ ' location: "/_vti_bin/_vti_adm/author.exe". This is very'\ ' uncommon.' desc = desc % author_location else: name = 'FrontPage FPAuthorScriptUrl' desc = 'The FPAuthorScriptUrl is at: "%s".' desc = desc % author_location i = Info(name, desc, response.id, self.get_name()) i.set_url(author_location) i['FPAuthorScriptUrl'] = author_location kb.kb.append(self, 'frontpage_version', i) om.out.information(i.get_desc())
def _fingerprint_meta(self, domain_path, wp_unique_url, response): ''' Check if the wp version is in index header ''' # Main scan URL passed from w3af + wp index page wp_index_url = domain_path.url_join('index.php') response = self._uri_opener.GET(wp_index_url, cache=True) # Find the string in the response html find = '<meta name="generator" content="[Ww]ord[Pp]ress (\d\.\d\.?\d?)" />' m = re.search(find, response.get_body()) # If string found, group version if m: version = m.group(1) # Save it to the kb! desc = 'WordPress version "%s" found in the index header.' desc = desc % version i = Info('Fingerprinted Wordpress version', desc, response.id, self.get_name()) i.set_url(wp_index_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc())
def _fingerprint_data(self, domain_path, wp_unique_url, response): ''' Find wordpress version from data ''' for wp_fingerprint in self._get_wp_fingerprints(): # The URL in the XML is relative AND it has two different variables # that we need to replace: # $wp-content$ -> wp-content/ # $wp-plugins$ -> wp-content/plugins/ path = wp_fingerprint.filepath path = path.replace('$wp-content$', 'wp-content/') path = path.replace('$wp-plugins$', 'wp-content/plugins/') test_url = domain_path.url_join(path) response = self._uri_opener.GET(test_url, cache=True) response_hash = hashlib.md5(response.get_body()).hexdigest() if response_hash == wp_fingerprint.hash: version = wp_fingerprint.version # Save it to the kb! desc = 'WordPress version "%s" fingerprinted by matching known md5'\ ' hashes to HTTP responses of static resources available at'\ ' the remote WordPress install.' desc = desc % version i = Info('Fingerprinted Wordpress version', desc, response.id, self.get_name()) i.set_url(test_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc()) break
def _force_disclosures(self, domain_path, potentially_vulnerable_paths): ''' :param domain_path: The path to wordpress' root directory :param potentially_vulnerable_paths: A list with the paths I'll URL-join with @domain_path, GET and parse. ''' for pvuln_path in potentially_vulnerable_paths: pvuln_url = domain_path.url_join(pvuln_path) response = self._uri_opener.GET(pvuln_url, cache=True) if is_404(response): continue response_body = response.get_body() if 'Fatal error: ' in response_body: desc = 'Analyze the HTTP response body to find the full path'\ ' where wordpress was installed.' i = Info('WordPress path disclosure', desc, response.id, self.get_name()) i.set_url(pvuln_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc()) break
def _do_request(self, url, mutant): ''' Perform a simple GET to see if the result is an error or not, and then run the actual fuzzing. ''' response = self._uri_opener.GET( mutant, cache=True, headers=self._headers) if not (is_404(response) or response.get_code() in (403, 401) or self._return_without_eval(mutant)): for fr in self._create_fuzzable_requests(response): self.output_queue.put(fr) # # Save it to the kb (if new)! # if response.get_url() not in self._seen and response.get_url().get_file_name(): desc = 'A potentially interesting file was found at: "%s".' desc = desc % response.get_url() i = Info('Potentially interesting file', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'files', i) om.out.information(i.get_desc()) # Report only once self._seen.add(response.get_url())
def _html_in_comment(self, comment, request, response): ''' Find HTML code in HTML comments ''' html_in_comment = self.HTML_RE.search(comment) if html_in_comment and \ (comment, response.get_url()) not in self._already_reported_interesting: # There is HTML code in the comment. comment = comment.replace('\n', '') comment = comment.replace('\r', '') desc = 'A comment with the string "%s" was found in: "%s".'\ ' This could be interesting.' desc = desc % (comment, response.get_url()) i = Info('HTML comment contains HTML code', desc, response.id, self.get_name()) i.set_dc(request.get_dc()) i.set_uri(response.get_uri()) i.add_to_highlight(html_in_comment.group(0)) kb.kb.append(self, 'html_comment_hides_html', i) om.out.information(i.get_desc()) self._already_reported_interesting.add( (comment, response.get_url()))
def discover(self, fuzzable_request): ''' :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' root_domain = fuzzable_request.get_url().get_root_domain() pks_se = pks(self._uri_opener) results = pks_se.search(root_domain) pks_url = 'http://pgp.mit.edu:11371/' for result in results: mail = result.username + '@' + root_domain desc = 'The mail account: "%s" was found at: "%s".' desc = desc % (mail, pks_url) i = Info('Email account', desc, result.id, self.get_name()) i.set_url(URL(pks_url)) i['mail'] = mail i['user'] = result.username i['name'] = result.name i['url_list'] = [ URL(pks_url), ] kb.kb.append('emails', 'emails', i) # Don't save duplicated information in the KB. It's useless. #kb.kb.append( self, 'emails', i ) om.out.information(i.get_desc())
def _analyze_results(self, filtered, not_filtered): ''' Analyze the test results and save the conclusion to the kb. ''' if len(filtered) >= len(self._get_offending_strings()) / 5.0: desc = 'The remote network has an active filter. IMPORTANT: The'\ ' result of all the other plugins will be unaccurate, web'\ ' applications could be vulnerable but "protected" by the'\ ' active filter.' i = Info('Active filter detected', desc, 1, self.get_name()) i['filtered'] = filtered kb.kb.append(self, 'afd', i) om.out.information(i.get_desc()) om.out.information('The following URLs were filtered:') for i in filtered: om.out.information('- ' + i) if not_filtered: om.out.information( 'The following URLs passed undetected by the filter:') for i in not_filtered: om.out.information('- ' + i)
def discover(self, fuzzable_request): ''' :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' root_domain = fuzzable_request.get_url().get_root_domain() pks_se = pks(self._uri_opener) results = pks_se.search(root_domain) pks_url = 'http://pgp.mit.edu:11371/' for result in results: mail = result.username + '@' + root_domain desc = 'The mail account: "%s" was found at: "%s".' desc = desc % (mail, pks_url) i = Info('Email account', desc, result.id, self.get_name()) i.set_url(URL(pks_url)) i['mail'] = mail i['user'] = result.username i['name'] = result.name i['url_list'] = set([URL(pks_url), ]) kb.kb.append('emails', 'emails', i) # Don't save duplicated information in the KB. It's useless. #kb.kb.append( self, 'emails', i ) om.out.information(i.get_desc())
def discover(self, fuzzable_request): ''' Identify server software using favicon. :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' domain_path = fuzzable_request.get_url().get_domain_path() # TODO: Maybe I should also parse the html to extract the favicon location? favicon_url = domain_path.url_join('favicon.ico') response = self._uri_opener.GET(favicon_url, cache=True) remote_fav_md5 = hashlib.md5(response.get_body()).hexdigest() if not is_404(response): # check if MD5 is matched in database/list for md5part, favicon_desc in self._read_favicon_db(): if md5part == remote_fav_md5: desc = 'Favicon.ico file was identified as "%s".' % favicon_desc i = Info('Favicon identification', desc, response.id, self.get_name()) i.set_url(favicon_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc()) break else: # # Report to the kb that we failed to ID this favicon.ico # and that the md5 should be sent to the developers. # desc = 'Favicon identification failed. If the remote site is' \ ' using framework that is being exposed by its favicon,'\ ' please send an email to [email protected]'\ ' including this md5 hash "%s" and the' \ ' name of the server or Web application it represents.' \ ' New fingerprints make this plugin more powerful and ' \ ' accurate.' desc = desc % remote_fav_md5 i = Info('Favicon identification failed', desc, response.id, self.get_name()) i.set_url(favicon_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc())
def _extract_version_from_egg(self, query_results): """ Analyzes the eggs and tries to deduce a PHP version number ( which is then saved to the kb ). """ if not query_results: return None else: cmp_list = [] for query_result in query_results: body = query_result.http_response.get_body() if isinstance(body, unicode): body = body.encode("utf-8") hash_str = hashlib.md5(body).hexdigest() cmp_list.append((hash_str, query_result.egg_desc)) cmp_set = set(cmp_list) found = False matching_versions = [] for version in self.EGG_DB: version_hashes = set(self.EGG_DB[version]) if len(cmp_set) == len(cmp_set.intersection(version_hashes)): matching_versions.append(version) found = True if matching_versions: desc = "The PHP framework version running on the remote" " server was identified as:\n- %s" versions = "\n- ".join(matching_versions) desc = desc % versions response_ids = [r.http_response.get_id() for r in query_results] i = Info("Fingerprinted PHP version", desc, response_ids, self.get_name()) i["version"] = matching_versions kb.kb.append(self, "version", i) om.out.information(i.get_desc()) if not found: version = "unknown" powered_by_headers = kb.kb.raw_read("server_header", "powered_by_string") try: for v in powered_by_headers: if "php" in v.lower(): version = v.split("/")[1] except: pass msg = ( "The PHP version could not be identified using PHP eggs," ", please send this signature and the PHP version to the" " w3af project develop mailing list. Signature:" " EGG_DB['%s'] = %s\n" ) msg = msg % (version, str(list(cmp_set))) om.out.information(msg)
def _extract_version_from_egg(self, query_results): ''' Analyzes the eggs and tries to deduce a PHP version number ( which is then saved to the kb ). ''' if not query_results: return None else: cmp_list = [] for query_result in query_results: body = query_result.http_response.get_body() if isinstance(body, unicode): body = body.encode('utf-8') hash_str = hashlib.md5(body).hexdigest() cmp_list.append((hash_str, query_result.egg_desc)) cmp_set = set(cmp_list) found = False matching_versions = [] for version in self.EGG_DB: version_hashes = set(self.EGG_DB[version]) if len(cmp_set) == len(cmp_set.intersection(version_hashes)): matching_versions.append(version) found = True if matching_versions: desc = 'The PHP framework version running on the remote'\ ' server was identified as:\n- %s' versions = '\n- '.join(matching_versions) desc = desc % versions response_ids = [r.http_response.get_id() for r in query_results] i = Info('Fingerprinted PHP version', desc, response_ids, self.get_name()) i['version'] = matching_versions kb.kb.append(self, 'version', i) om.out.information(i.get_desc()) if not found: version = 'unknown' powered_by_headers = kb.kb.raw_read('server_header', 'powered_by_string') try: for v in powered_by_headers: if 'php' in v.lower(): version = v.split('/')[1] except: pass msg = 'The PHP version could not be identified using PHP eggs,'\ ', please send this signature and the PHP version to the'\ ' w3af project develop mailing list. Signature:'\ ' EGG_DB[\'%s\'] = %s\n' msg = msg % (version, str(list(cmp_set))) om.out.information(msg)
def _check_server_header(self, fuzzable_request): ''' HTTP GET and analyze response for server header ''' response = self._uri_opener.GET(fuzzable_request.get_url(), cache=True) for hname, hvalue in response.get_lower_case_headers().iteritems(): if hname == 'server': server = hvalue desc = 'The server header for the remote web server is: "%s".' desc = desc % server i = Info('Server header', desc, response.id, self.get_name()) i['server'] = server i.add_to_highlight(hname + ':') om.out.information(i.get_desc()) # Save the results in the KB so the user can look at it kb.kb.append(self, 'server', i) # Also save this for easy internal use # other plugins can use this information kb.kb.raw_write(self, 'server_string', server) break else: # strange ! desc = 'The remote HTTP Server omitted the "server" header in'\ ' its response.' i = Info('Omitted server header', desc, response.id, self.get_name()) om.out.information(i.get_desc()) # Save the results in the KB so that other plugins can use this # information kb.kb.append(self, 'ommited_server_header', i) # Also save this for easy internal use # other plugins can use this information kb.kb.raw_write(self, 'server_string', '')
def _report_no_realm(self, response): # Report this strange case desc = 'The resource: "%s" requires authentication (HTTP Code'\ ' 401) but the www-authenticate header is not present.'\ ' This requires human verification.' desc = desc % response.get_url() i = Info('Authentication without www-authenticate header', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'non_rfc_auth', i) om.out.information(i.get_desc())
def _report_finding(self, response): ''' Save the finding to the kb. :param response: The response that triggered the detection ''' desc = 'The remote web server seems to have a reverse proxy installed.' i = Info('Reverse proxy identified', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'detect_reverse_proxy', i) om.out.information(i.get_desc())
def _kb_info_user(self, url, response_id, username): ''' Put user in Kb :return: None, everything is saved in kb ''' desc = 'WordPress user "%s" found during username enumeration.' desc = desc % username i = Info('Identified WordPress user', desc, response_id, self.get_name()) i.set_url(url) kb.kb.append(self, 'users', i) om.out.information(i.get_desc())
def _analyze_gears_manifest(self, url, response, file_name): if '"entries":' in response: # Save it to the kb! desc = 'A gears manifest file was found at: "%s".'\ ' Each file should be manually reviewed for sensitive'\ ' information that may get cached on the client.' desc = desc % url i = Info('Gears manifest resource', desc, response.id, self.get_name()) i.set_url(url) kb.kb.append(self, url, i) om.out.information(i.get_desc())
def _parse_zone_h_result(self, response): ''' Parse the result from the zone_h site and create the corresponding info objects. :return: None ''' # # I'm going to do only one big "if": # # - The target site was hacked more than one time # - The target site was hacked only one time # # This is the string I have to parse: # in the zone_h response, they are two like this, the first has to be ignored! regex = 'Total notifications: <b>(\d*)</b> of which <b>(\d*)</b> single ip and <b>(\d*)</b> mass' regex_result = re.findall(regex, response.get_body()) try: total_attacks = int(regex_result[0][0]) except IndexError: om.out.debug( 'An error was generated during the parsing of the zone_h website.' ) else: # Do the if... if total_attacks > 1: desc = 'The target site was defaced more than one time in the'\ ' past. For more information please visit the following'\ ' URL: "%s".' % response.get_url() v = Vuln('Previous defacements', desc, severity.MEDIUM, response.id, self.get_name()) v.set_url(response.get_url()) kb.kb.append(self, 'defacements', v) om.out.information(v.get_desc()) elif total_attacks == 1: desc = 'The target site was defaced in the past. For more'\ ' information please visit the following URL: "%s".' desc = desc % response.get_url() i = Info('Previous defacements', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'defacements', i) om.out.information(i.get_desc())
def crawl(self, fuzzable_request): ''' Get the robots.txt file and parse it. :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' dirs = [] base_url = fuzzable_request.get_url().base_url() robots_url = base_url.url_join('robots.txt') http_response = self._uri_opener.GET(robots_url, cache=True) if not is_404(http_response): # Save it to the kb! desc = 'A robots.txt file was found at: "%s", this file might'\ ' expose private URLs and requires a manual review. The'\ ' scanner will add all URLs listed in this files to the'\ ' analysis queue.' desc = desc % robots_url i = Info('robots.txt file', desc, http_response.id, self.get_name()) i.set_url(robots_url) kb.kb.append(self, 'robots.txt', i) om.out.information(i.get_desc()) # Work with it... dirs.append(robots_url) for line in http_response.get_body().split('\n'): line = line.strip() if len(line) > 0 and line[0] != '#' and \ (line.upper().find('ALLOW') == 0 or line.upper().find('DISALLOW') == 0): url = line[line.find(':') + 1:] url = url.strip() try: url = base_url.url_join(url) except: # Simply ignore the invalid URL pass else: dirs.append(url) self.worker_pool.map(self.http_get_and_parse, dirs)
def _fingerprint_installer(self, domain_path, wp_unique_url, response): ''' GET latest.zip and latest.tar.gz and compare with the hashes from the release.db that was previously generated from wordpress.org [0] and contains all release hashes. This gives the initial wordpress version, not the current one. [0] http://wordpress.org/download/release-archive/ ''' zip_url = domain_path.url_join('latest.zip') tar_gz_url = domain_path.url_join('latest.tar.gz') install_urls = [zip_url, tar_gz_url] for install_url in install_urls: response = self._uri_opener.GET(install_url, cache=True, respect_size_limit=False) # md5sum the response body m = hashlib.md5() m.update(response.get_body()) remote_release_hash = m.hexdigest() release_db = self._release_db for line in file(release_db): try: line = line.strip() release_db_hash, release_db_name = line.split(',') except: continue if release_db_hash == remote_release_hash: desc = 'The sysadmin used WordPress version "%s" during the'\ ' installation, which was found by matching the contents'\ ' of "%s" with the hashes of known releases. If the'\ ' sysadmin did not update wordpress, the current version'\ ' will still be the same.' desc = desc % (release_db_name, install_url) i = Info('Fingerprinted Wordpress version', desc, response.id, self.get_name()) i.set_url(install_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc())
def discover(self, fuzzable_request): ''' :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' if self._is_proxyed_conn(fuzzable_request): desc = 'Your ISP seems to have a transparent proxy installed,'\ ' this can influence scan results in unexpected ways.' i = Info('Transparent proxy detected', desc, 1, self.get_name()) i.set_url(fuzzable_request.get_url()) kb.kb.append(self, 'detect_transparent_proxy', i) om.out.information(i.get_desc()) else: om.out.information('Your ISP has no transparent proxy.')
def _parse_zone_h_result(self, response): ''' Parse the result from the zone_h site and create the corresponding info objects. :return: None ''' # # I'm going to do only one big "if": # # - The target site was hacked more than one time # - The target site was hacked only one time # # This is the string I have to parse: # in the zone_h response, they are two like this, the first has to be ignored! regex = 'Total notifications: <b>(\d*)</b> of which <b>(\d*)</b> single ip and <b>(\d*)</b> mass' regex_result = re.findall(regex, response.get_body()) try: total_attacks = int(regex_result[0][0]) except IndexError: om.out.debug('An error was generated during the parsing of the zone_h website.') else: # Do the if... if total_attacks > 1: desc = 'The target site was defaced more than one time in the'\ ' past. For more information please visit the following'\ ' URL: "%s".' % response.get_url() v = Vuln('Previous defacements', desc, severity.MEDIUM, response.id, self.get_name()) v.set_url(response.get_url()) kb.kb.append(self, 'defacements', v) om.out.information(v.get_desc()) elif total_attacks == 1: desc = 'The target site was defaced in the past. For more'\ ' information please visit the following URL: "%s".' desc = desc % response.get_url() i = Info('Previous defacements', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'defacements', i) om.out.information(i.get_desc())
def _check_x_power(self, fuzzable_request): ''' Analyze X-Powered-By header. ''' response = self._uri_opener.GET(fuzzable_request.get_url(), cache=True) for header_name in response.get_headers().keys(): for i in ['ASPNET', 'POWERED']: if i in header_name.upper() or header_name.upper() in i: powered_by = response.get_headers()[header_name] # Only get the first one self._x_powered = False # # Check if I already have this info in the KB # pow_by_kb = kb.kb.get('server_header', 'powered_by') powered_by_in_kb = [j['powered_by'] for j in pow_by_kb] if powered_by not in powered_by_in_kb: # # I don't have it in the KB, so I need to add it, # desc = 'The %s header for the target HTTP server is "%s".' desc = desc % (header_name, powered_by) i = Info('Powered-by header', desc, response.id, self.get_name()) i['powered_by'] = powered_by i.add_to_highlight(header_name + ':') om.out.information(i.get_desc()) # Save the results in the KB so that other plugins can # use this information. Before knowing that some servers # may return more than one poweredby header I had: # kb.kb.raw_write( self , 'powered_by' , powered_by ) # But I have seen an IIS server with PHP that returns # both the ASP.NET and the PHP headers kb.kb.append(self, 'powered_by', i) # Update the list and save it, powered_by_in_kb.append(powered_by) kb.kb.raw_write(self, 'powered_by_string', powered_by_in_kb)
def _analyze_response(self, response): ''' It seems that we have found a _vti_inf file, parse it and analyze the content! :param response: The http response object for the _vti_inf file. :return: None. All the info is saved to the kb. ''' version_mo = self.VERSION_RE.search(response.get_body()) admin_mo = self.ADMIN_URL_RE.search(response.get_body()) author_mo = self.AUTHOR_URL_RE.search(response.get_body()) if version_mo and admin_mo and author_mo: #Set the self._exec to false self._exec = False desc = 'The FrontPage Configuration Information file was found'\ ' at: "%s" and the version of FrontPage Server Extensions'\ ' is: "%s".' desc = desc % (response.get_url(), version_mo.group(1)) i = Info('FrontPage configuration information', desc, response.id, self.get_name()) i.set_url(response.get_url()) i['version'] = version_mo.group(1) kb.kb.append(self, 'frontpage_version', i) om.out.information(i.get_desc()) # # Handle the admin.exe file # self._analyze_admin(response, admin_mo) # # Handle the author.exe file # self._analyze_author(response, author_mo) else: # This is strange... we found a _vti_inf file, but there is no frontpage # information in it... IPS? WAF? honeypot? msg = '[IMPROVEMENT] Invalid frontPage configuration information'\ ' found at %s (id: %s).' msg = msg % (response.get_url(), response.id) om.out.debug(msg)
def _extract_server_version(self, fuzzable_request, response): ''' Get the server version from the HTML: <dl><dt>Server Version: Apache/2.2.9 (Unix)</dt> ''' for version in re.findall('<dl><dt>Server Version: (.*?)</dt>', response.get_body()): # Save the results in the KB so the user can look at it desc = 'The web server has the apache server status module'\ ' enabled which discloses the following remote server'\ ' version: "%s".' desc = desc % version i = Info('Apache Server version', desc, response.id, self.get_name()) i.set_url(response.get_url()) om.out.information(i.get_desc()) kb.kb.append(self, 'server', i)
def _find_OS(self, fuzzable_request): ''' Analyze responses and determine if remote web server runs on windows or *nix. @Return: None, the knowledge is saved in the knowledgeBase ''' freq_url = fuzzable_request.get_url() filename = freq_url.get_file_name() dirs = freq_url.get_directories()[:-1] # Skipping "domain level" dir. if dirs and filename: last_url = dirs[-1] last_url = last_url.url_string windows_url = URL(last_url[0:-1] + '\\' + filename) windows_response = self._uri_opener.GET(windows_url) original_response = self._uri_opener.GET(freq_url) if relative_distance_ge(original_response.get_body(), windows_response.get_body(), 0.98): desc = 'Fingerprinted this host as a Microsoft Windows system.' os_str = 'windows' else: desc = 'Fingerprinted this host as a *nix system. Detection for'\ ' this operating system is weak, "if not windows then'\ ' linux".' os_str = 'unix' response_ids = [windows_response.id, original_response.id] i = Info('Operating system', desc, response_ids, self.get_name()) i.set_url(windows_response.get_url()) kb.kb.raw_write(self, 'operating_system_str', os_str) kb.kb.append(self, 'operating_system', i) om.out.information(i.get_desc()) return True return False
def crawl(self, fuzzable_request): """ Find CAPTCHA images. :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. """ result, captchas = self._identify_captchas(fuzzable_request) if result: for captcha in captchas: desc = 'Found a CAPTCHA image at: "%s".' % captcha.img_src response_ids = [response.id for response in captcha.http_responses] i = Info("Captcha image detected", desc, response_ids, self.get_name()) i.set_uri(captcha.img_src) kb.kb.append(self, "CAPTCHA", i) om.out.information(i.get_desc())
def crawl(self, fuzzable_request): ''' GET some files and parse them. :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' base_url = fuzzable_request.get_url().base_url() for url, re_obj in self.ORACLE_DATA: oracle_discovery_URL = base_url.url_join(url) response = self._uri_opener.GET(oracle_discovery_URL, cache=True) if not is_404(response): # Extract the links and send to core for fr in self._create_fuzzable_requests(response): self.output_queue.put(fr) # pylint: disable=E1101 # E1101: Instance of 'str' has no 'search' member mo = re_obj.search(response.get_body(), re.DOTALL) if mo: desc = '"%s" version "%s" was detected at "%s".' desc = desc % (mo.group(1).title(), mo.group(2).title(), response.get_url()) i = Info('Oracle Application Server', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'oracle_discovery', i) om.out.information(i.get_desc()) else: msg = 'oracle_discovery found the URL: "%s" but failed to'\ ' parse it as an Oracle page. The first 50 bytes of'\ ' the response body is: "%s".' body_start = response.get_body()[:50] om.out.debug(msg % (response.get_url(), body_start))
def _are_php_eggs(self, query_results): """ Now I analyze if this is really a PHP eggs thing, or simply a response that changes a lot on each request. Before, I had something like this: if relative_distance(original_response.get_body(), response.get_body()) < 0.1: But I got some reports about false positives with this approach, so now I'm changing it to something a little bit more specific. """ images = 0 not_images = 0 for query_result in query_results: if "image" in query_result.http_response.content_type: images += 1 else: not_images += 1 if images == 3 and not_images == 1: # # The remote web server has expose_php = On. Report all the findings. # for query_result in query_results: desc = ( "The PHP framework running on the remote server has a" ' "%s" easter egg, access to the PHP egg is possible' ' through the URL: "%s".' ) desc = desc % (query_result.egg_desc, query_result.egg_URL) i = Info("PHP Egg", desc, query_result.http_response.id, self.get_name()) i.set_url(query_result.egg_URL) kb.kb.append(self, "eggs", i) om.out.information(i.get_desc()) return True return False
def _interesting_word(self, comment, request, response): ''' Find interesting words in HTML comments ''' comment = comment.lower() for word in self._multi_in.query(response.body): if (word, response.get_url()) not in self._already_reported_interesting: desc = 'A comment with the string "%s" was found in: "%s".'\ ' This could be interesting.' desc = desc % (word, response.get_url()) i = Info('Interesting HTML comment', desc, response.id, self.get_name()) i.set_dc(request.get_dc()) i.set_uri(response.get_uri()) i.add_to_highlight(word) kb.kb.append(self, 'interesting_comments', i) om.out.information(i.get_desc()) self._already_reported_interesting.add((word, response.get_url()))
def _are_php_eggs(self, query_results): ''' Now I analyze if this is really a PHP eggs thing, or simply a response that changes a lot on each request. Before, I had something like this: if relative_distance(original_response.get_body(), response.get_body()) < 0.1: But I got some reports about false positives with this approach, so now I'm changing it to something a little bit more specific. ''' images = 0 not_images = 0 for query_result in query_results: if 'image' in query_result.http_response.content_type: images += 1 else: not_images += 1 if images == 3 and not_images == 1: # # The remote web server has expose_php = On. Report all the findings. # for query_result in query_results: desc = 'The PHP framework running on the remote server has a'\ ' "%s" easter egg, access to the PHP egg is possible'\ ' through the URL: "%s".' desc = desc % (query_result.egg_desc, query_result.egg_URL) i = Info('PHP Egg', desc, query_result.http_response.id, self.get_name()) i.set_url(query_result.egg_URL) kb.kb.append(self, 'eggs', i) om.out.information(i.get_desc()) return True return False
def _report_finding(self, name, response, protected_by=None): ''' Creates a information object based on the name and the response parameter and saves the data in the kb. :param name: The name of the WAF :param response: The HTTP response object that was used to identify the WAF :param protected_by: A more detailed description/version of the WAF ''' desc = 'The remote network seems to have a "%s" WAF deployed to' \ ' protect access to the web server.' desc = desc % name if protected_by: desc += ' The following is the WAF\'s version: "%s".' i = Info('Web Application Firewall fingerprint', desc, response.id, self.get_name()) i.set_url(response.get_url()) i.set_id(response.id) kb.kb.append(self, name, i) om.out.information(i.get_desc())
def _interesting_word(self, comment, request, response): ''' Find interesting words in HTML comments ''' comment = comment.lower() for word in self._multi_in.query(response.body): if (word, response.get_url() ) not in self._already_reported_interesting: desc = 'A comment with the string "%s" was found in: "%s".'\ ' This could be interesting.' desc = desc % (word, response.get_url()) i = Info('Interesting HTML comment', desc, response.id, self.get_name()) i.set_dc(request.get_dc()) i.set_uri(response.get_uri()) i.add_to_highlight(word) kb.kb.append(self, 'interesting_comments', i) om.out.information(i.get_desc()) self._already_reported_interesting.add( (word, response.get_url()))
def crawl(self, fuzzable_request): ''' Find CAPTCHA images. :param fuzzable_request: A fuzzable_request instance that contains (among other things) the URL to test. ''' result, captchas = self._identify_captchas(fuzzable_request) if result: for captcha in captchas: desc = 'Found a CAPTCHA image at: "%s".' % captcha.img_src response_ids = [ response.id for response in captcha.http_responses ] i = Info('Captcha image detected', desc, response_ids, self.get_name()) i.set_uri(captcha.img_src) kb.kb.append(self, 'CAPTCHA', i) om.out.information(i.get_desc())
def _fingerprint_readme(self, domain_path, wp_unique_url, response): ''' GET the readme.html file and extract the version information from there. ''' wp_readme_url = domain_path.url_join('readme.html') response = self._uri_opener.GET(wp_readme_url, cache=True) # Find the string in the response html find = '<br /> Version (\d\.\d\.?\d?)' m = re.search(find, response.get_body()) # If string found, group version if m: version = m.group(1) desc = 'WordPress version "%s" found in the readme.html file.' desc = desc % version i = Info('Fingerprinted Wordpress version', desc, response.id, self.get_name()) i.set_url(wp_readme_url) kb.kb.append(self, 'info', i) om.out.information(i.get_desc())
def _verify_content_neg_enabled(self, fuzzable_request): ''' Checks if the remote website is vulnerable or not. Saves the result in self._content_negotiation_enabled , because we want to perform this test only once. :return: True if vulnerable. ''' if self._content_negotiation_enabled is not None: # The test was already performed, we return the old response return self._content_negotiation_enabled else: # We perform the test, for this we need a URL that has a filename, URL's # that don't have a filename can't be used for this. filename = fuzzable_request.get_url().get_file_name() if filename == '': return None filename = filename.split('.')[0] # Now I simply perform the request: alternate_resource = fuzzable_request.get_url().url_join(filename) headers = fuzzable_request.get_headers() headers['Accept'] = 'w3af/bar' response = self._uri_opener.GET(alternate_resource, headers=headers) if 'alternates' in response.get_lower_case_headers(): # Even if there is only one file, with an unique mime type, # the content negotiation will return an alternates header. # So this is pretty safe. # Save the result internally self._content_negotiation_enabled = True # Save the result as an info in the KB, for the user to see it: desc = 'HTTP Content negotiation is enabled in the remote web'\ ' server. This could be used to bruteforce file names'\ ' and find new resources.' i = Info('HTTP Content Negotiation enabled', desc, response.id, self.get_name()) i.set_url(response.get_url()) kb.kb.append(self, 'content_negotiation', i) om.out.information(i.get_desc()) else: om.out.information( 'The remote Web server has Content Negotiation disabled.') # I want to perform this test a couple of times... so I only return False # if that "couple of times" is empty self._tries_left -= 1 if self._tries_left == 0: # Save the FALSE result internally self._content_negotiation_enabled = False else: # None tells the plugin to keep trying with the next URL return None return self._content_negotiation_enabled
def _extract_version_from_egg(self, query_results): ''' Analyzes the eggs and tries to deduce a PHP version number ( which is then saved to the kb ). ''' if not query_results: return None else: cmp_list = [] for query_result in query_results: body = query_result.http_response.get_body() if isinstance(body, unicode): body = body.encode('utf-8') hash_str = hashlib.md5(body).hexdigest() cmp_list.append((hash_str, query_result.egg_desc)) cmp_set = set(cmp_list) found = False matching_versions = [] for version in self.EGG_DB: version_hashes = set(self.EGG_DB[version]) if len(cmp_set) == len(cmp_set.intersection(version_hashes)): matching_versions.append(version) found = True if matching_versions: desc = 'The PHP framework version running on the remote'\ ' server was identified as:\n- %s' versions = '\n- '.join(matching_versions) desc = desc % versions response_ids = [ r.http_response.get_id() for r in query_results ] i = Info('Fingerprinted PHP version', desc, response_ids, self.get_name()) i['version'] = matching_versions kb.kb.append(self, 'version', i) om.out.information(i.get_desc()) if not found: version = 'unknown' powered_by_headers = kb.kb.raw_read('server_header', 'powered_by_string') try: for v in powered_by_headers: if 'php' in v.lower(): version = v.split('/')[1] except: pass msg = 'The PHP version could not be identified using PHP eggs,'\ ', please send this signature and the PHP version to the'\ ' w3af project develop mailing list. Signature:'\ ' EGG_DB[\'%s\'] = %s\n' msg = msg % (version, str(list(cmp_set))) om.out.information(msg)