def attack(self): methods = "" if self.do_get: methods += "G" if self.do_post: methods += "PF" mutator = Mutator(methods=methods, payloads=self.random_string, qs_inject=self.must_attack_query_string, skip=self.options.get("skipped_parameters")) http_resources = self.persister.get_links( attack_module=self.name) if self.do_get else [] forms = self.persister.get_forms( attack_module=self.name) if self.do_post else [] for original_request in chain(http_resources, forms): if self.verbose >= 1: print("[+] {}".format(original_request)) for mutated_request, parameter, taint, flags in mutator.mutate( original_request): try: # We don't display the mutated request here as the payload is not interesting try: response = self.crawler.send(mutated_request) except ReadTimeout: # We just inserted harmless characters, if we get a timeout here, it's not interesting continue else: # We keep a history of taint values we sent because in case of stored value, the taint code # may be found in another webpage by the permanentxss module. self.tried_xss[taint] = (mutated_request, parameter, flags) # Reminder: valid_xss_content_type is not called before before content is not necessary # reflected here, may be found in another webpage so we have to inject tainted values # even if the Content-Type seems uninteresting. if taint.lower() in response.content.lower( ) and valid_xss_content_type(mutated_request): # Simple text injection worked in HTML response, let's try with JS code payloads = generate_payloads( response.content, taint, self.PAYLOADS_FILE) # TODO: check that and make it better if flags.method == PayloadType.get: method = "G" elif flags.method == PayloadType.file: method = "F" else: method = "P" self.attempt_exploit(method, payloads, original_request, parameter, taint) except KeyboardInterrupt as exception: yield exception yield original_request
def test_missing_value(): req2 = Request("http://perdu.com/directory/?high=tone", ) # Filename of the target URL should be injected but it is missing here, we should not raise a mutation mutator = Mutator(payloads=[("[FILE_NAME]::$DATA", Flags())]) count = 0 for __ in mutator.mutate(req2): count += 1 assert count == 0
async def attempt_exploit(self, method, payloads, injection_request, parameter, taint, output_request): timeouted = False page = injection_request.path saw_internal_error = False output_url = output_request.url 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( injection_request): if self.verbose == 2: logging.info("[¨] {0}".format(evil_request)) try: await self.crawler.async_send(evil_request) except ReadTimeout: self.network_errors += 1 if timeouted: continue self.log_orange("---") self.log_orange(Messages.MSG_TIMEOUT, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(evil_request.http_repr()) self.log_orange("---") if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(xss_param) await self.add_anom_medium( request_id=injection_request.path_id, category=Messages.RES_CONSUMPTION, request=evil_request, info=anom_msg, parameter=xss_param) timeouted = True except RequestError: self.network_errors += 1 continue else: try: response = await self.crawler.async_send(output_request) except RequestError: self.network_errors += 1 continue if (response.status not in (301, 302, 303) and valid_xss_content_type(evil_request) and self.check_payload(response, xss_flags, taint)): if page == output_request.path: description = _( "Permanent XSS vulnerability found via injection in the parameter {0}" ).format(xss_param) else: description = _( "Permanent XSS vulnerability found in {0} by injecting" " the parameter {1} of {2}").format( output_request.url, parameter, page) if has_strong_csp(response): description += ".\n" + _( "Warning: Content-Security-Policy is present!") await self.add_vuln_high( request_id=injection_request.path_id, category=NAME, request=evil_request, parameter=xss_param, info=description) if xss_param == "QUERY_STRING": injection_msg = Messages.MSG_QS_INJECT else: injection_msg = Messages.MSG_PARAM_INJECT self.log_red("---") # TODO: a last parameter should give URL used to pass the vulnerable parameter self.log_red(injection_msg, self.MSG_VULN, output_url, xss_param) if has_strong_csp(response): self.log_red( _("Warning: Content-Security-Policy is present!")) self.log_red(Messages.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 = Messages.MSG_QS_500 else: anom_msg = Messages.MSG_PARAM_500.format(xss_param) await self.add_anom_high( request_id=injection_request.path_id, category=Messages.ERROR_500, request=evil_request, info=anom_msg, parameter=xss_param) self.log_orange("---") self.log_orange(Messages.MSG_500, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(evil_request.http_repr()) self.log_orange("---") saw_internal_error = True
async def boolean_based_attack(self, request: Request, parameters_to_skip: set): try: good_response = await self.crawler.async_send(request) good_status = good_response.status good_redirect = good_response.redirection_url # good_title = response.title good_hash = good_response.text_only_md5 except ReadTimeout: self.network_errors += 1 return methods = "" if self.do_get: methods += "G" if self.do_post: methods += "PF" mutator = Mutator( methods=methods, payloads=generate_boolean_payloads(), qs_inject=self.must_attack_query_string, skip=self.options.get("skipped_parameters", set()) | parameters_to_skip ) page = request.path current_parameter = None skip_till_next_parameter = False current_session = None test_results = [] last_mutated_request = None for mutated_request, parameter, __, flags in mutator.mutate(request): # Make sure we always pass through the following block to see changes of payloads formats if current_session != flags.platform: # We start a new set of payloads, let's analyse results for previous ones if test_results and all(test_results): # We got a winner skip_till_next_parameter = True vuln_info = _("SQL Injection") if current_parameter == "QUERY_STRING": vuln_message = Messages.MSG_QS_INJECT.format(vuln_info, page) else: vuln_message = _("{0} via injection in the parameter {1}").format(vuln_info, current_parameter) await self.add_vuln_critical( request_id=request.path_id, category=NAME, request=last_mutated_request, info=vuln_message, parameter=current_parameter, wstg=WSTG_CODE ) log_red("---") log_red( Messages.MSG_QS_INJECT if current_parameter == "QUERY_STRING" else Messages.MSG_PARAM_INJECT, vuln_info, page, current_parameter ) log_red(Messages.MSG_EVIL_REQUEST) log_red(last_mutated_request.http_repr()) log_red("---") # Don't forget to reset session and results current_session = flags.platform test_results = [] if current_parameter != parameter: # Start attacking a new parameter, forget every state we kept current_parameter = parameter skip_till_next_parameter = False elif skip_till_next_parameter: # If parameter is vulnerable, just skip till next parameter continue if test_results and not all(test_results): # No need to go further: one of the tests was wrong continue log_verbose(f"[¨] {mutated_request}") try: response = await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 # We need all cases to make sure SQLi is there test_results.append(False) continue comparison = ( response.status == good_status and response.redirection_url == good_redirect and response.text_only_md5 == good_hash ) test_results.append(comparison == (flags.section == "True")) last_mutated_request = mutated_request
class mod_xss(Attack): """Detects stored (aka permanent) Cross-Site Scripting vulnerabilities on the web server.""" name = "xss" # two dict exported for permanent XSS scanning # GET_XSS structure : # {uniq_code : http://url/?param1=value1¶m2=uniq_code¶m3..., next_uniq_code : ...} # GET_XSS = {} # POST XSS structure : # {uniq_code: [target_url, {param1: val1, param2: uniq_code, param3:...}, referer_ul], next_uniq_code : [...]...} # POST_XSS = {} tried_xss = {} PHP_SELF = [] # key = taint code, value = (payload, flags) successful_xss = {} PAYLOADS_FILE = path_join(Attack.DATA_DIR, "xssPayloads.ini") MSG_VULN = _("XSS vulnerability") def __init__(self, crawler, persister, attack_options, stop_event): Attack.__init__(self, crawler, persister, attack_options, stop_event) methods = "" if self.do_get: methods += "G" if self.do_post: methods += "PF" self.mutator = Mutator(methods=methods, payloads=random_string_with_flags, qs_inject=self.must_attack_query_string, skip=self.options.get("skipped_parameters")) async def attack(self, request: Request): for mutated_request, parameter, taint, flags in self.mutator.mutate( request): # We don't display the mutated request here as the payload is not interesting try: response = await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 # We just inserted harmless characters, if we get a timeout here, it's not interesting continue else: # We keep a history of taint values we sent because in case of stored value, the taint code # may be found in another webpage by the permanentxss module. self.tried_xss[taint] = (mutated_request, parameter, flags) # Reminder: valid_xss_content_type is not called before before content is not necessary # reflected here, may be found in another webpage so we have to inject tainted values # even if the Content-Type seems uninteresting. if taint.lower() in response.content.lower( ) and valid_xss_content_type(mutated_request): # Simple text injection worked in HTML response, let's try with JS code payloads = generate_payloads(response.content, taint, self.PAYLOADS_FILE, self.external_endpoint) # TODO: check that and make it better if flags.method == PayloadType.get: method = "G" elif flags.method == PayloadType.file: method = "F" else: method = "P" await self.attempt_exploit(method, payloads, request, parameter, taint) async 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: logging.info("[¨] {0}".format(evil_request)) try: response = await self.crawler.async_send(evil_request) except ReadTimeout: self.network_errors += 1 if timeouted: continue self.log_orange("---") self.log_orange(Messages.MSG_TIMEOUT, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(evil_request.http_repr()) self.log_orange("---") if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(xss_param) await self.add_anom_medium(request_id=original_request.path_id, category=Messages.RES_CONSUMPTION, request=evil_request, info=anom_msg, parameter=xss_param) timeouted = True except RequestError: self.network_errors += 1 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_strong_csp(response): message += ".\n" + _( "Warning: Content-Security-Policy is present!") await self.add_vuln_medium( request_id=original_request.path_id, category=NAME, request=evil_request, parameter=xss_param, info=message) if xss_param == "QUERY_STRING": injection_msg = Messages.MSG_QS_INJECT else: injection_msg = Messages.MSG_PARAM_INJECT self.log_red("---") self.log_red(injection_msg, self.MSG_VULN, page, xss_param) if has_strong_csp(response): self.log_red( _("Warning: Content-Security-Policy is present!")) self.log_red(Messages.MSG_EVIL_REQUEST) self.log_red(evil_request.http_repr()) self.log_red("---") # stop trying payloads and jump to the next parameter break if response.status == 500 and not saw_internal_error: if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_500 else: anom_msg = Messages.MSG_PARAM_500.format(xss_param) await self.add_anom_high( request_id=original_request.path_id, category=Messages.ERROR_500, request=evil_request, info=anom_msg, parameter=xss_param) self.log_orange("---") self.log_orange(Messages.MSG_500, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(evil_request.http_repr()) self.log_orange("---") saw_internal_error = True def check_payload(self, response, flags, taint): config_reader = ConfigParser(interpolation=None) config_reader.read_file( open(path_join(self.DATA_DIR, self.PAYLOADS_FILE))) for section in config_reader.sections(): if section == flags.section: expected_value = config_reader[section]["value"].replace( '[EXTERNAL_ENDPOINT]', self.external_endpoint) expected_value = expected_value.replace("__XSS__", taint) tag_names = config_reader[section]["tag"].split(",") attribute = config_reader[section]["attribute"] case_sensitive = config_reader[section].getboolean( "case_sensitive") match_type = config_reader[section].get("match_type", "exact") attribute_constraint = { attribute: True } if attribute not in ["full_string", "string"] else {} for tag in response.soup.find_all(tag_names, attrs=attribute_constraint): non_exec_parent = find_non_exec_parent(tag) if non_exec_parent and not (tag.name == "frame" and non_exec_parent == "frameset"): continue if attribute == "string" and tag.string: if case_sensitive: if expected_value in tag.string: return True else: if expected_value.lower() in tag.string.lower(): return True elif attribute == "full_string" and tag.string: if case_sensitive: if match_type == "exact" and expected_value == tag.string.strip( ): return True if match_type == "starts_with" and tag.string.strip( ).startswith(expected_value): return True else: if match_type == "exact" and expected_value.lower( ) == tag.string.strip().lower(): return True if match_type == "starts_with" and \ tag.string.strip().lower().startswith(expected_value.lower()): return True else: # Found attribute specified in .ini file in attributes of the HTML tag if attribute in tag.attrs: if case_sensitive: if match_type == "exact" and tag[ attribute] == expected_value: return True if match_type == "starts_with" and tag[ attribute].startswith(expected_value): return True else: if match_type == "exact" and tag[ attribute].lower( ) == expected_value.lower(): return True if match_type == "starts_with" and \ expected_value.lower().startswith(tag[attribute].lower()): return True break return False
async def finish(self): endpoint_url = f"{self.internal_endpoint}get_ssrf.php?session_id={self._session_id}" logging.info(_("[*] Asking endpoint URL {} for results, please wait...").format(endpoint_url)) await sleep(2) # A la fin des attaques on questionne le endpoint pour savoir s'il a été contacté endpoint_request = Request(endpoint_url) try: response = await self.crawler.async_send(endpoint_request) except RequestError: self.network_errors += 1 logging.error(_("[!] Unable to request endpoint URL '{}'").format(self.internal_endpoint)) else: data = response.json if isinstance(data, dict): for request_id in data: original_request = await self.persister.get_path_by_id(request_id) if original_request is None: raise ValueError("Could not find the original request with that ID") page = original_request.path for hex_param in data[request_id]: parameter = unhexlify(hex_param).decode("utf-8") for infos in data[request_id][hex_param]: request_url = infos["url"] # Date in ISO format request_date = infos["date"] request_ip = infos["ip"] request_method = infos["method"] # request_size = infos["size"] if parameter == "QUERY_STRING": vuln_message = Messages.MSG_QS_INJECT.format(self.MSG_VULN, page) else: vuln_message = _( "{0} via injection in the parameter {1}.\n" "The target performed an outgoing HTTP {2} request at {3} with IP {4}.\n" "Full request can be seen at {5}" ).format( self.MSG_VULN, parameter, request_method, request_date, request_ip, request_url ) mutator = Mutator( methods="G" if original_request.method == "GET" else "PF", payloads=[("http://external.url/page", Flags())], qs_inject=self.must_attack_query_string, parameters=[parameter], skip=self.options.get("skipped_parameters") ) mutated_request, __, __, __ = next(mutator.mutate(original_request)) await self.add_vuln_critical( request_id=original_request.path_id, category=NAME, request=mutated_request, info=vuln_message, parameter=parameter, wstg=WSTG_CODE ) log_red("---") log_red( Messages.MSG_QS_INJECT if parameter == "QUERY_STRING" else Messages.MSG_PARAM_INJECT, self.MSG_VULN, page, parameter ) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---")
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 finish(self): endpoint_url = "{}get_xxe.php?session_id={}".format( self.internal_endpoint, self._session_id) print( _("[*] Asking endpoint URL {} for results, please wait...").format( endpoint_url)) sleep(2) # A la fin des attaques on questionne le endpoint pour savoir s'il a été contacté endpoint_request = Request(endpoint_url) try: response = self.crawler.send(endpoint_request) except RequestException: self.network_errors += 1 print( _("[!] Unable to request endpoint URL '{}'").format( self.internal_endpoint)) return data = response.json if not isinstance(data, dict): return for request_id in data: original_request = self.persister.get_path_by_id(request_id) if original_request is None: continue # raise ValueError("Could not find the original request with ID {}".format(request_id)) page = original_request.path for hex_param in data[request_id]: parameter = unhexlify(hex_param).decode("utf-8") for infos in data[request_id][hex_param]: request_url = infos["url"] # Date in ISO format request_date = infos["date"] request_ip = infos["ip"] request_size = infos["size"] payload_name = infos["payload"] if parameter == "QUERY_STRING": vuln_message = Messages.MSG_QS_INJECT.format( self.MSG_VULN, page) elif parameter == "raw body": vuln_message = _( "Out-Of-Band {0} by sending raw XML in request body" ).format(self.MSG_VULN) else: vuln_message = _( "Out-Of-Band {0} via injection in the parameter {1}" ).format(self.MSG_VULN, parameter) more_infos = _( "The target sent {0} bytes of data to the endpoint at {1} with IP {2}.\n" "Received data can be seen at {3}.").format( request_size, request_date, request_ip, request_url) vuln_message += "\n" + more_infos # placeholder if shit happens payload = ( "<xml>" "See https://phonexicum.github.io/infosec/xxe.html#attack-vectors" "</xml>") for payload, flags in self.payloads: if "{}.dtd".format(payload_name) in payload: payload = payload.replace( "[PATH_ID]", str(original_request.path_id)) payload = payload.replace("[PARAM_AS_HEX]", "72617720626f6479") break if parameter == "raw body": mutated_request = Request(original_request.path, method="POST", enctype="text/xml", post_params=payload) elif parameter == "QUERY_STRING": mutated_request = Request("{}?{}".format( original_request.path, quote(payload)), method="GET") elif parameter in original_request.get_keys or parameter in original_request.post_keys: mutator = Mutator( methods="G" if original_request.method == "GET" else "P", payloads=[(payload, Flags())], qs_inject=self.must_attack_query_string, parameters=[parameter], skip=self.options.get("skipped_parameters")) mutated_request, __, __, __ = next( mutator.mutate(original_request)) else: mutator = FileMutator( payloads=[(payload, Flags())], parameters=[parameter], skip=self.options.get("skipped_parameters")) mutated_request, __, __, __ = next( mutator.mutate(original_request)) self.add_vuln(request_id=original_request.path_id, category=NAME, level=HIGH_LEVEL, request=mutated_request, info=vuln_message, parameter=parameter) self.log_red("---") self.log_red(vuln_message) self.log_red(Messages.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---")
def test_mutations(): req = Request("http://perdu.com/page.php", method="POST", get_params=[["p", "login.php"]], post_params=[["user", "admin"], ["password", "letmein"]], file_params=[["file", ["pix.gif", "GIF89a", "image/gif"]]]) mutator = Mutator(payloads=[("INJECT", set())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 4 mutator = Mutator(payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set()), ("PAYLOAD_3", set())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 12 mutator = Mutator(methods="G", payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set()), ("PAYLOAD_3", set())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 3 mutator = Mutator(methods="P", payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set()), ("PAYLOAD_3", set())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 6 mutator = Mutator(methods="PF", payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set()), ("PAYLOAD_3", set())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 9 mutator = Mutator(payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set()), ("PAYLOAD_3", set())], parameters=["user", "file"]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 6 mutator = Mutator(payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set()), ("PAYLOAD_3", set())], skip={"p"}) count = 0 for __, __, __, __ in mutator.mutate(req): count += 1 assert count == 9 # JSESSIONID is marked as annoying parameter req2 = Request("http://perdu.com/page.php", method="POST", get_params=[["JSESSIONID", "deadbeef"]], post_params=[["user", "admin"], ["password", "letmein"]], file_params=[["file", ["pix.gif", "GIF89a", "image/gif"]]]) mutator = Mutator(payloads=[("INJECT", set())]) count = 0 for __ in mutator.mutate(req2): count += 1 assert count == 3 # Inject into query string. Will only work if method is GET without any parameter req3 = Request("http://perdu.com/page.php") mutator = Mutator(payloads=[("PAYLOAD_1", set()), ("PAYLOAD_2", set())], qs_inject=True) count = 0 for __, __, __, __ in mutator.mutate(req3): count += 1 assert count == 2
class ModuleXss(Attack): """Detects stored (aka permanent) Cross-Site Scripting vulnerabilities on the web server.""" name = "xss" # two dict exported for permanent XSS scanning # GET_XSS structure : # {uniq_code : http://url/?param1=value1¶m2=uniq_code¶m3..., next_uniq_code : ...} # GET_XSS = {} # POST XSS structure : # {uniq_code: [target_url, {param1: val1, param2: uniq_code, param3:...}, referer_ul], next_uniq_code : [...]...} # POST_XSS = {} tried_xss = {} PHP_SELF = [] # key = taint code, value = (payload, flags) successful_xss = {} PAYLOADS_FILE = path_join(Attack.DATA_DIR, "xssPayloads.ini") MSG_VULN = _("XSS vulnerability") RANDOM_WEBSITE = f"https://{random_string(length=6)}.com/" def __init__(self, crawler, persister, attack_options, stop_event): Attack.__init__(self, crawler, persister, attack_options, stop_event) methods = "" if self.do_get: methods += "G" if self.do_post: methods += "PF" self.mutator = Mutator(methods=methods, payloads=random_string_with_flags, qs_inject=self.must_attack_query_string, skip=self.options.get("skipped_parameters")) @property def external_endpoint(self): return self.RANDOM_WEBSITE async def attack(self, request: Request): for mutated_request, parameter, taint, flags in self.mutator.mutate( request): # We don't display the mutated request here as the payload is not interesting try: response = await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 # We just inserted harmless characters, if we get a timeout here, it's not interesting continue else: # We keep a history of taint values we sent because in case of stored value, the taint code # may be found in another webpage by the permanentxss module. self.tried_xss[taint] = (mutated_request, parameter, flags) # Reminder: valid_xss_content_type is not called before before content is not necessary # reflected here, may be found in another webpage so we have to inject tainted values # even if the Content-Type seems uninteresting. if taint.lower() in response.content.lower( ) and valid_xss_content_type(mutated_request): # Simple text injection worked in HTML response, let's try with JS code payloads = generate_payloads(response.content, taint, self.PAYLOADS_FILE, self.external_endpoint) # TODO: check that and make it better if flags.method == PayloadType.get: method = "G" elif flags.method == PayloadType.file: method = "F" else: method = "P" await self.attempt_exploit(method, payloads, request, parameter, taint) async 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): log_verbose(f"[¨] {evil_request}") try: response = await self.crawler.async_send(evil_request) except ReadTimeout: self.network_errors += 1 if timeouted: continue log_orange("---") log_orange(Messages.MSG_TIMEOUT, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(evil_request.http_repr()) log_orange("---") if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(xss_param) await self.add_anom_medium(request_id=original_request.path_id, category=Messages.RES_CONSUMPTION, request=evil_request, info=anom_msg, parameter=xss_param, wstg=RESOURCE_CONSUMPTION_WSTG_CODE) timeouted = True except RequestError: self.network_errors += 1 else: if (response.status not in (301, 302, 303) and valid_xss_content_type(evil_request) and check_payload(self.DATA_DIR, self.PAYLOADS_FILE, self.external_endpoint, self.proto_endpoint, 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_strong_csp(response): message += ".\n" + _( "Warning: Content-Security-Policy is present!") await self.add_vuln_medium( request_id=original_request.path_id, category=NAME, request=evil_request, parameter=xss_param, info=message, wstg=WSTG_CODE) if xss_param == "QUERY_STRING": injection_msg = Messages.MSG_QS_INJECT else: injection_msg = Messages.MSG_PARAM_INJECT log_red("---") log_red(injection_msg, self.MSG_VULN, page, xss_param) if has_strong_csp(response): log_red( _("Warning: Content-Security-Policy is present!")) log_red(Messages.MSG_EVIL_REQUEST) log_red(evil_request.http_repr()) log_red("---") # stop trying payloads and jump to the next parameter break if response.status == 500 and not saw_internal_error: if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_500 else: anom_msg = Messages.MSG_PARAM_500.format(xss_param) await self.add_anom_high( request_id=original_request.path_id, category=Messages.ERROR_500, request=evil_request, info=anom_msg, parameter=xss_param, wstg=INTERNAL_ERROR_WSTG_CODE) log_orange("---") log_orange(Messages.MSG_500, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(evil_request.http_repr()) log_orange("---") saw_internal_error = True
async 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): log_verbose(f"[¨] {evil_request}") try: response = await self.crawler.async_send(evil_request) except ReadTimeout: self.network_errors += 1 if timeouted: continue log_orange("---") log_orange(Messages.MSG_TIMEOUT, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(evil_request.http_repr()) log_orange("---") if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(xss_param) await self.add_anom_medium(request_id=original_request.path_id, category=Messages.RES_CONSUMPTION, request=evil_request, info=anom_msg, parameter=xss_param, wstg=RESOURCE_CONSUMPTION_WSTG_CODE) timeouted = True except RequestError: self.network_errors += 1 else: if (response.status not in (301, 302, 303) and valid_xss_content_type(evil_request) and check_payload(self.DATA_DIR, self.PAYLOADS_FILE, self.external_endpoint, self.proto_endpoint, 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_strong_csp(response): message += ".\n" + _( "Warning: Content-Security-Policy is present!") await self.add_vuln_medium( request_id=original_request.path_id, category=NAME, request=evil_request, parameter=xss_param, info=message, wstg=WSTG_CODE) if xss_param == "QUERY_STRING": injection_msg = Messages.MSG_QS_INJECT else: injection_msg = Messages.MSG_PARAM_INJECT log_red("---") log_red(injection_msg, self.MSG_VULN, page, xss_param) if has_strong_csp(response): log_red( _("Warning: Content-Security-Policy is present!")) log_red(Messages.MSG_EVIL_REQUEST) log_red(evil_request.http_repr()) log_red("---") # stop trying payloads and jump to the next parameter break if response.status == 500 and not saw_internal_error: if xss_param == "QUERY_STRING": anom_msg = Messages.MSG_QS_500 else: anom_msg = Messages.MSG_PARAM_500.format(xss_param) await self.add_anom_high( request_id=original_request.path_id, category=Messages.ERROR_500, request=evil_request, info=anom_msg, parameter=xss_param, wstg=INTERNAL_ERROR_WSTG_CODE) log_orange("---") log_orange(Messages.MSG_500, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(evil_request.http_repr()) log_orange("---") saw_internal_error = True
def attack(self, http_resources, forms): methods = "" if self.do_get: methods += "G" if self.do_post: methods += "PF" mutator = Mutator( methods=methods, payloads=self.random_string, qs_inject=self.must_attack_query_string, skip=self.options.get("skipped_parameters") ) for original_request in chain(http_resources, forms): timeouted = False page = original_request.path saw_internal_error = False if self.verbose >= 1: print("[+] {}".format(original_request)) for mutated_request, parameter, taint, flags in mutator.mutate(original_request): try: # We don't display the mutated request here as the payload is not interesting try: response = self.crawler.send(mutated_request) except ReadTimeout: # We just inserted harmless characters, if we get a timeout here, it's not interesting continue else: if taint in response.content: # Simple text injection worked, let's try with JS code payloads = [(js_code, set()) for js_code in self.generate_payloads(response.content, taint)] # TODO: check that and make it better if PayloadType.get in flags: method = "G" elif PayloadType.file in flags: method = "F" else: method = "P" # We keep a history of taint values we sent because in case of stored value, the taint code # may be found in another webpage by the permanentxss module. if mutated_request.method == "GET": self.GET_XSS[taint] = (mutated_request, parameter) else: self.POST_XSS[taint] = (mutated_request, parameter) 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) data = response.content 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: # TODO: call _valid_xss_content_type sooner ? if self._valid_xss_content_type(evil_request) and data: if taint.lower() in data.lower(): self.SUCCESSFUL_XSS[taint] = xss_payload self.add_vuln( request_id=original_request.path_id, category=Vulnerability.XSS, level=Vulnerability.HIGH_LEVEL, request=evil_request, parameter=xss_param, info=_("XSS vulnerability found via injection" " in the parameter {0}").format(xss_param) ) 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 ) 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 except KeyboardInterrupt as exception: yield exception yield original_request
def attempt_exploit(self, method, payloads, injection_request, parameter, taint, output_request): timeouted = False page = injection_request.path saw_internal_error = False output_url = output_request.url 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( injection_request): if self.verbose == 2: print("[¨] {0}".format(evil_request)) try: 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=injection_request.path_id, category=Anomaly.RES_CONSUMPTION, level=Anomaly.MEDIUM_LEVEL, request=evil_request, info=anom_msg, parameter=xss_param) timeouted = True else: try: response = self.crawler.send(output_request) except ReadTimeout: continue if self.check_payload(response, xss_flags, taint): if page == output_request.path: description = _( "Permanent XSS vulnerability found via injection in the parameter {0}" ).format(xss_param) else: description = _( "Permanent XSS vulnerability found in {0} by injecting" " the parameter {1} of {2}").format( output_request.url, parameter, page) self.add_vuln(request_id=injection_request.path_id, category=Vulnerability.XSS, level=Vulnerability.HIGH_LEVEL, request=evil_request, parameter=xss_param, info=description) if xss_param == "QUERY_STRING": injection_msg = Vulnerability.MSG_QS_INJECT else: injection_msg = Vulnerability.MSG_PARAM_INJECT self.log_red("---") # TODO: a last parameter should give URL used to pass the vulnerable parameter self.log_red(injection_msg, self.MSG_VULN, output_url, xss_param) 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=injection_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