def _check_methods(self, url): ''' Perform some requests in order to check if we are able to retrieve some data with methods that may be wrongly enabled. ''' allowed_methods = [] for method in ['GET', 'POST', 'ABCD', 'HEAD']: method_functor = getattr(self._uri_opener, method) try: response = apply(method_functor, (url,), {}) code = response.get_code() except: pass else: if code not in self.BAD_METHODS: allowed_methods.append((method, response.id)) if len(allowed_methods) > 0: response_ids = [i for m, i in allowed_methods] methods = ', '.join([m for m, i in allowed_methods]) + '.' desc = 'The resource: "%s" requires authentication but the access'\ ' is misconfigured and can be bypassed using these'\ ' methods: %s.' desc = desc % (url, methods) v = Vuln('Misconfigured access control', desc, severity.MEDIUM, response_ids, self.get_name()) v.set_url(url) v['methods'] = allowed_methods self.kb_append(self, 'auth', v)
def _send_and_check(self, repo_url, repo_get_files, repo, domain_path): ''' Check if a repository index exists in the domain_path. :return: None, everything is saved to the self.out_queue. ''' http_response = self.http_get_and_parse(repo_url) if not is_404(http_response): filenames = repo_get_files(http_response.get_body()) parsed_url_set = set() for filename in self._clean_filenames(filenames): test_url = domain_path.url_join(filename) if test_url not in self._analyzed_filenames: parsed_url_set.add(test_url) self._analyzed_filenames.add(filename) self.worker_pool.map(self.http_get_and_parse, parsed_url_set) if parsed_url_set: desc = 'A %s was found at: "%s"; this could indicate that'\ ' a %s is accessible. You might be able to download'\ ' the Web application source code.' desc = desc % (repo, http_response.get_url(), repo) v = Vuln('Source code repository', desc, severity.MEDIUM, http_response.id, self.get_name()) v.set_url(http_response.get_url()) kb.kb.append(self, repo, v) om.out.vulnerability(v.get_desc(), severity=v.get_severity())
def _analyze_html(self, request, response): ''' Search for IP addresses in the HTML ''' if not response.is_text_or_html(): return # Performance improvement! if not (('10.' in response) or ('172.' in response) or ('192.168.' in response) or ('169.254.' in response)): return for regex in self._regex_list: for match in regex.findall(response.get_body()): match = match.strip() # Some proxy servers will return errors that include headers in the body # along with the client IP which we want to ignore if re.search("^.*X-Forwarded-For: .*%s" % match, response.get_body(), re.M): continue # If i'm requesting 192.168.2.111 then I don't want to be alerted about it if match not in self._ignore_if_match and \ not request.sent(match): desc = 'The URL: "%s" returned an HTML document'\ ' with a private IP address: "%s".' desc = desc % (response.get_url(), match) v = Vuln('Private IP disclosure vulnerability', desc, severity.LOW, response.id, self.get_name()) v.set_url(response.get_url()) v['IP'] = match v.add_to_highlight(match) self.kb_append(self, 'HTML', v)
def _http_only(self, request, response, cookie_obj, cookie_header_value, fingerprinted): ''' Verify if the cookie has the httpOnly parameter set Reference: http://www.owasp.org/index.php/HTTPOnly http://en.wikipedia.org/wiki/HTTP_cookie :param request: The http request object :param response: The http response object :param cookie_obj: The cookie object to analyze :param cookie_header_value: The cookie, as sent in the HTTP response :param fingerprinted: True if the cookie was fingerprinted :return: None ''' if not self.HTTPONLY_RE.search(cookie_header_value): vuln_severity = severity.MEDIUM if fingerprinted else severity.LOW desc = 'A cookie without the HttpOnly flag was sent when requesting' \ ' "%s". The HttpOnly flag prevents potential intruders from' \ ' accessing the cookie value through Cross-Site Scripting' \ ' attacks.' desc = desc % response.get_url() v = Vuln('Cookie without HttpOnly', desc, vuln_severity, response.id, self.get_name()) v.set_url(response.get_url()) self._set_cookie_to_rep(v, cobj=cookie_obj) kb.kb.append(self, 'security', v)
def crawl(self, fuzzable_request): ''' Plugin entry point, perform all the work. ''' to_check = self._get_to_check(fuzzable_request.get_url()) # I found some URLs, create fuzzable requests phishtank_matches = self._is_in_phishtank(to_check) for ptm in phishtank_matches: response = self._uri_opener.GET(ptm.url) for fr in self._create_fuzzable_requests(response): self.output_queue.put(fr) # Only create the vuln object once if phishtank_matches: desc = 'The URL: "%s" seems to be involved in a phishing scam.' \ ' Please see %s for more info.' desc = desc % (ptm.url, ptm.more_info_URL) v = Vuln('Phishing scam', desc, severity.MEDIUM, response.id, self.get_name()) v.set_url(ptm.url) kb.kb.append(self, 'phishtank', v) om.out.vulnerability(v.get_desc(), severity=v.get_severity())
def end(self): ''' This method is called when the plugin wont be used anymore. The real job of this plugin is done here, where I will try to see if one of the error_500 responses were not identified as a vuln by some of my audit plugins ''' all_vulns = kb.kb.get_all_vulns() all_vulns_tuples = [(v.get_uri(), v.get_dc()) for v in all_vulns] for request, error_500_response_id in self._error_500_responses: if (request.get_uri(), request.get_dc()) not in all_vulns_tuples: # Found a err 500 that wasnt identified !!! desc = 'An unidentified web application error (HTTP response'\ ' code 500) was found at: "%s". Enable all plugins and'\ ' try again, if the vulnerability still is not identified'\ ', please verify manually and report it to the w3af'\ ' developers.' desc = desc % request.get_url() v = Vuln('Unhandled error in web application', desc, severity.MEDIUM, error_500_response_id, self.get_name()) v.set_uri(request.get_uri()) self.kb_append_uniq(self, 'error_500', v, 'VAR') self._error_500_responses.cleanup()
def write_vuln_to_kb(vulnty, url, funcs): vulndata = php_sca.KB_DATA[vulnty] for f in funcs: vuln_sev = vulndata['severity'] desc = name = vulndata['name'] v = Vuln(name, desc, vuln_sev, 1, 'PHP Static Code Analyzer') v.set_uri(url) v.set_var(f.vulnsources[0]) args = list(vulndata['kb_key']) + [v] # TODO: Extract the method from the PHP code # $_GET == GET # $_POST == POST # $_REQUEST == GET v.set_method('GET') # TODO: Extract all the other variables that are # present in the PHP file using the SCA v.set_dc(DataContainer()) # # TODO: This needs to be checked! OS Commanding specific # attributes. v['os'] = 'unix' v['separator'] = '' kb.kb.append(*args)
def grep(self, request, response): ''' Plugin entry point. :param request: The HTTP request object. :param response: The HTTP response object :return: None, all results are saved in the kb. ''' uri = response.get_uri() if response.is_text_or_html() and uri not in self._already_inspected: # Don't repeat URLs self._already_inspected.add(uri) for regex in self._regex_list: for m in regex.findall(response.get_body()): user = m[0] desc = 'The URL: "%s" contains a SVN versioning signature'\ ' with the username "%s".' desc = desc % (uri, user) v = Vuln('SVN user disclosure vulnerability', desc, severity.LOW, response.id, self.get_name()) v.set_uri(uri) v['user'] = user v.add_to_highlight(user) self.kb_append_uniq(self, 'users', v, 'URL')
def _analyze_headers(self, request, response): ''' Search for IP addresses in HTTP headers ''' # Get the headers string headers_string = response.dump_headers() # Match the regular expressions for regex in self._regex_list: for match in regex.findall(headers_string): # If i'm requesting 192.168.2.111 then I don't want to be # alerted about it if match not in self._ignore_if_match: desc = 'The URL: "%s" returned an HTTP header with a'\ ' private IP address: "%s".' desc = desc % (response.get_url(), match) v = Vuln('Private IP disclosure vulnerability', desc, severity.LOW, response.id, self.get_name()) v.set_url(response.get_url()) v['IP'] = match v.add_to_highlight(match) self.kb_append(self, 'header', v)
def _ssl_cookie_via_http(self, request, response): ''' Analyze if a cookie value, sent in a HTTPS request, is now used for identifying the user in an insecure page. Example: Login is done over SSL The rest of the page is HTTP ''' if request.get_url().get_protocol().lower() == 'https': return for cookie in kb.kb.get('analyze_cookies', 'cookies'): if cookie.get_url().get_protocol().lower() == 'https' and \ request.get_url().get_domain() == cookie.get_url().get_domain(): # The cookie was sent using SSL, I'll check if the current # request, is using these values in the POSTDATA / QS / COOKIE for key in cookie['cookie-object'].keys(): value = cookie['cookie-object'][key].value # This if is to create less false positives if len(value) > 6 and value in request.dump(): desc = 'Cookie values that were set over HTTPS, are' \ ' then sent over an insecure channel in a' \ ' request to "%s".' desc = desc % request.get_url() v = Vuln('Secure cookies over insecure channel', desc, severity.HIGH, response.id, self.get_name()) v.set_url(response.get_url()) self._set_cookie_to_rep(v, cobj=cookie['cookie-object']) kb.kb.append(self, 'security', v)
def _check_if_exists(self, web_shell_url): ''' Check if the file exists. :param web_shell_url: The URL to check ''' try: response = self._uri_opener.GET(web_shell_url, cache=True) except w3afException: om.out.debug('Failed to GET webshell:' + web_shell_url) else: if self._is_possible_backdoor(response): desc = 'A web backdoor was found at: "%s"; this could ' \ 'indicate that the server has been compromised.' desc = desc % response.get_url() v = Vuln('Potential web backdoor', desc, severity.HIGH, response.id, self.get_name()) v.set_url(response.get_url()) kb.kb.append(self, 'backdoors', v) om.out.vulnerability(v.get_desc(), severity=v.get_severity()) for fr in self._create_fuzzable_requests(response): self.output_queue.put(fr)
def _SEARCH(self, domain_path): ''' Test SEARCH method. ''' content = "<?xml version='1.0'?>\r\n" content += "<g:searchrequest xmlns:g='DAV:'>\r\n" content += "<g:sql>\r\n" content += "Select 'DAV:displayname' from scope()\r\n" content += "</g:sql>\r\n" content += "</g:searchrequest>\r\n" res = self._uri_opener.SEARCH(domain_path, data=content) content_matches = '<a:response>' in res or '<a:status>' in res or \ 'xmlns:a="DAV:"' in res if content_matches and res.get_code() in xrange(200, 300): msg = 'Directory listing with HTTP SEARCH method was found at' \ 'directory: "%s".' % domain_path v = Vuln('Insecure DAV configuration', msg, severity.MEDIUM, res.id, self.get_name()) v.set_url(res.get_url()) v.set_method('SEARCH') self.kb_append(self, 'dav', v)
def _PROPFIND(self, domain_path): ''' Test PROPFIND method ''' content = "<?xml version='1.0'?>\r\n" content += "<a:propfind xmlns:a='DAV:'>\r\n" content += "<a:prop>\r\n" content += "<a:displayname:/>\r\n" content += "</a:prop>\r\n" content += "</a:propfind>\r\n" hdrs = Headers([('Depth', '1')]) res = self._uri_opener.PROPFIND( domain_path, data=content, headers=hdrs) if "D:href" in res and res.get_code() in xrange(200, 300): msg = 'Directory listing with HTTP PROPFIND method was found at' \ ' directory: "%s".' % domain_path v = Vuln('Insecure DAV configuration', msg, severity.MEDIUM, res.id, self.get_name()) v.set_url(res.get_url()) v.set_method('PROPFIND') self.kb_append(self, 'dav', v)
def grep(self, request, response): ''' Plugin entry point, search for directory indexing. :param request: The HTTP request object. :param response: The HTTP response object :return: None ''' if not response.is_text_or_html(): return if response.get_url().get_domain_path() in self._already_visited: return self._already_visited.add(response.get_url().get_domain_path()) html_string = response.get_body() for dir_indexing_match in self._multi_in.query(html_string): desc = 'The URL: "%s" has a directory indexing vulnerability.' desc = desc % response.get_url() v = Vuln('Directory indexing', desc, severity.LOW, response.id, self.get_name()) v.set_url(response.get_url()) self.kb_append_uniq(self, 'directory', v, 'URL') break
def _not_secure_over_https(self, request, response, cookie_obj, cookie_header_value): ''' Checks if a cookie that does NOT have a secure flag is sent over https. :param request: The http request object :param response: The http response object :param cookie_obj: The cookie object to analyze :param cookie_header_value: The cookie, as sent in the HTTP response :return: None ''' # BUGBUG: See other reference in this file for http://bugs.python.org/issue1028088 if response.get_url().get_protocol().lower() == 'https' and \ not self.SECURE_RE.search(cookie_header_value): desc = 'A cookie without the secure flag was sent in an HTTPS' \ ' response at "%s". The secure flag prevents the browser' \ ' from sending a "secure" cookie over an insecure HTTP' \ ' channel, thus preventing potential session hijacking' \ ' attacks.' desc = desc % response.get_url() v = Vuln('Secure flag missing in HTTPS cookie', desc, severity.HIGH, response.id, self.get_name()) v.set_url(response.get_url()) self._set_cookie_to_rep(v, cobj=cookie_obj) kb.kb.append(self, 'security', v)
def _universal_allow(self, forged_req, url, origin, response, allow_origin, allow_credentials, allow_methods): ''' Check if the allow_origin is set to *. :return: A list of vulnerability objects with the identified vulns (if any). ''' if allow_origin == '*': msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to "*" which is insecure'\ ' and leaves the application open to Cross-domain attacks.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN) v = Vuln('Access-Control-Allow-Origin set to "*"', msg, severity.LOW, response.get_id(), self.get_name()) v.set_url(forged_req.get_url()) self.kb_append(self, 'cors_origin', v) return self._filter_report('_universal_allow_counter', 'universal allow-origin', severity.MEDIUM, [v, ]) return []
def _universal_allow(self, forged_req, url, origin, response, allow_origin, allow_credentials, allow_methods): ''' Check if the allow_origin is set to *. :return: A list of vulnerability objects with the identified vulns (if any). ''' if allow_origin == '*': msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to "*" which is insecure'\ ' and leaves the application open to Cross-domain attacks.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN) v = Vuln('Access-Control-Allow-Origin set to "*"', msg, severity.LOW, response.get_id(), self.get_name()) v.set_url(forged_req.get_url()) self.kb_append(self, 'cors_origin', v) return self._filter_report('_universal_allow_counter', 'universal allow-origin', severity.MEDIUM, [ v, ]) return []
def _check_methods(self, url): ''' Perform some requests in order to check if we are able to retrieve some data with methods that may be wrongly enabled. ''' allowed_methods = [] for method in ['GET', 'POST', 'ABCD', 'HEAD']: method_functor = getattr(self._uri_opener, method) try: response = apply(method_functor, (url, ), {}) code = response.get_code() except: pass else: if code not in self.BAD_METHODS: allowed_methods.append((method, response.id)) if len(allowed_methods) > 0: response_ids = [i for m, i in allowed_methods] methods = ', '.join([m for m, i in allowed_methods]) + '.' desc = 'The resource: "%s" requires authentication but the access'\ ' is misconfigured and can be bypassed using these'\ ' methods: %s.' desc = desc % (url, methods) v = Vuln('Misconfigured access control', desc, severity.MEDIUM, response_ids, self.get_name()) v.set_url(url) v['methods'] = allowed_methods self.kb_append(self, 'auth', v)
def grep(self, request, response): """ Plugin entry point, find the SSN numbers. :param request: The HTTP request object. :param response: The HTTP response object :return: None. """ uri = response.get_uri() if ( response.is_text_or_html() and response.get_code() == 200 and response.get_clear_text_body() is not None and uri not in self._already_inspected ): # Don't repeat URLs self._already_inspected.add(uri) found_ssn, validated_ssn = self._find_SSN(response.get_clear_text_body()) if validated_ssn: desc = 'The URL: "%s" possibly discloses a US Social Security' ' Number: "%s".' desc = desc % (uri, validated_ssn) v = Vuln("US Social Security Number disclosure", desc, severity.LOW, response.id, self.get_name()) v.set_uri(uri) v.add_to_highlight(found_ssn) self.kb_append_uniq(self, "ssn", v, "URL")
def _parse_xssed_result(self, response): ''' Parse the result from the xssed site and create the corresponding info objects. :return: Fuzzable requests pointing to the XSS (if any) ''' html_body = response.get_body() if "<b>XSS:</b>" in html_body: # # Work! # regex_many_vulns = re.findall( "<a href='(/mirror/\d*/)' target='_blank'>", html_body) for mirror_relative_link in regex_many_vulns: mirror_url = self._xssed_url.url_join(mirror_relative_link) xss_report_response = self._uri_opener.GET(mirror_url) matches = re.findall("URL:.+", xss_report_response.get_body()) dxss = self._decode_xssed_url if self._fixed in xss_report_response.get_body(): vuln_severity = severity.LOW desc = 'This script contained a XSS vulnerability: "%s".' desc = desc % dxss(dxss(matches[0])) else: vuln_severity = severity.HIGH desc = 'According to xssed.com, this script contains a'\ ' XSS vulnerability: "%s".' desc = desc % dxss(dxss(matches[0])) v = Vuln('Potential XSS vulnerability', desc, vuln_severity, response.id, self.get_name()) v.set_url(mirror_url) kb.kb.append(self, 'xss', v) om.out.information(v.get_desc()) # # Add the fuzzable request, this is useful if I have the # XSS plugin enabled because it will re-test this and # possibly confirm the vulnerability # fuzzable_request_list = self._create_fuzzable_requests( xss_report_response) return fuzzable_request_list else: # Nothing to see here... om.out.debug('xssed_dot_com did not find any previously reported' ' XSS vulnerabilities.') return []
def _origin_echo(self, forged_req, url, origin, response, allow_origin, allow_credentials_str, allow_methods): ''' First check if the @allow_origin is set to the value we sent (@origin) and if the allow_credentials is set to True. If this test is successful (most important vulnerability) then do not check for the @allow_origin is set to the value we sent. :return: A list of vulnerability objects with the identified vulns (if any). ''' if allow_origin is not None: allow_origin = allow_origin.lower() allow_credentials = False if allow_credentials_str is not None: allow_credentials = 'true' in allow_credentials_str.lower() if origin in allow_origin: if allow_credentials: sev = severity.HIGH name = 'Insecure Access-Control-Allow-Origin with credentials' msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to the value sent in the'\ ' request\'s Origin header and a %s header with the value'\ ' set to "true", which is insecure and leaves the'\ ' application open to Cross-domain attacks which can' \ ' affect logged-in users.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_CREDENTIALS) else: sev = severity.LOW name = 'Insecure Access-Control-Allow-Origin' msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to the value sent in the'\ ' request\'s Origin header, which is insecure and leaves'\ ' the application open to Cross-domain attacks.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN) v = Vuln(name, msg, sev, response.get_id(), self.get_name()) v.set_url(forged_req.get_url()) self.kb_append(self, 'cors_origin', v) return self._filter_report('_origin_echo_counter', 'origin echoed in allow-origin', severity.HIGH, [ v, ]) return []
def _PUT(self, domain_path): ''' Tests PUT method. ''' # upload url = domain_path.url_join(rand_alpha(5)) rnd_content = rand_alnum(6) put_response = self._uri_opener.PUT(url, data=rnd_content) # check if uploaded res = self._uri_opener.GET(url, cache=True) if res.get_body() == rnd_content: msg = 'File upload with HTTP PUT method was found at resource:' \ ' "%s". A test file was uploaded to: "%s".' msg = msg % (domain_path, res.get_url()) v = Vuln('Insecure DAV configuration', msg, severity.HIGH, [put_response.id, res.id], self.get_name()) v.set_url(url) v.set_method('PUT') self.kb_append(self, 'dav', v) # Report some common errors elif put_response.get_code() == 500: msg = 'DAV seems to be incorrectly configured. The web server' \ ' answered with a 500 error code. In most cases, this means'\ ' that the DAV extension failed in some way. This error was'\ ' found at: "%s".' % put_response.get_url() i = Info('DAV incorrect configuration', msg, res.id, self.get_name()) i.set_url(url) i.set_method('PUT') self.kb_append(self, 'dav', i) # Report some common errors elif put_response.get_code() == 403: msg = 'DAV seems to be correctly configured and allowing you to'\ ' use the PUT method but the directory does not have the'\ ' correct permissions that would allow the web server to'\ ' write to it. This error was found at: "%s".' msg = msg % put_response.get_url() i = Info('DAV incorrect configuration', msg, [put_response.id, res.id], self.get_name()) i.set_url(url) i.set_method('PUT') self.kb_append(self, 'dav', i)
def _origin_echo(self, forged_req, url, origin, response, allow_origin, allow_credentials_str, allow_methods): ''' First check if the @allow_origin is set to the value we sent (@origin) and if the allow_credentials is set to True. If this test is successful (most important vulnerability) then do not check for the @allow_origin is set to the value we sent. :return: A list of vulnerability objects with the identified vulns (if any). ''' if allow_origin is not None: allow_origin = allow_origin.lower() allow_credentials = False if allow_credentials_str is not None: allow_credentials = 'true' in allow_credentials_str.lower() if origin in allow_origin: if allow_credentials: sev = severity.HIGH name = 'Insecure Access-Control-Allow-Origin with credentials' msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to the value sent in the'\ ' request\'s Origin header and a %s header with the value'\ ' set to "true", which is insecure and leaves the'\ ' application open to Cross-domain attacks which can' \ ' affect logged-in users.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_CREDENTIALS) else: sev = severity.LOW name = 'Insecure Access-Control-Allow-Origin' msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to the value sent in the'\ ' request\'s Origin header, which is insecure and leaves'\ ' the application open to Cross-domain attacks.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN) v = Vuln(name, msg, sev, response.get_id(), self.get_name()) v.set_url(forged_req.get_url()) self.kb_append(self, 'cors_origin', v) return self._filter_report('_origin_echo_counter', 'origin echoed in allow-origin', severity.HIGH, [v, ]) return []
def _parse_xssed_result(self, response): ''' Parse the result from the xssed site and create the corresponding info objects. :return: Fuzzable requests pointing to the XSS (if any) ''' html_body = response.get_body() if "<b>XSS:</b>" in html_body: # # Work! # regex_many_vulns = re.findall( "<a href='(/mirror/\d*/)' target='_blank'>", html_body) for mirror_relative_link in regex_many_vulns: mirror_url = self._xssed_url.url_join(mirror_relative_link) xss_report_response = self._uri_opener.GET(mirror_url) matches = re.findall("URL:.+", xss_report_response.get_body()) dxss = self._decode_xssed_url if self._fixed in xss_report_response.get_body(): vuln_severity = severity.LOW desc = 'This script contained a XSS vulnerability: "%s".' desc = desc % dxss(dxss(matches[0])) else: vuln_severity = severity.HIGH desc = 'According to xssed.com, this script contains a'\ ' XSS vulnerability: "%s".' desc = desc % dxss(dxss(matches[0])) v = Vuln('Potential XSS vulnerability', desc, vuln_severity, response.id, self.get_name()) v.set_url(mirror_url) kb.kb.append(self, 'xss', v) om.out.information(v.get_desc()) # # Add the fuzzable request, this is useful if I have the # XSS plugin enabled because it will re-test this and # possibly confirm the vulnerability # fuzzable_request_list = self._create_fuzzable_requests( xss_report_response) return fuzzable_request_list else: # Nothing to see here... om.out.debug('xssed_dot_com did not find any previously reported' ' XSS vulnerabilities.')
def _secure_over_http(self, request, response, cookie_obj, cookie_header_value): ''' Checks if a cookie marked as secure is sent over http. Reference: http://en.wikipedia.org/wiki/HTTP_cookie :param request: The http request object :param response: The http response object :param cookie_obj: The cookie object to analyze :param cookie_header_value: The cookie, as sent in the HTTP response :return: None ''' # BUGBUG: http://bugs.python.org/issue1028088 # # I workaround this issue by using the raw string from the HTTP # response instead of the parsed: # # cookie_obj_str = cookie_obj.output(header='') # # Bug can be reproduced like this: # >>> import Cookie # >>> cookie_object = Cookie.SimpleCookie() # >>> cookie_object.load('a=b; secure; httponly') # >>> cookie_object.output(header='') # ' a=b' # # Note the missing secure/httponly in the output return # And now, the code: if self.SECURE_RE.search(cookie_header_value) and \ response.get_url().get_protocol().lower() == 'http': desc = 'A cookie marked with the secure flag was sent over' \ ' an insecure channel (HTTP) when requesting the URL:'\ ' "%s", this usually means that the Web application was'\ ' designed to run over SSL and was deployed without'\ ' security or that the developer does not understand the'\ ' "secure" flag.' desc = desc % response.get_url() v = Vuln('Secure cookie over HTTP', desc, severity.HIGH, response.id, self.get_name()) v.set_url(response.get_url()) self._set_cookie_to_rep(v, cobj=cookie_obj) kb.kb.append(self, 'security', v)
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 _analyze_result(self, mutant, response): ''' Analyze results of the _send_mutant method. ''' # # I will only report the vulnerability once. # if self._has_no_bug(mutant): if self._header_was_injected(mutant, response): desc = 'Response splitting was found at: %s' % mutant.found_at( ) v = Vuln.from_mutant('Response splitting vulnerability', desc, severity.MEDIUM, response.id, self.get_name(), mutant) self.kb_append_uniq(self, 'response_splitting', v) # When trying to send a response splitting to php 5.1.2 I get : # Header may not contain more than a single header, new line detected for error in self.HEADER_ERRORS: if error in response: desc = 'The variable "%s" at URL "%s" modifies the HTTP'\ ' response headers, but this error was sent while'\ ' testing for response splitting: "%s".' desc = desc % (mutant.get_var(), mutant.get_url(), error) i = Info.from_mutant('Parameter modifies response headers', desc, response.id, self.get_name(), mutant) self.kb_append_uniq(self, 'response_splitting', i) return
def test_from_mutant(self): dc = DataContainer() url = URL('http://moth/') payloads = ['abc', 'def'] dc['a'] = ['1', ] dc['b'] = ['2', ] freq = FuzzableRequest(url, dc=dc) fuzzer_config = {} created_mutants = Mutant.create_mutants(freq, payloads, [], False, fuzzer_config) mutant = created_mutants[0] inst = Vuln.from_mutant('TestCase', 'desc' * 30, 'High', 1, 'plugin_name', mutant) self.assertIsInstance(inst, Vuln) self.assertEqual(inst.get_uri(), mutant.get_uri()) self.assertEqual(inst.get_url(), mutant.get_url()) self.assertEqual(inst.get_method(), mutant.get_method()) self.assertEqual(inst.get_dc(), mutant.get_dc()) self.assertEqual(inst.get_var(), mutant.get_var())
def _analyze_result(self, mutant, response): ''' Analyze results of the _send_mutant method. ''' # # I will only report the vulnerability once. # if self._has_no_bug(mutant): ldap_error_list = self._find_ldap_error(response) for ldap_error_string in ldap_error_list: if ldap_error_string not in mutant.get_original_response_body( ): desc = 'LDAP injection was found at: %s' % mutant.found_at( ) v = Vuln.from_mutant('LDAP injection vulnerability', desc, severity.HIGH, response.id, self.get_name(), mutant) v.add_to_highlight(ldap_error_string) self.kb_append_uniq(self, 'ldapi', v) break
def audit(self, freq, orig_response): ''' Tests a URL for CSRF vulnerabilities. :param freq: A FuzzableRequest ''' if not self._is_suitable(freq): return # Referer/Origin check # # IMPORTANT NOTE: I'm aware that checking for the referer header does # NOT protect the application against all cases of CSRF, but it's a # very good first step. In order to exploit a CSRF in an application # that protects using this method an intruder would have to identify # other vulnerabilities such as XSS or open redirects. # # TODO: This algorithm has lots of room for improvement if self._is_origin_checked(freq, orig_response): om.out.debug('Origin for %s is checked' % freq.get_url()) return # Does the request have CSRF token in query string or POST payload? if self._find_csrf_token(freq): om.out.debug('Token for %s exists and was checked' % freq.get_url()) return # Ok, we have found vulnerable to CSRF attack request msg = 'Cross Site Request Forgery has been found at: ' + freq.get_url() v = Vuln.from_fr('CSRF vulnerability', msg, severity.HIGH, orig_response.id, self.get_name(), freq) self.kb_append_uniq(self, 'csrf', v)
def end(self): # If all URLs implement protection, don't report anything. if not self._vuln_count: return response_ids = [_id for _id in self._ids] # If none of the URLs implement protection, simply report # ONE vulnerability that says that. if self._total_count == self._vuln_count: desc = 'The whole target has no protection (X-Frame-Options'\ ' header) against Click-Jacking attacks' # If most of the URLs implement the protection but some # don't, report ONE vulnerability saying: "Most are protected, # but x, y are not. if self._total_count > self._vuln_count: desc = 'Some URLs have no protection (X-Frame-Options header) '\ 'against Click-Jacking attacks. Among them:\n '\ ' '.join([str(url) + '\n' for url in self._vulns]) v = Vuln('Click-Jacking vulnerability', desc, severity.MEDIUM, response_ids, self.get_name()) self.kb_append(self, 'click_jacking', v) self._vulns.cleanup() self._ids.cleanup()
def _report_vuln(self, mutant, response, mod_value): ''' Create a Vuln object and store it in the KB. :return: None ''' csp_protects = site_protected_against_xss_by_csp(response) vuln_severity = severity.LOW if csp_protects else severity.MEDIUM desc = 'A Cross Site Scripting vulnerability was found at: %s' desc = desc % mutant.found_at() if csp_protects: desc += 'The risk associated with this vulnerability was lowered'\ ' because the site correctly implements CSP. The'\ ' vulnerability is still a risk for the application since'\ ' only the latest versions of some browsers implement CSP'\ ' checking.' v = Vuln.from_mutant('Cross site scripting vulnerability', desc, vuln_severity, response.id, self.get_name(), mutant) v.add_to_highlight(mod_value) self.kb_append_uniq(self, 'xss', v)
def _analyze_ips(self, ip_address_list, fuzzable_request): ''' Search all IP addresses in Bing and determine if they have more than one domain hosted on it. Store findings in KB. ''' bing_wrapper = bing(self._uri_opener) # This is the best way to search, one by one! for ip_address in ip_address_list: results = bing_wrapper.get_n_results('ip:' + ip_address, self._result_limit) results = [r.URL.base_url() for r in results] results = list(set(results)) # not vuln by default is_vulnerable = False if len(results) > 1: # We may have something... is_vulnerable = True if len(results) == 2: # Maybe we have this case: # [Mon 09 Jun 2008 01:08:26 PM ART] - http://216.244.147.14/ # [Mon 09 Jun 2008 01:08:26 PM ART] - http://www.business.com/ # Where www.business.com resolves to 216.244.147.14; so we don't really # have more than one domain in the same server. try: res0 = socket.gethostbyname(results[0].get_domain()) res1 = socket.gethostbyname(results[1].get_domain()) except: pass else: if res0 == res1: is_vulnerable = False if is_vulnerable: desc = 'The web application under test seems to be in a shared' \ ' hosting. This list of domains, and the domain of the ' \ ' web application under test, all point to the same IP' \ ' address (%s):\n' % ip_address domain_list = kb.kb.raw_read(self, 'domains') for url in results: domain = url.get_domain() desc += '- %s\n' % domain domain_list.append(domain) kb.kb.raw_write(self, 'domains', domain_list) v = Vuln.from_fr('Shared hosting', desc, severity.MEDIUM, 1, self.get_name(), fuzzable_request) v['also_in_hosting'] = results om.out.vulnerability(desc, severity=severity.MEDIUM) kb.kb.append(self, 'shared_hosting', v)
def is_injectable(self, mutant): ''' Check if this mutant is delay injectable or not. @mutant: The mutant object that I have to inject to :return: A vulnerability object or None if nothing is found ''' for delay_obj in self._get_delays(): ed = ExactDelayController(mutant, delay_obj, self._uri_opener) success, responses = ed.delay_is_controlled() if success: # Now I can be sure that I found a vuln, we control the response # time with the delay desc = 'Blind SQL injection using time delays was found at: %s' desc = desc % mutant.found_at() response_ids = [r.id for r in responses] v = Vuln.from_mutant('Blind SQL injection vulnerability', desc, severity.HIGH, response_ids, 'blind_sqli', mutant) om.out.debug(v.get_desc()) return v return None
def _with_time_delay(self, freq): ''' Tests an URL for OS Commanding vulnerabilities using time delays. :param freq: A FuzzableRequest ''' fake_mutants = create_mutants(freq, [ '', ]) for mutant in fake_mutants: if self._has_bug(mutant): continue for delay_obj in self._get_wait_commands(): ed = ExactDelayController(mutant, delay_obj, self._uri_opener) success, responses = ed.delay_is_controlled() if success: desc = 'OS Commanding was found at: %s' % mutant.found_at() v = Vuln.from_mutant('OS commanding vulnerability', desc, severity.HIGH, [r.id for r in responses], self.get_name(), mutant) v['os'] = delay_obj.get_OS() v['separator'] = delay_obj.get_separator() self.kb_append_uniq(self, 'os_commanding', v) break
def end(self): # If all URLs implement protection, don't report anything. if not self._vuln_count: return # If none of the URLs implement protection, simply report # ONE vulnerability that says that. if self._total_count == self._vuln_count: desc = 'The whole target web application has no protection (Pragma'\ ' and Cache-Control headers) against sensitive content'\ ' caching.' # If most of the URLs implement the protection but some # don't, report ONE vulnerability saying: "Most are protected, but x, y are not. if self._total_count > self._vuln_count: desc = 'Some URLs have no protection (Pragma and Cache-Control'\ ' headers) against sensitive content caching. Among them:\n' desc += ' '.join([str(url) + '\n' for url in self._vulns]) response_ids = [_id for _id in self._ids] v = Vuln('Missing cache control for HTTPS content', desc, severity.LOW, response_ids, self.get_name()) self.kb_append_uniq(self, 'cache_control', v, 'URL') self._vulns.cleanup() self._ids.cleanup()
def _analyze_result(self, mutant, response): ''' Analyze results of the _send_mutant method. ''' if self._has_bug(mutant): return dom = response.get_dom() if response.is_text_or_html() and dom is not None: elem_list = self._tag_xpath(dom) for element in elem_list: if 'src' not in element.attrib: return [] src_attr = element.attrib['src'] for url in self._test_urls: if src_attr.startswith(url): # Vuln vuln! desc = 'A phishing vector was found at: %s' desc = desc % mutant.found_at() v = Vuln.from_mutant('Phishing vector', desc, severity.LOW, response.id, self.get_name(), mutant) v.add_to_highlight(src_attr) self.kb_append_uniq(self, 'phishing_vector', v)
def _confirm_file_upload(self, path, mutant, http_response): """ Confirms if the file was uploaded to path :param path: The URL where we suspect that a file was uploaded to. :param mutant: The mutant that originated the file on the remote end :param http_response: The HTTP response asociated with sending mutant """ get_response = self._uri_opener.GET(path, cache=False) if not is_404(get_response) and self._has_no_bug(mutant): # This is necessary, if I don't do this, the session # saver will break cause REAL file objects can't # be picked mutant.set_mod_value("<file_object>") desc = "A file upload to a directory inside the webroot" " was found at: %s" % mutant.found_at() v = Vuln.from_mutant( "Insecure file upload", desc, severity.HIGH, [http_response.id, get_response.id], self.get_name(), mutant, ) v["file_dest"] = get_response.get_url() v["file_vars"] = mutant.get_file_vars() self.kb_append_uniq(self, "file_upload", v) return
def audit(self, freq, orig_response): ''' Tests an URL for ReDoS vulnerabilities using time delays. :param freq: A FuzzableRequest ''' if self.ignore_this_request(freq): return fake_mutants = create_mutants(freq, ['', ]) for mutant in fake_mutants: for delay_obj in self.get_delays(): adc = AproxDelayController(mutant, delay_obj, self._uri_opener, delay_setting=EXPONENTIALLY) success, responses = adc.delay_is_controlled() if success: # Now I can be sure that I found a vuln, we control the # response time with the delay desc = 'ReDoS was found at: %s' % mutant.found_at() response_ids = [r.id for r in responses] v = Vuln.from_mutant('ReDoS vulnerability', desc, severity.MEDIUM, response_ids, self.get_name(), mutant) self.kb_append_uniq(self, 'redos', v) break
def _analyze_result(self, mutant, response): ''' Analyze results of the _send_mutant method. ''' # # I will only report the vulnerability once. # if self._has_no_bug(mutant): if self._header_was_injected(mutant, response): desc = 'Response splitting was found at: %s' % mutant.found_at() v = Vuln.from_mutant('Response splitting vulnerability', desc, severity.MEDIUM, response.id, self.get_name(), mutant) self.kb_append_uniq(self, 'response_splitting', v) # When trying to send a response splitting to php 5.1.2 I get : # Header may not contain more than a single header, new line detected for error in self.HEADER_ERRORS: if error in response: desc = 'The variable "%s" at URL "%s" modifies the HTTP'\ ' response headers, but this error was sent while'\ ' testing for response splitting: "%s".' desc = desc % (mutant.get_var(), mutant.get_url(), error) i = Info.from_mutant('Parameter modifies response headers', desc, response.id, self.get_name(), mutant) self.kb_append_uniq(self, 'response_splitting', i) return
def _analyze_echo(self, mutant, response): ''' Analyze results of the _send_mutant method that was sent in the _with_echo method. ''' # # I will only report the vulnerability once. # if self._has_no_bug(mutant): for file_pattern_match in self._multi_in.query( response.get_body()): if file_pattern_match not in mutant.get_original_response_body( ): # Search for the correct command and separator sentOs, sentSeparator = self._get_os_separator(mutant) desc = 'OS Commanding was found at: %s' % mutant.found_at() # Create the vuln obj v = Vuln.from_mutant('OS commanding vulnerability', desc, severity.HIGH, response.id, self.get_name(), mutant) v['os'] = sentOs v['separator'] = sentSeparator v.add_to_highlight(file_pattern_match) self.kb_append_uniq(self, 'os_commanding', v) break
def _confirm_file_upload(self, path, mutant, http_response): ''' Confirms if the file was uploaded to path :param path: The URL where we suspect that a file was uploaded to. :param mutant: The mutant that originated the file on the remote end :param http_response: The HTTP response asociated with sending mutant ''' get_response = self._uri_opener.GET(path, cache=False) if not is_404(get_response) and self._has_no_bug(mutant): # This is necessary, if I don't do this, the session # saver will break cause REAL file objects can't # be picked mutant.set_mod_value('<file_object>') desc = 'A file upload to a directory inside the webroot' \ ' was found at: %s' % mutant.found_at() v = Vuln.from_mutant('Insecure file upload', desc, severity.HIGH, [http_response.id, get_response.id], self.get_name(), mutant) v['file_dest'] = get_response.get_url() v['file_vars'] = mutant.get_file_vars() self.kb_append_uniq(self, 'file_upload', v) return
def audit(self, freq, orig_response): ''' Tests an URL for ReDoS vulnerabilities using time delays. :param freq: A FuzzableRequest ''' if self.ignore_this_request(freq): return fake_mutants = create_mutants(freq, [ '', ]) for mutant in fake_mutants: for delay_obj in self.get_delays(): adc = AproxDelayController(mutant, delay_obj, self._uri_opener, delay_setting=EXPONENTIALLY) success, responses = adc.delay_is_controlled() if success: # Now I can be sure that I found a vuln, we control the # response time with the delay desc = 'ReDoS was found at: %s' % mutant.found_at() response_ids = [r.id for r in responses] v = Vuln.from_mutant('ReDoS vulnerability', desc, severity.MEDIUM, response_ids, self.get_name(), mutant) self.kb_append_uniq(self, 'redos', v) break
def _with_time_delay(self, freq): ''' Tests an URL for OS Commanding vulnerabilities using time delays. :param freq: A FuzzableRequest ''' fake_mutants = create_mutants(freq, ['', ]) for mutant in fake_mutants: if self._has_bug(mutant): continue for delay_obj in self._get_wait_commands(): ed = ExactDelayController(mutant, delay_obj, self._uri_opener) success, responses = ed.delay_is_controlled() if success: desc = 'OS Commanding was found at: %s' % mutant.found_at() v = Vuln.from_mutant('OS commanding vulnerability', desc, severity.HIGH, [r.id for r in responses], self.get_name(), mutant) v['os'] = delay_obj.get_OS() v['separator'] = delay_obj.get_separator() self.kb_append_uniq(self, 'os_commanding', v) break
def _analyze_echo(self, mutant, response): ''' Analyze results of the _send_mutant method that was sent in the _with_echo method. ''' # # I will only report the vulnerability once. # if self._has_no_bug(mutant): for file_pattern_match in self._multi_in.query(response.get_body()): if file_pattern_match not in mutant.get_original_response_body(): # Search for the correct command and separator sentOs, sentSeparator = self._get_os_separator(mutant) desc = 'OS Commanding was found at: %s' % mutant.found_at() # Create the vuln obj v = Vuln.from_mutant('OS commanding vulnerability', desc, severity.HIGH, response.id, self.get_name(), mutant) v['os'] = sentOs v['separator'] = sentSeparator v.add_to_highlight(file_pattern_match) self.kb_append_uniq(self, 'os_commanding', v) break
def _analyze_result(self, rfi_data, mutant, response): ''' Analyze results of the _send_mutant method. ''' if rfi_data.rfi_result in response: desc = 'A remote file inclusion vulnerability that allows remote' \ ' code execution was found at: %s' % mutant.found_at() v = Vuln.from_mutant('Remote code execution', desc, severity.HIGH, response.id, self.get_name(), mutant) self._vulns.append(v) elif rfi_data.rfi_result_part_1 in response \ and rfi_data.rfi_result_part_2 in response: # This means that both parts ARE in the response body but the # rfi_data.rfi_result is NOT in it. In other words, the remote # content was embedded but not executed desc = 'A remote file inclusion vulnerability without code' \ ' execution was found at: %s' % mutant.found_at() v = Vuln.from_mutant('Remote file inclusion', desc, severity.MEDIUM, response.id, self.get_name(), mutant) self._vulns.append(v) else: # # Analyze some errors that indicate that there is a RFI but # with some "configuration problems" # for error in self.RFI_ERRORS: if error in response and not error in mutant.get_original_response_body( ): desc = 'A potential remote file inclusion vulnerability' \ ' was identified by the means of application error' \ ' messages at: %s' % mutant.found_at() v = Vuln.from_mutant('Potential remote file inclusion', desc, severity.LOW, response.id, self.get_name(), mutant) v.add_to_highlight(error) self._vulns.append(v) break
def _classic_worker(self, gh, search_term): ''' Perform the searches and store the results in the kb. ''' google_list = self._google_se.get_n_results(search_term, 9) for result in google_list: # I found a vuln in the site! response = self._uri_opener.GET(result.URL, cache=True) if not is_404(response): desc = 'ghdb plugin found a vulnerability at URL: "%s".' \ ' According to GHDB the vulnerability description'\ ' is "%s".' desc = desc % (response.get_url(), gh.desc) v = Vuln('Google hack database match', desc, severity.MEDIUM, 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=severity.LOW) # Create the fuzzable requests for fr in self._create_fuzzable_requests(response): self.output_queue.put(fr)
def _universal_origin_allow_creds(self, forged_req, url, origin, response, allow_origin, allow_credentials_str, allow_methods): ''' Quote: "The above example would fail if the header was wildcarded as: Access-Control-Allow-Origin: *. Since the Access-Control-Allow-Origin explicitly mentions http://foo.example, the credential-cognizant content is returned to the invoking web content. Note that in line 23, a further cookie is set." https://developer.mozilla.org/en-US/docs/HTTP_access_control This method detects this bad implementation, which this is not a vuln it might be interesting for the developers and/or security admins. :return: Any implementation errors (as vuln objects) that might be found. ''' allow_credentials = False if allow_credentials_str is not None: allow_credentials = 'true' in allow_credentials_str.lower() if allow_credentials and allow_origin == '*': msg = 'The remote Web application, specifically "%s", returned' \ ' an %s header with the value set to "*" and an %s header'\ ' with the value set to "true" which according to Mozilla\'s'\ ' documentation is invalid. This implementation error might'\ ' affect the application behavior.' msg = msg % (forged_req.get_url(), ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_CREDENTIALS) v = Vuln('Incorrect withCredentials implementation', msg, severity.INFORMATION, response.get_id(), self.get_name()) v.set_url(forged_req.get_url()) self.kb_append(self, 'cors_origin', v) return self._filter_report( '_universal_origin_allow_creds_counter', 'withCredentials CORS implementation error', severity.INFORMATION, [ v, ]) return []
def _from_csv_get_vulns(self): file_vulns = [] vuln_reader = csv.reader(open(self.OUTPUT_FILE, 'rb'), delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) for name, method, uri, var, dc, _id, desc in vuln_reader: v = Vuln(name, desc, 'High', json.loads(_id), 'TestCase') v.set_method(method) v.set_uri(URL(uri)) v.set_var(var) v.set_dc(dc) file_vulns.append(v) return file_vulns
def _parse_cookie(self, request, response, cookie_header_value): ''' If the response sets more than one Cookie, this method will be called once for each "Set-Cookie" header. BUGBUG: The urllib2 library concatenates , values of repeated headers. See HTTPMessage.addheader() in httplib.py :param request: The HTTP request object. :param response: The HTTP response object :param cookie_header_value: The cookie, as sent in the HTTP response :return: The cookie object or None if the parsing failed ''' cookie_object = Cookie.SimpleCookie() # FIXME: Workaround for bug in Python's Cookie.py # # if type(rawdata) == type(""): # self.__ParseString(rawdata) # # Should read "if isinstance(rawdata, basestring)" cookie_header_value = cookie_header_value.encode('utf-8') try: # Note to self: This line may print some chars to the console cookie_object.load(cookie_header_value) except Cookie.CookieError: desc = 'The remote Web application sent a cookie with an' \ ' incorrect format: "%s" that does NOT respect the RFC.' desc = desc % cookie_header_value i = Vuln('Invalid cookie', desc, severity.HIGH, response.id, self.get_name()) i.set_url(response.get_url()) self._set_cookie_to_rep(i, cstr=cookie_header_value) # The cookie is invalid, this is worth mentioning ;) kb.kb.append(self, 'invalid-cookies', i) return None else: return cookie_object