def test_csp_detection(): url = "http://perdu.com/" responses.add( responses.GET, url, status=200, adding_headers={ "Content-Type": "text/html" } ) resp = requests.get(url) page = Page(resp) assert not has_csp(page) url = "http://perdu.com/http_csp" responses.add( responses.GET, url, status=200, adding_headers={ "Content-Type": "text/html", "Content-Security-Policy": "blahblah;" } ) resp = requests.get(url) page = Page(resp) assert has_csp(page) url = "http://perdu.com/meta_csp" responses.add( responses.GET, url, status=200, adding_headers={ "Content-Type": "text/html" }, body="""<html> <head> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';"> </head> <body>Hello there</body> </html>""" ) resp = requests.get(url) page = Page(resp) assert has_csp(page)
def attempt_exploit(self, method, payloads, original_request, parameter, taint): timeouted = False page = original_request.path saw_internal_error = False attack_mutator = Mutator(methods=method, payloads=payloads, qs_inject=self.must_attack_query_string, parameters=[parameter], skip=self.options.get("skipped_parameters")) for evil_request, xss_param, xss_payload, xss_flags in attack_mutator.mutate( original_request): if self.verbose == 2: print("[¨] {0}".format(evil_request)) try: response = self.crawler.send(evil_request) except ReadTimeout: if timeouted: continue self.log_orange("---") self.log_orange(Anomaly.MSG_TIMEOUT, page) self.log_orange(Anomaly.MSG_EVIL_REQUEST) self.log_orange(evil_request.http_repr()) self.log_orange("---") if xss_param == "QUERY_STRING": anom_msg = Anomaly.MSG_QS_TIMEOUT else: anom_msg = Anomaly.MSG_PARAM_TIMEOUT.format(xss_param) self.add_anom(request_id=original_request.path_id, category=Anomaly.RES_CONSUMPTION, level=Anomaly.MEDIUM_LEVEL, request=evil_request, info=anom_msg, parameter=xss_param) timeouted = True else: if (response.status not in (301, 302, 303) and valid_xss_content_type(evil_request) and self.check_payload(response, xss_flags, taint)): self.successful_xss[taint] = (xss_payload, xss_flags) message = _( "XSS vulnerability found via injection in the parameter {0}" ).format(xss_param) if has_csp(response): message += ".\n" + _( "Warning: Content-Security-Policy is present!") self.add_vuln(request_id=original_request.path_id, category=Vulnerability.XSS, level=Vulnerability.HIGH_LEVEL, request=evil_request, parameter=xss_param, info=message) if xss_param == "QUERY_STRING": injection_msg = Vulnerability.MSG_QS_INJECT else: injection_msg = Vulnerability.MSG_PARAM_INJECT self.log_red("---") self.log_red(injection_msg, self.MSG_VULN, page, xss_param) if has_csp(response): self.log_red( _("Warning: Content-Security-Policy is present!")) self.log_red(Vulnerability.MSG_EVIL_REQUEST) self.log_red(evil_request.http_repr()) self.log_red("---") # stop trying payloads and jump to the next parameter break elif response.status == 500 and not saw_internal_error: if xss_param == "QUERY_STRING": anom_msg = Anomaly.MSG_QS_500 else: anom_msg = Anomaly.MSG_PARAM_500.format(xss_param) self.add_anom(request_id=original_request.path_id, category=Anomaly.ERROR_500, level=Anomaly.HIGH_LEVEL, request=evil_request, info=anom_msg, parameter=xss_param) self.log_orange("---") self.log_orange(Anomaly.MSG_500, page) self.log_orange(Anomaly.MSG_EVIL_REQUEST) self.log_orange(evil_request.http_repr()) self.log_orange("---") saw_internal_error = True
def attack(self): """This method searches XSS which could be permanently stored in the web application""" get_resources = self.persister.get_links(attack_module=self.name) if self.do_get else [] for original_request in get_resources: if not valid_xss_content_type(original_request) or original_request.status in (301, 302, 303): # If that content-type can't be interpreted as HTML by browsers then it is useless # Same goes for redirections continue url = original_request.url target_req = web.Request(url) referer = original_request.referer headers = {} if referer: headers["referer"] = referer if self.verbose >= 1: print("[+] {}".format(url)) try: response = self.crawler.send(target_req, headers=headers) data = response.content except Timeout: continue except OSError as exception: # TODO: those error messages are useless, don't give any valuable information print(_("error: {0} while attacking {1}").format(exception.strerror, url)) continue except Exception as exception: print(_("error: {0} while attacking {1}").format(exception, url)) continue # Should we look for taint codes sent with GET in the webpages? # Exploiting those may imply sending more GET requests # Search in the page source for every taint code used by mod_xss for taint in self.TRIED_XSS: input_request = self.TRIED_XSS[taint][0] # Such situations should not occur as it would be stupid to block POST (or GET) requests for mod_xss # and not mod_permanentxss, but it is possible so let's filter that. if not self.do_get and input_request.method == "GET": continue if not self.do_post and input_request.method == "POST": continue if taint.lower() in data.lower(): # Code found in the webpage ! # Did mod_xss saw this as a reflected XSS ? if taint in self.SUCCESSFUL_XSS: # Yes, it means XSS payloads were injected, not just tainted code. payload, flags = self.SUCCESSFUL_XSS[taint] if self.check_payload(response, flags, taint): # If we can find the payload again, this is in fact a stored XSS get_params = input_request.get_params post_params = input_request.post_params file_params = input_request.file_params referer = input_request.referer # The following trick may seems dirty but it allows to treat GET and POST requests # the same way. for params_list in [get_params, post_params, file_params]: for i in range(len(params_list)): parameter, value = params_list[i] parameter = quote(parameter) if value != taint: continue if params_list is file_params: params_list[i][1][0] = payload else: params_list[i][1] = payload # we found the xss payload again -> stored xss vuln evil_request = web.Request( input_request.path, method=input_request.method, get_params=get_params, post_params=post_params, file_params=file_params, referer=referer ) if original_request.path == input_request.path: description = _( "Permanent XSS vulnerability found via injection in the parameter {0}" ).format(parameter) else: description = _( "Permanent XSS vulnerability found in {0} by injecting" " the parameter {1} of {2}" ).format( original_request.url, parameter, input_request.path ) if has_csp(response): description += ".\n" + _("Warning: Content-Security-Policy is present!") self.add_vuln( request_id=original_request.path_id, category=Vulnerability.XSS, level=Vulnerability.HIGH_LEVEL, request=evil_request, parameter=parameter, info=description ) if parameter == "QUERY_STRING": injection_msg = Vulnerability.MSG_QS_INJECT else: injection_msg = Vulnerability.MSG_PARAM_INJECT self.log_red("---") self.log_red( injection_msg, self.MSG_VULN, original_request.path, parameter ) if has_csp(response): self.log_red(_("Warning: Content-Security-Policy is present!")) self.log_red(Vulnerability.MSG_EVIL_REQUEST) self.log_red(evil_request.http_repr()) self.log_red("---") # FIX: search for the next code in the webpage # Ok the content is stored, but will we be able to inject javascript? else: parameter = self.TRIED_XSS[taint][1] payloads = generate_payloads(response.content, taint, self.independant_payloads) flags = self.TRIED_XSS[taint][2] # TODO: check that and make it better if PayloadType.get in flags: method = "G" elif PayloadType.file in flags: method = "F" else: method = "P" self.attempt_exploit(method, payloads, input_request, parameter, taint, original_request) yield original_request