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): url = request.path self.attacked_get.append(url) # We can't see anything by printing requests because payload is in headers so let's print nothing :) evil_req = Request(url) try: resp = await self.crawler.async_send(evil_req, headers=self.hdrs) except RequestError: self.network_errors += 1 return if resp: data = resp.content if self.rand_string in data: log_red( _("URL {0} seems vulnerable to Shellshock attack!").format( url)) await self.add_vuln_high( request_id=request.path_id, category=NAME, request=evil_req, info=_("URL {0} seems vulnerable to Shellshock attack" ).format(url), wstg=WSTG_CODE)
async def attack(self, request: Request): self.finished = True request_to_root = Request(request.url) try: response = await self.crawler.async_get(request_to_root, follow_redirects=True) except RequestError: self.network_errors += 1 return if "Content-Security-Policy" not in response.headers: log_red(MSG_NO_CSP) await self.add_vuln_low(category=NAME, request=request_to_root, info=MSG_NO_CSP, wstg=WSTG_CODE) else: csp_dict = csp_header_to_dict( response.headers["Content-Security-Policy"]) for policy_name in CSP_CHECK_LISTS: result = check_policy_values(policy_name, csp_dict) if result <= 0: if result == -1: info = MSG_CSP_MISSING.format(policy_name) else: # result == 0 info = MSG_CSP_UNSAFE.format(policy_name) log_red(info) await self.add_vuln_low(category=NAME, request=request_to_root, info=info, wstg=WSTG_CODE)
async def check_header(self, response: Page, request: Request, header: str, check_list: List[str], info: str, log: str, wstg: str): log_blue(log) if not self.is_set(response, header, check_list): log_red(info) await self.add_vuln_low(category=NAME, request=request, info=info, wstg=wstg) else: log_green("OK")
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 attack(self, request: Request): csrf_value = self.is_csrf_present(request) # check if token is present if not csrf_value: vuln_message = _("Lack of anti CSRF token") elif not await self.is_csrf_verified(request): vuln_message = _( "CSRF token '{}' is not properly checked in backend").format( self.csrf_string) elif not self.is_csrf_robust(csrf_value): vuln_message = _( "CSRF token '{}' might be easy to predict").format( self.csrf_string) else: return self.already_vulnerable.append((request.url, request.post_keys)) log_red("---") log_red(vuln_message) log_red(request.http_repr()) log_red("---") await self.add_vuln_medium(request_id=request.path_id, category=NAME, request=request, info=vuln_message, wstg=WSTG_CODE)
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 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_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): self.finished = True cookies = self.crawler.session_cookies for cookie in cookies.jar: log_blue(_("Checking cookie : {}").format(cookie.name)) if not self.check_httponly_flag(cookie): log_red(INFO_COOKIE_HTTPONLY.format(cookie.name)) await self.add_vuln_low( category=COOKIE_HTTPONLY_DISABLED, request=request, info=INFO_COOKIE_HTTPONLY.format(cookie.name), wstg=COOKIE_HTTPONLY_WSTG ) if not self.check_secure_flag(cookie): log_red(INFO_COOKIE_SECURE.format(cookie.name)) await self.add_vuln_low( category=COOKIE_SECURE_DISABLED, request=request, info=INFO_COOKIE_SECURE.format(cookie.name), wstg=COOKIE_SECURE_WSTG )
async def _verify_url_vulnerability(self, request: Request, param_uuid: uuid.UUID): if not await self._verify_dns(str(param_uuid)): return await self.add_vuln_critical( category=NAME, request=request, info=_("URL {0} seems vulnerable to Log4Shell attack") \ .format(request.url), parameter="", wstg=WSTG_CODE ) log_red("---") log_red(_("URL {0} seems vulnerable to Log4Shell attack"), request.url) log_red(request.http_repr()) log_red("---")
async def _verify_header_vulnerability(self, modified_request: Request, header: str, payload: str, unique_id: uuid.UUID): if await self._verify_dns(str(unique_id)) is True: await self.add_vuln_critical( category=NAME, request=modified_request, info=_("URL {0} seems vulnerable to Log4Shell attack by using the {1} {2}") \ .format(modified_request.url, "header", header), parameter=f"{header}: {payload}", wstg=WSTG_CODE ) log_red("---") log_red( _("URL {0} seems vulnerable to Log4Shell attack by using the {1} {2}" ), modified_request.url, "header", header) log_red(modified_request.http_repr()) log_red("---")
async def _verify_param_vulnerability(self, request: Request, param_uuid: uuid.UUID, param_name: str): if not await self._verify_dns(str(param_uuid)): return element_type = "query parameter" if request.method == "GET" else "body parameter" await self.add_vuln_critical( category=NAME, request=request, info=_("URL {0} seems vulnerable to Log4Shell attack by using the {1} {2}") \ .format(request.url, element_type, param_name), parameter=f"{param_name}", wstg=WSTG_CODE ) log_red("---") log_red( _("URL {0} seems vulnerable to Log4Shell attack by using the {1} {2}" ), request.url, element_type, param_name) log_red(request.http_repr()) log_red("---")
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 check_path(self, url): page = Request(url) try: response = await self.crawler.async_send(page) except RequestError: self.network_errors += 1 return False if response.redirection_url: loc = response.redirection_url if response.is_directory_redirection: log_red(f"Found webpage {loc}") self.new_resources.append(loc) else: log_red(f"Found webpage {page.path}") self.new_resources.append(page.path) return True if response.status not in [403, 404, 429]: log_red(f"Found webpage {page.path}") self.new_resources.append(page.path) return True return False
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): try: page = await self.crawler.async_get(Request(request.referer), follow_redirects=True) except RequestError: self.network_errors += 1 return login_form, username_field_idx, password_field_idx = page.find_login_form( ) if not login_form: return try: failure_text = await self.send_credentials(login_form, username_field_idx, password_field_idx, "invalid", "invalid") if self.check_success_auth(failure_text): # Ignore this case as it raises false positives return except RequestError: self.network_errors += 1 return tasks = set() pending_count = 0 found = False creds_iterator = product(self.get_usernames(), self.get_passwords()) while True: if pending_count < self.options[ "tasks"] and not self._stop_event.is_set() and not found: try: username, password = next(creds_iterator) except StopIteration: pass else: task = asyncio.create_task( self.test_credentials(login_form, username_field_idx, password_field_idx, username, password, failure_text)) 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: result = await task except RequestError: self.network_errors += 1 else: if result: found = True username, password = result vuln_message = _( "Credentials found for URL {} : {} / {}").format( request.referer, username, password) # Recreate the request that succeed in order to print and store it post_params = login_form.post_params get_params = login_form.get_params if login_form.method == "POST": post_params[username_field_idx][1] = username post_params[password_field_idx][1] = password else: get_params[username_field_idx][1] = username get_params[password_field_idx][1] = password evil_request = Request( path=login_form.url, method=login_form.method, post_params=post_params, get_params=get_params, referer=login_form.referer, link_depth=login_form.link_depth) await self.add_vuln_low(request_id=request.path_id, category=NAME, request=evil_request, info=vuln_message, wstg=WSTG_CODE) log_red("---") log_red(vuln_message) log_red(Messages.MSG_EVIL_REQUEST) log_red(evil_request.http_repr()) log_red("---") tasks.remove(task) if self._stop_event.is_set() or found: # If we found valid credentials we need to stop pending tasks as they may generate false positives # because the session is opened on the website and next attempts may appear as logged in for task in pending_tasks: task.cancel() tasks.remove(task)
def process_certificate_info(certinfo_result): for cert_deployment in certinfo_result.certificate_deployments: leaf_certificate = cert_deployment.received_certificate_chain[0] message = _("Certificate subject: {0}").format( get_common_name(leaf_certificate.subject)) log_blue(message) yield INFO_LEVEL, message message = _("Alt. names: {0}").format( extract_dns_subject_alternative_names(leaf_certificate)) log_blue(message) yield INFO_LEVEL, message message = _("Issuer: {0}").format( get_common_name(leaf_certificate.issuer)) log_blue(message) yield INFO_LEVEL, message if not cert_deployment.leaf_certificate_subject_matches_hostname: message = _( "Requested hostname doesn't match those in the certificate") log_red(message) yield CRITICAL_LEVEL, message if not cert_deployment.received_chain_has_valid_order: message = _("Certificate chain is in invalid order") log_orange(message) yield MEDIUM_LEVEL, message public_key = leaf_certificate.public_key() if isinstance(public_key, EllipticCurvePublicKey): key_size = public_key.curve.key_size else: key_size = public_key.key_size if public_key.__class__.__name__ == "_RSAPublicKey": algorithm = "RSA" elif public_key.__class__.__name__ == "_EllipticCurvePublicKey": algorithm = "ECC" else: algorithm = public_key.__class__.__name__ message = _("Key: {0} {1} bits").format(algorithm, key_size) log_blue(message) yield INFO_LEVEL, message message = _("Signature Algorithm: {0}").format( leaf_certificate.signature_hash_algorithm.name) log_blue(message) yield INFO_LEVEL, message # print(f"Valid from {leaf_certificate.not_valid_before} to {leaf_certificate.not_valid_after}") if leaf_certificate.not_valid_after > datetime.utcnow(): # We should add a method for humanize inside our language package # _t = humanize.i18n.activate("fr_FR") message = _("Certificate expires in ") + \ humanize.precisedelta(leaf_certificate.not_valid_after - datetime.utcnow()) log_green(message) yield INFO_LEVEL, message else: message = _("Certificate has expired at" ) + f" {leaf_certificate.not_valid_after}" log_red(message) yield CRITICAL_LEVEL, message if not cert_deployment.leaf_certificate_is_ev: message = _("Certificate doesn't use Extended Validation") log_orange(message) yield MEDIUM_LEVEL, message # https://en.wikipedia.org/wiki/OCSP_stapling if not cert_deployment.leaf_certificate_has_must_staple_extension: message = _("OCSP Must-Staple extension is missing") log_orange(message) yield MEDIUM_LEVEL, message if cert_deployment.leaf_certificate_signed_certificate_timestamps_count is None: message = _("Certificate transparency:") + " " + _( "Unknown (Your OpenSSL version is not recent enough)") log_orange(message) yield MEDIUM_LEVEL, message elif cert_deployment.leaf_certificate_signed_certificate_timestamps_count: message = _("Certificate transparency:") + " " + _("Yes") + \ f" ({cert_deployment.leaf_certificate_signed_certificate_timestamps_count} SCT)" log_green(message) yield INFO_LEVEL, message else: message = _("Certificate transparency:") + " " + _("No") log_red(message) yield HIGH_LEVEL, message if cert_deployment.verified_chain_has_sha1_signature: message = _( "One of the certificate in the chain is signed using SHA-1") log_red(message) yield HIGH_LEVEL, message for validation_result in cert_deployment.path_validation_results: if not validation_result.was_validation_successful: message = _( "Certificate is invalid for {} trust store: {}").format( validation_result.trust_store.name, validation_result.openssl_error_string) log_red(message) yield CRITICAL_LEVEL, message # Currently we stop at the first certificate of the server, maybe improve later # Right now several certificates generates too much confusion in report break
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("---")
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 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 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("---")
def analyze(hostname: str, port: int) -> List[Tuple[int, str]]: results = [] # Define the server that you want to scan try: server_location = ServerNetworkLocation(hostname, port) except ServerHostnameCouldNotBeResolved: log_red(_("Could not resolve {0}"), hostname) return results # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest( server_location=server_location, scan_commands={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.ROBOT, ScanCommand.HEARTBLEED, ScanCommand.TLS_COMPRESSION, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.SESSION_RENEGOTIATION, ScanCommand.HTTP_HEADERS }, network_configuration=ServerNetworkConfiguration( tls_server_name_indication=server_location.hostname, network_timeout=5, network_max_retries=2)) scanner.queue_scans([server_scan_req]) # TLS 1.2 / 1.3 results good_protocols = { ScanCommand.TLS_1_2_CIPHER_SUITES: "TLS v1.2", ScanCommand.TLS_1_3_CIPHER_SUITES: "TLS v1.3" } # https://blog.mozilla.org/security/2014/10/14/the-poodle-attack-and-the-end-of-ssl-3-0/ # https://blog.qualys.com/product-tech/2018/11/19/grade-change-for-tls-1-0-and-tls-1-1-protocols bad_protocols = { ScanCommand.SSL_2_0_CIPHER_SUITES: "SSL v2", ScanCommand.SSL_3_0_CIPHER_SUITES: "SSL v3", ScanCommand.TLS_1_0_CIPHER_SUITES: "TLS v1.0", ScanCommand.TLS_1_1_CIPHER_SUITES: "TLS v1.1" } # Then retrieve the results for result in scanner.get_results(): log_blue("\n" + _("Results for") + f" {result.server_location.hostname}:") deprecated_protocols = [] if result.connectivity_error_trace: # Stuff like connection timeout log_red(result.connectivity_error_trace) continue for scan_command in result.scan_result.__annotations__: scan_results = getattr(result.scan_result, scan_command) if scan_results.error_reason: log_red(scan_results.error_reason) continue if scan_results.status != ScanCommandAttemptStatusEnum.COMPLETED: continue if scan_command == ScanCommand.CERTIFICATE_INFO: for level, message in process_certificate_info( scan_results.result): results.append((level, message)) elif scan_command in bad_protocols: if scan_results.result.accepted_cipher_suites: deprecated_protocols.append(bad_protocols[scan_command]) elif scan_command == ScanCommand.ROBOT: if scan_results.result.robot_result in ( RobotScanResultEnum.VULNERABLE_WEAK_ORACLE, RobotScanResultEnum.VULNERABLE_STRONG_ORACLE): message = _("Server is vulnerable to ROBOT attack") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.HEARTBLEED: if scan_results.result.is_vulnerable_to_heartbleed: message = _("Server is vulnerable to Heartbleed attack") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_COMPRESSION: if scan_results.result.supports_compression: message = _( "Server is vulnerable to CRIME attack (compression is supported)" ) log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_FALLBACK_SCSV: if not scan_results.result.supports_fallback_scsv: message = _( "Server is vulnerable to downgrade attacks (support for TLS_FALLBACK_SCSV is missing)" ) log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_1_3_EARLY_DATA: # https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/ if scan_results.result.supports_early_data: message = _( "TLS 1.3 Early Data (0RTT) is vulnerable to replay attacks" ) log_orange(message) results.append((MEDIUM_LEVEL, message)) elif scan_command == ScanCommand.OPENSSL_CCS_INJECTION: if scan_results.result.is_vulnerable_to_ccs_injection: message = _( "Server is vulnerable to OpenSSL CCS (CVE-2014-0224)") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.SESSION_RENEGOTIATION: if scan_results.result.is_vulnerable_to_client_renegotiation_dos: message = _( "Server honors client-initiated renegotiations (vulnerable to DoS attacks)" ) log_red(message) results.append((HIGH_LEVEL, message)) if not scan_results.result.supports_secure_renegotiation: message = _("Server doesn't support secure renegotiations") log_orange(message) results.append((MEDIUM_LEVEL, message)) elif scan_command == ScanCommand.HTTP_HEADERS: if scan_results.result.strict_transport_security_header is None: message = _("Strict Transport Security (HSTS) is not set") log_red(message) results.append((HIGH_LEVEL, message)) elif scan_command in good_protocols: for level, message in process_cipher_suites( scan_results.result, good_protocols[scan_command]): results.append((level, message)) if deprecated_protocols: message = _("The following protocols are deprecated and/or insecure and should be deactivated:") + \ " " + ", ".join(deprecated_protocols) log_red(message) results.append((CRITICAL_LEVEL, message)) return results
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): """This method searches XSS which could be permanently stored in the web application""" url = request.url target_req = Request(url) referer = request.referer headers = {} if referer: headers["referer"] = referer try: response = await self.crawler.async_send(target_req, headers=headers) data = response.content except RequestError: self.network_errors += 1 return # Should we look for taint codes sent with GET in the webpages? # Exploiting those may imply sending more GET requests # Search in the page source for every taint code used by mod_xss for taint in self.tried_xss: input_request = self.tried_xss[taint][0] # Such situations should not occur as it would be stupid to block POST (or GET) requests for mod_xss # and not mod_permanentxss, but it is possible so let's filter that. if not self.do_get and input_request.method == "GET": continue if not self.do_post and input_request.method == "POST": continue if taint.lower() in data.lower(): # Code found in the webpage ! # Did mod_xss saw this as a reflected XSS ? if taint in self.successful_xss: # Yes, it means XSS payloads were injected, not just tainted code. payload, flags = self.successful_xss[taint] if check_payload(self.DATA_DIR, self.PAYLOADS_FILE, self.external_endpoint, self.proto_endpoint, response, flags, taint): # If we can find the payload again, this is in fact a stored XSS get_params = input_request.get_params post_params = input_request.post_params file_params = input_request.file_params referer = input_request.referer # The following trick may seems dirty but it allows to treat GET and POST requests # the same way. for params_list in [ get_params, post_params, file_params ]: for i, __ in enumerate(params_list): parameter, value = params_list[i] parameter = quote(parameter) if value != taint: continue if params_list is file_params: params_list[i][1][0] = payload else: params_list[i][1] = payload # we found the xss payload again -> stored xss vuln evil_request = Request( input_request.path, method=input_request.method, get_params=get_params, post_params=post_params, file_params=file_params, referer=referer) if request.path == input_request.path: description = _( "Permanent XSS vulnerability found via injection in the parameter {0}" ).format(parameter) else: description = _( "Permanent XSS vulnerability found in {0} by injecting" " the parameter {1} of {2}").format( request.url, parameter, input_request.path) if has_strong_csp(response): description += ".\n" + _( "Warning: Content-Security-Policy is present!" ) await self.add_vuln_high( request_id=request.path_id, category=NAME, request=evil_request, parameter=parameter, info=description, 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, request.path, parameter) 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("---") # FIX: search for the next code in the webpage # Ok the content is stored, but will we be able to inject javascript? else: parameter = self.tried_xss[taint][1] payloads = generate_payloads(response.content, taint, self.PAYLOADS_FILE, self.external_endpoint) flags = self.tried_xss[taint][2] # 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, input_request, parameter, taint, request)
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 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 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 finish(self): endpoint_url = f"{self.internal_endpoint}get_xxe.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)) return data = response.json if not isinstance(data, dict): return for request_id in data: original_request = await 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 f"{payload_name}.dtd" 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( f"{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)) await self.add_vuln_high( request_id=original_request.path_id, category=NAME, request=mutated_request, info=vuln_message, parameter=parameter, wstg=WSTG_CODE) log_red("---") log_red(vuln_message) log_red(Messages.MSG_EVIL_REQUEST) log_red(mutated_request.http_repr()) log_red("---")