async def worker(self, queue: asyncio.Queue, resolvers: Iterator[str], root_domain: str, bad_responses: Set[str]): while True: try: domain = queue.get_nowait().strip() except asyncio.QueueEmpty: await asyncio.sleep(.05) else: queue.task_done() if domain == "__exit__": break try: resolver = dns.asyncresolver.Resolver() resolver.timeout = 10. resolver.nameservers = [ next(resolvers) for __ in range(10) ] answers = await resolver.resolve(domain, 'CNAME', raise_on_no_answer=False) except (socket.gaierror, UnicodeError): continue except (dns.asyncresolver.NXDOMAIN, dns.exception.Timeout): continue except (dns.name.EmptyLabel, dns.resolver.NoNameservers) as exception: logging.warning(f"{domain}: {exception}") continue for answer in answers: cname = answer.to_text().strip(".") if cname in bad_responses: continue log_verbose(_(f"Record {domain} points to {cname}")) try: if get_root_domain(cname) == root_domain: # If it is an internal CNAME (like www.target.tld to target.tld) just ignore continue except (TldDomainNotFound, TldBadUrl): logging.warning(f"{cname} is not a valid domain name") continue if await self.takeover.check(domain, cname): log_red("---") log_red( _(f"CNAME {domain} to {cname} seems vulnerable to takeover" )) log_red("---") await self.add_vuln_high( category=NAME, info= _(f"CNAME {domain} to {cname} seems vulnerable to takeover" ), request=Request(f"https://{domain}/"), wstg=WSTG_CODE)
async def attack(self, request: Request): page = request.path self.excluded_path.add(page) option_request = Request(page, "OPTIONS", referer=request.referer, link_depth=request.link_depth) log_verbose(f"[+] {option_request}") try: response = await self.crawler.async_send(option_request) except RequestError: self.network_errors += 1 return if response.is_success or response.is_redirect: methods = response.headers.get("allow", '').upper().split(',') methods = {method.strip() for method in methods if method.strip()} interesting_methods = sorted(methods - self.KNOWN_METHODS) if interesting_methods: log_orange("---") log_orange( _("Interesting methods allowed on {}: {}").format( page, ", ".join(interesting_methods))) await self.add_addition( category=NAME, request=option_request, info=_("Interesting methods allowed on {}: {}").format( page, ", ".join(interesting_methods)), wstg=WSTG_CODE) log_orange("---")
async def attack(self, request: Request): page = request.path for mutated_request, parameter, __, __ in self.mutator.mutate(request): log_verbose(f"[¨] {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), 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("---")
async def attack(self, request: Request): # Let's just send payloads, we don't care of the response as what we want to know is if the target # contacted the endpoint. for mutated_request, _parameter, _payload, _flags in self.mutator.mutate(request): log_verbose(f"[¨] {mutated_request}") try: await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 continue
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("---")
async def test_directory(self, path: str): log_verbose(f"[¨] Testing directory {path}") test_page = Request(path + "does_n0t_exist.htm") try: response = await self.crawler.async_send(test_page) except RequestError: self.network_errors += 1 return if response.status not in [403, 404]: # we don't want to deal with this at the moment return tasks = set() pending_count = 0 payload_iterator = iter(self.payloads) while True: if pending_count < self.options[ "tasks"] and not self._stop_event.is_set(): try: candidate, __ = next(payload_iterator) except StopIteration: pass else: url = path + candidate if url not in self.known_dirs and url not in self.known_pages and url not in self.new_resources: task = asyncio.create_task(self.check_path(url)) tasks.add(task) if not tasks: break done_tasks, pending_tasks = await asyncio.wait( tasks, timeout=0.01, return_when=asyncio.FIRST_COMPLETED) pending_count = len(pending_tasks) for task in done_tasks: try: await task except RequestError: self.network_errors += 1 tasks.remove(task) if self._stop_event.is_set(): for task in pending_tasks: task.cancel() tasks.remove(task)
async def attack(self, request: Request): page = request.path for payload, __ in self.payloads: if self._stop_event.is_set(): break if request.file_name: if "[FILE_" not in payload: continue payload = payload.replace("[FILE_NAME]", request.file_name) payload = payload.replace("[FILE_NOEXT]", splitext(request.file_name)[0]) url = page.replace(request.file_name, payload) else: if "[FILE_" in payload: continue url = urljoin(request.path, payload) log_verbose(f"[¨] {url}") self.attacked_get.append(page) evil_req = Request(url) try: response = await self.crawler.async_send(evil_req) except RequestError: self.network_errors += 1 continue if response and response.status == 200: # FIXME: Right now we cannot remove the pylint: disable line because the current I18N system # uses the string as a token so we cannot use f string # pylint: disable=consider-using-f-string log_red(_("Found backup file {}".format(evil_req.url))) await self.add_vuln_low( request_id=request.path_id, category=NAME, request=evil_req, info=_("Backup file {0} found for {1}").format(url, page), wstg=WSTG_CODE )
async def attack_upload(self, original_request): mutator = FileMutator(payloads=self.payloads) current_parameter = None vulnerable_parameter = False for mutated_request, parameter, _payload, flags in mutator.mutate( original_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: pattern = search_pattern(response.content, self.flag_to_patterns(flags)) if pattern and not await self.false_positive( original_request, pattern): await self.add_vuln_high( request_id=original_request.path_id, category=NAME, request=mutated_request, info="XXE vulnerability leading to file disclosure", parameter=parameter, wstg=WSTG_CODE) log_red("---") log_red(Messages.MSG_PARAM_INJECT, self.MSG_VULN, original_request.url, parameter) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---") vulnerable_parameter = True self.vulnerables.add(original_request.path_id)
async def attack_body(self, original_request): for payload, tags in self.payloads: payload = payload.replace("[PATH_ID]", str(original_request.path_id)) payload = payload.replace("[PARAM_AS_HEX]", "72617720626f6479") # raw body mutated_request = Request(original_request.url, method="POST", enctype="text/xml", post_params=payload) log_verbose(f"[¨] {mutated_request}") try: response = await self.crawler.async_send(mutated_request) except RequestError: self.network_errors += 1 continue else: pattern = search_pattern(response.content, self.flag_to_patterns(tags)) if pattern and not await self.false_positive( original_request, pattern): await self.add_vuln_high( request_id=original_request.path_id, category=NAME, request=mutated_request, info="XXE vulnerability leading to file disclosure", parameter="raw body", wstg=WSTG_CODE) log_red("---") log_red("{0} in {1} leading to file disclosure", self.MSG_VULN, original_request.url) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---") self.vulnerables.add(original_request.path_id) break
async def attack(self, request: Request): url = request.path referer = request.referer headers = {} if referer: headers["referer"] = referer evil_req = Request(url, method="ABC") try: response = await self.crawler.async_send(evil_req, headers=headers) except RequestError: self.network_errors += 1 return if response.status == 404 or response.status < 400 or response.status >= 500: # Every 4xx status should be uninteresting (specially bad request in our case) unblocked_content = response.content log_red("---") await self.add_vuln_medium( request_id=request.path_id, category=NAME, request=evil_req, info=_("{0} bypassable weak restriction").format(evil_req.url), wstg=WSTG_CODE) log_red(_("Weak restriction bypass vulnerability: {0}"), evil_req.url) log_red( _("HTTP status code changed from {0} to {1}").format( request.status, response.status)) log_verbose(_("Source code:")) log_verbose(unblocked_content) log_red("---") self.attacked_get.append(url)
async def process_line(self, line): match = match_or = match_and = False fail = fail_or = False osv_id = line[1] path = line[3] method = line[4] vuln_desc = line[10] post_data = line[11] path = path.replace("@CGIDIRS", "/cgi-bin/") path = path.replace("@ADMIN", "/admin/") path = path.replace("@NUKE", "/modules/") path = path.replace("@PHPMYADMIN", "/phpMyAdmin/") path = path.replace("@POSTNUKE", "/postnuke/") path = re.sub(r"JUNK\((\d+)\)", lambda x: self.junk_string[:int(x.group(1))], path) if path[0] == "@": return if not path.startswith("/"): path = "/" + path try: url = f"{self.parts.scheme}://{self.parts.netloc}{path}" except UnicodeDecodeError: return if method == "GET": evil_request = Request(url) elif method == "POST": evil_request = Request(url, post_params=post_data, method=method) else: evil_request = Request(url, post_params=post_data, method=method) if method == "GET": log_verbose(f"[¨] {evil_request.url}") else: log_verbose(f"[¨] {evil_request.http_repr()}") try: response = await self.crawler.async_send(evil_request) page = response.content code = response.status except (RequestError, ConnectionResetError): self.network_errors += 1 return except Exception as exception: logging.warning( f"{exception} occurred with URL {evil_request.url}") return raw = " ".join([x + ": " + y for x, y in response.headers.items()]) raw += page # See https://github.com/sullo/nikto/blob/master/program/plugins/nikto_tests.plugin for reference expected_status_codes = [] # First condition (match) if len(line[5]) == 3 and line[5].isdigit(): expected_status_code = int(line[5]) expected_status_codes.append(expected_status_code) if code == expected_status_code: match = True else: if line[5] in raw: match = True # Second condition (or) if line[6] != "": if len(line[6]) == 3 and line[6].isdigit(): expected_status_code = int(line[6]) expected_status_codes.append(expected_status_code) if code == expected_status_code: match_or = True else: if line[6] in raw: match_or = True # Third condition (and) if line[7] != "": if len(line[7]) == 3 and line[7].isdigit(): if code == int(line[7]): match_and = True else: if line[7] in raw: match_and = True else: match_and = True # Fourth condition (fail) if line[8] != "": if len(line[8]) == 3 and line[8].isdigit(): if code == int(line[8]): fail = True else: if line[8] in raw: fail = True # Fifth condition (or) if line[9] != "": if len(line[9]) == 3 and line[9].isdigit(): if code == int(line[9]): fail_or = True else: if line[9] in raw: fail_or = True if ((match or match_or) and match_and) and not (fail or fail_or): if expected_status_codes: if await self.is_false_positive(evil_request, expected_status_codes): return log_red("---") log_red(vuln_desc) log_red(url) refs = [] if osv_id != "0": refs.append("https://vulners.com/osvdb/OSVDB:" + osv_id) # CERT cert_advisory = re.search("(CA-[0-9]{4}-[0-9]{2})", vuln_desc) if cert_advisory is not None: refs.append("http://www.cert.org/advisories/" + cert_advisory.group(0) + ".html") # SecurityFocus securityfocus_bid = re.search("BID-([0-9]{4})", vuln_desc) if securityfocus_bid is not None: refs.append("http://www.securityfocus.com/bid/" + securityfocus_bid.group(1)) # Mitre.org mitre_cve = re.search("((CVE|CAN)-[0-9]{4}-[0-9]{4,})", vuln_desc) if mitre_cve is not None: refs.append("http://cve.mitre.org/cgi-bin/cvename.cgi?name=" + mitre_cve.group(0)) # CERT Incidents cert_incident = re.search("(IN-[0-9]{4}-[0-9]{2})", vuln_desc) if cert_incident is not None: refs.append("http://www.cert.org/incident_notes/" + cert_incident.group(0) + ".html") # Microsoft Technet ms_bulletin = re.search("(MS[0-9]{2}-[0-9]{3})", vuln_desc) if ms_bulletin is not None: refs.append( "http://www.microsoft.com/technet/security/bulletin/" + ms_bulletin.group(0) + ".asp") info = vuln_desc if refs: log_red(_("References:")) log_red(" " + "\n ".join(refs)) info += "\n" + _("References:") + "\n" info += "\n".join(refs) log_red("---") await self.add_vuln_high(category=NAME, request=evil_request, info=info, wstg=WSTG_CODE)
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 attack(self, request: Request): warned = False timeouted = False page = request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False for mutated_request, parameter, __, flags 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 if flags.payload_type == PayloadType.time and request.path_id in self.false_positive_timeouts: # If the original request is known to gives timeout and payload is time-based, just skip # and move to next payload continue log_verbose(f"[¨] {mutated_request}") try: response = await self.crawler.async_send(mutated_request) except ReadTimeout: if flags.payload_type == PayloadType.time: if await self.does_timeout(request): self.network_errors += 1 self.false_positive_timeouts.add(request.path_id) continue vuln_info = _("Blind command execution") 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("---") vulnerable_parameter = True continue self.network_errors += 1 if timeouted: continue log_orange("---") log_orange(Messages.MSG_TIMEOUT, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(mutated_request.http_repr()) log_orange("---") if parameter == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(parameter) await self.add_anom_medium(request_id=request.path_id, category=Messages.RES_CONSUMPTION, request=mutated_request, info=anom_msg, parameter=parameter, wstg=RESOURCE_CONSUMPTION_WSTG_CODE) timeouted = True except RequestError: self.network_errors += 1 else: # No timeout raised vuln_info, executed, warned = self._find_pattern_in_response( response.content, warned) if vuln_info: # An error message implies that a vulnerability may exists if parameter == "QUERY_STRING": vuln_message = Messages.MSG_QS_INJECT.format( vuln_info, page) log_message = Messages.MSG_QS_INJECT else: vuln_message = _( "{0} via injection in the parameter {1}").format( vuln_info, parameter) log_message = Messages.MSG_PARAM_INJECT 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(log_message, vuln_info, page, parameter) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---") if executed: # We reached maximum exploitation for this parameter, don't send more payloads vulnerable_parameter = True continue elif response.is_server_error 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("---")
async def attack(self, request: Request): page = request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False for mutated_request, parameter, _payload, _flags 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 ReadTimeout: # The request with time based payload did timeout, what about a regular request? if await self.does_timeout(request): self.network_errors += 1 logging.error( "[!] Too much lag from website, can't reliably test time-based blind SQL" ) break if parameter == "QUERY_STRING": vuln_message = Messages.MSG_QS_INJECT.format( self.MSG_VULN, page) log_message = Messages.MSG_QS_INJECT else: vuln_message = _( "{0} via injection in the parameter {1}").format( self.MSG_VULN, parameter) log_message = Messages.MSG_PARAM_INJECT 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(log_message, self.MSG_VULN, 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 continue except RequestError: self.network_errors += 1 continue else: if response.is_server_error 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("---")
async def attack(self, request: Request): warned = False timeouted = False page = request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False for mutated_request, parameter, payload, flags 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 ReadTimeout: self.network_errors += 1 if timeouted: continue log_orange("---") log_orange(Messages.MSG_TIMEOUT, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(mutated_request.http_repr()) log_orange("---") if parameter == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(parameter) await self.add_anom_medium(request_id=request.path_id, category=Messages.RES_CONSUMPTION, request=mutated_request, info=anom_msg, parameter=parameter, wstg=RESOURCE_CONSUMPTION_WSTG_CODE) timeouted = True except RequestError: self.network_errors += 1 continue else: file_warning = None # original_payload = self.payload_to_rules[flags.section] for rule in self.payload_to_rules[flags.section]: if rule in response.content: found_pattern = rule vulnerable_method = self.rules_to_messages[rule] inclusion_succeed = True break else: # No successful inclusion or directory traversal but perhaps we can control something inclusion_succeed = False file_warning = find_warning_message( response.content, payload) if file_warning: found_pattern = file_warning.pattern vulnerable_method = file_warning.function else: found_pattern = vulnerable_method = None if found_pattern: # Interesting pattern found, either inclusion or error message if await self.is_false_positive(request, found_pattern): continue if not inclusion_succeed: if warned: # No need to warn more than once continue # Mark as eventuality vulnerable_method = _("Possible {0} vulnerability" ).format(vulnerable_method) warned = True # An error message implies that a vulnerability may exists if parameter == "QUERY_STRING": vuln_message = Messages.MSG_QS_INJECT.format( vulnerable_method, page) else: vuln_message = _( "{0} via injection in the parameter {1}").format( vulnerable_method, parameter) constraint_message = "" if file_warning and file_warning.uri: constraints = has_prefix_or_suffix( payload, file_warning.uri) if constraints: constraint_message += _("Constraints: {}").format( ", ".join(constraints)) vuln_message += " (" + constraint_message + ")" 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, vulnerable_method, page, parameter) if constraint_message: log_red(constraint_message) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---") if inclusion_succeed: # We reached maximum exploitation for this parameter, don't send more payloads vulnerable_parameter = True continue elif response.is_server_error 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("---")
async def async_analyze(self, request) -> Tuple[bool, List]: async with self._sem: self._processed_requests.append(request) # thread safe log_verbose(f"[+] {request}") dir_name = request.dir_name # Currently not exploited. Would be interesting though but then it should be implemented separately # Maybe in another task as we don't want to spend to much time in this function # async with self._shared_lock: # # lock to prevent launching duplicates requests that would otherwise waste time # if dir_name not in self._custom_404_codes: # invalid_page = "zqxj{0}.html".format("".join([choice(ascii_letters) for __ in range(10)])) # invalid_resource = web.Request(dir_name + invalid_page) # try: # page = await self._crawler.async_get(invalid_resource) # self._custom_404_codes[dir_name] = page.status # except httpx.RequestError: # pass self._hostnames.add(request.hostname) resource_url = request.url try: page = await self._crawler.async_send(request) except (TypeError, UnicodeDecodeError) as exception: logging.debug(f"{exception} with url {resource_url}") # debug return False, [] # TODO: what to do of connection errors ? sleep a while before retrying ? except ConnectionError: logging.error(_("[!] Connection error with URL"), resource_url) return False, [] except httpx.RequestError as error: logging.error( _("[!] {} with url {}").format(error.__class__.__name__, resource_url)) return False, [] if self._max_files_per_dir: async with self._shared_lock: self._file_counts[dir_name] += 1 if self._qs_limit and request.parameters_count: async with self._shared_lock: self._pattern_counts[request.pattern] += 1 if request.link_depth == self._max_depth: # We are at the edge of the depth so next links will have depth + 1 so to need to parse the page. return True, [] # Above this line we need the content of the page. As we are in stream mode we must force reading the body. await page.read() # Sur les ressources statiques le content-length est généralement indiqué if self._max_page_size > 0: if page.raw_size > self._max_page_size: await page.clean() return False, [] await asyncio.sleep(0) resources = self.extract_links(page, request) # TODO: there's more situations where we would not want to attack the resource... must check this if page.is_directory_redirection: return False, resources return True, resources
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): log_verbose(f"[¨] {evil_request}") try: 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=injection_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 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 check_payload(self.DATA_DIR, self.PAYLOADS_FILE, self.external_endpoint, self.proto_endpoint, 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, wstg=WSTG_CODE) if xss_param == "QUERY_STRING": injection_msg = Messages.MSG_QS_INJECT else: injection_msg = Messages.MSG_PARAM_INJECT log_red("---") # TODO: a last parameter should give URL used to pass the vulnerable parameter log_red(injection_msg, self.MSG_VULN, output_url, 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=injection_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 attack(self, request: Request): timeouted = False page = request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False if request.url not in self.attacked_urls: await self.attack_body(request) self.attacked_urls.add(request.url) if request.path_id in self.vulnerables: return if request.is_multipart: await self.attack_upload(request) if request.path_id in self.vulnerables: return for mutated_request, parameter, __, flags 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 ReadTimeout: self.network_errors += 1 if timeouted: continue log_orange("---") log_orange(Messages.MSG_TIMEOUT, page) log_orange(Messages.MSG_EVIL_REQUEST) log_orange(mutated_request.http_repr()) log_orange("---") if parameter == "QUERY_STRING": anom_msg = Messages.MSG_QS_TIMEOUT else: anom_msg = Messages.MSG_PARAM_TIMEOUT.format(parameter) await self.add_anom_medium(request_id=request.path_id, category=Messages.RES_CONSUMPTION, request=mutated_request, info=anom_msg, parameter=parameter, wstg=RESOURCE_CONSUMPTION_WSTG_CODE) timeouted = True except RequestError: self.network_errors += 1 continue else: pattern = search_pattern(response.content, self.flag_to_patterns(flags)) if pattern and not await self.false_positive(request, pattern): # An error message implies that a vulnerability may exists 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}").format( self.MSG_VULN, parameter) await self.add_vuln_high(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, self.MSG_VULN, 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 continue if 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("---")
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
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 (not response.is_redirect 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.is_server_error 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