def _check_charset(url: str, res: Response, raw: str) -> List[Result]: results: List[Result] = [] # if the body is empty, we really don't care about this if len(res.content) == 0: return results if "Content-Type" in res.headers: content_type = str(res.headers["Content-Type"]).lower() if "charset" not in content_type and "text/html" in content_type: # not charset specified results.append( Result( f"Charset Not Defined in '{res.headers['Content-Type']}' at {url}", Vulnerabilities.HTTP_HEADER_CONTENT_TYPE_NO_CHARSET, url, [res.headers["Content-Type"], raw], )) else: # content-type missing results.append( Result( f"Content-Type Missing: {url} ({res.request.method} - {res.status_code})", Vulnerabilities.HTTP_HEADER_CONTENT_TYPE_MISSING, url, raw, )) return results
def _check_charset(url: str, res: Response) -> List[Result]: results: List[Result] = [] # if the body is empty, we really don't care about this if len(res.content) == 0: return results try: if "Content-Type" in res.headers: content_type = str(res.headers["Content-Type"]).lower() if "charset" not in content_type and "text/html" in content_type: # not charset specified results.append( Result.from_evidence( Evidence.from_response(res, {"content-type": content_type}), f"Charset Not Defined in '{res.headers['Content-Type']}' at {url}", Vulnerabilities.HTTP_HEADER_CONTENT_TYPE_NO_CHARSET, )) else: # content-type missing results.append( Result.from_evidence( Evidence.from_response(res), f"Content-Type Missing: {url} ({res.request.method} - {res.status_code})", Vulnerabilities.HTTP_HEADER_CONTENT_TYPE_MISSING, )) except Exception: output.debug_exception() return results
def check_json_user_enum(url: str) -> List[Result]: results = [] target = urljoin(url, "wp-json/wp/v2/users") res = network.http_get(target, False) body = res.text if res.status_code < 300 and "slug" in body: data = res.json() # log the enum finding results.append( Result.from_evidence( Evidence.from_response(res), f"WordPress WP-JSON User Enumeration at {target}", Vulnerabilities.APP_WORDPRESS_USER_ENUM_API, )) # log the individual users for user in data: results.append( Result.from_evidence( Evidence.from_response( res, { "user_id": user["id"], "user_slug": user["slug"], "user_name": user["name"], }, ), f"ID: {user['id']}\tUser Slug: '{user['slug']}'\t\tUser Name: '{user['name']}'", Vulnerabilities.APP_WORDPRESS_USER_FOUND, )) return results
def check_options(url: str) -> List[Result]: results: List[Result] = [] res = network.http_options(url) if "Allow" in res.headers: results.append( Result.from_evidence( Evidence.from_response(res), f"Allow HTTP Verbs (OPTIONS): {res.headers['Allow']}", Vln.HTTP_OPTIONS_ALLOW, ) ) if "Public" in res.headers: results.append( Result.from_evidence( Evidence.from_response(res), f"Public HTTP Verbs (OPTIONS): {res.headers['Public']}", Vln.HTTP_OPTIONS_PUBLIC, ) ) results += response_scanner.check_response(url, res) return results
def check_version(banner: str, raw: str, url: str) -> List[Result]: results = [] if not banner.startswith("PHP/"): return [] # we've got a PHP version results.append( Result( f"PHP Version Exposed: {banner}", Vulnerabilities.HTTP_PHP_VERSION_EXPOSED, url, raw, ) ) # parse the version, and get the latest version - see if the server is up to date ver = cast(version.Version, version.parse(banner.split("/")[1])) curr_version = version_checker.get_latest_version("php", ver) if curr_version is not None and curr_version > ver: results.append( Result( f"PHP Outdated: {ver} - Current: {curr_version}", Vulnerabilities.SERVER_PHP_OUTDATED, url, raw, ) ) return results
def check_options(url: str) -> List[Result]: results: List[Result] = [] res = network.http_options(url) if "Allow" in res.headers: results.append( Result( f"Allow HTTP Verbs (OPTIONS): {res.headers['Allow']}", Vulnerabilities.HTTP_OPTIONS_ALLOW, url, [ network.http_build_raw_request(res.request), network.http_build_raw_response(res), ], ) ) if "Public" in res.headers: results.append( Result( f"Public HTTP Verbs (OPTIONS): {res.headers['Allow']}", Vulnerabilities.HTTP_OPTIONS_PUBLIC, url, [ network.http_build_raw_request(res.request), network.http_build_raw_response(res), ], ) ) results += response_scanner.check_response(url, res) return results
def identify(url: str) -> Tuple[Union[str, None], List[Result]]: results = [] # find WordPress res, path = _identify_by_path(url, "") if path is None: res, path = _identify_by_path(url, "blog/") # check to see if we have a valid hit if path is not None: # we have a WordPress install, let's see if we can get a version body = res.text ver = "Unknown" # this works for modern versions m = re.search(r"login.min.css\?ver=\d+\.\d+\.?\d*", body) if m: ver = m.group(0).split("=")[1] else: # the current method doesn't work, fall back to an older method m = re.search(r"load-styles.php\?[\w,;=&%]+;ver=\d+\.\d+\.?\d*", body) if m: ver = m.group(0).split("=")[-1] # report that we found WordPress results.append( Result.from_evidence( Evidence.from_response(res, {"version": ver}), f"Found WordPress v{ver} at {path}", Vulnerabilities.APP_WORDPRESS_VERSION, ) ) # is this a current version? ver = cast(version.Version, version.parse(ver)) curr_version = version_checker.get_latest_version("wordpress", ver) if curr_version is not None and curr_version > ver: results.append( Result.from_evidence( Evidence.from_response( res, { "installed_version": str(ver), "current_verison": str(curr_version), }, ), f"WordPress Outdated: {ver} - Current: {curr_version}", Vulnerabilities.APP_WORDPRESS_OUTDATED, ) ) return path, results else: return None, []
def get_waf(headers: Dict, raw: str, url: str) -> List[Result]: results = [] if "Server" in headers: if headers["Server"] == "cloudflare": results.append( Result("WAF Detected: Cloudflare", Vulnerabilities.WAF_CLOUDFLARE, url, raw)) if "X-CDN" in headers or "X-Iinfo" in headers: results.append( Result("WAF Detected: Incapsula", Vulnerabilities.WAF_INCAPSULA, url, raw)) return results
def check_banner(banner: str, raw: str, url: str) -> List[Result]: if not banner.startswith("nginx"): return [] results: List[Result] = [] if "/" in banner: # we've got a Nginx version results.append( Result( f"Nginx Version Exposed: {banner}", Vulnerabilities.HTTP_BANNER_NGINX_VERSION, url, { "response": raw, "banner": banner }, )) # parse the version, and get the latest version - see if the server is up to date ver = cast(version.Version, version.parse(banner.split("/")[1])) curr_version = version_checker.get_latest_version("nginx", ver) if curr_version is not None and curr_version > ver: results.append( Result( f"Nginx Outdated: {ver} - Current: {curr_version}", Vulnerabilities.SERVER_NGINX_OUTDATED, url, { "response": raw, "banner": banner }, )) else: # this means that it's just a generic banner, with no info results.append( Result( "Generic Nginx Server Banner Found", Vulnerabilities.HTTP_BANNER_GENERIC_NGINX, url, { "response": raw, "banner": banner }, )) return results
def check_asp_net_debug(url: str) -> List[Result]: results: List[Result] = [] res = network.http_custom( "DEBUG", url, additional_headers={"Command": "stop-debug", "Accept": "*/*"} ) if res.status_code == 200 and "OK" in res.text: # we've got a hit, but could be a false positive # try this again, with a different verb xres = network.http_custom( "XDEBUG", url, additional_headers={"Command": "stop-debug", "Accept": "*/*"} ) # if we get a 200 when using an invalid verb, it's a false positive # if we get something else, then the DEBUG actually did something if xres.status_code != 200: results.append( Result( "ASP.NET Debugging Enabled", Vulnerabilities.SERVER_ASPNET_DEBUG_ENABLED, url, [ network.http_build_raw_request(res.request), network.http_build_raw_response(res), ], ) ) else: output.debug("Server responds to invalid HTTP verbs with status 200") results += response_scanner.check_response(url, res) return results
def check_aspnet_handlers(url: str) -> List[Result]: results = [] file_name = secrets.token_hex(12) exts = ["ashx", "aspx", "asmx", "soap", "rem"] for ext in exts: target = urljoin(url, f"{file_name}.{ext}") vuln = False res = network.http_get(target, False) body = res.text if "Location" in res.headers and "aspxerrorpath" in res.headers["Location"]: vuln = True elif ( res.status_code >= 400 and "Remoting.RemotingException" in body or "HttpException" in body or "FileNotFoundException" in body ): vuln = True if vuln: results.append( Result.from_evidence( Evidence.from_response(res, {"handler": ext}), f"ASP.NET Handler Enumeration: {ext}", Vulnerabilities.SERVER_ASPNET_HANDLER_ENUM, ) ) return results
def check_path_disclosure(wp_url: str) -> List[Result]: # this is a list of files that are known to throw a fatal error when accessed directly # this is from a manual review of all plugins with at least 1M installs urls = [ "wp-content/plugins/hello.php", "wp-content/plugins/akismet/akismet.php", "wp-content/plugins/contact-form-7/includes/capabilities.php", "wp-content/plugins/wordpress-seo/admin/views/partial-alerts-errors.php", "wp-content/plugins/jetpack/load-jetpack.php", "wp-content/plugins/jetpack/uninstall.php", "wp-content/plugins/duplicate-post/duplicate-post-admin.php", "wp-content/plugins/wpforms-lite/includes/admin/class-welcome.php", "wp-content/plugins/wp-google-maps/base/includes/welcome.php", "wp-content/plugins/wp-super-cache/wp-cache.php", "wp-content/plugins/mailchimp-for-wp/integrations/wpforms/bootstrap.php", "wp-content/plugins/mailchimp-for-wp/integrations/bootstrap.php", "wp-content/plugins/regenerate-thumbnails/regenerate-thumbnails.php", "wp-content/plugins/advanced-custom-fields/includes/deprecated.php", "wp-content/plugins/redirection/redirection.php", "wp-content/plugins/wpforms-lite/includes/admin/importers/class-ninja-forms.php", "wp-content/plugins/ninja-forms/includes/deprecated.php", "wp-content/plugins/so-widgets-bundle/so-widgets-bundle.php", "wp-content/plugins/wp-fastest-cache/templates/preload.php", "wp-content/plugins/duplicate-page/duplicatepage.php", "wp-content/plugins/better-wp-security/better-wp-security.php", "wp-content/plugins/all-in-one-wp-security-and-firewall/other-includes/wp-security-unlock-request.php", "wp-content/plugins/related-posts/views/settings.php", "wp-content/plugins/wpcontentguard/views/settings.php", "wp-content/plugins/simple-social-icons/simple-social-icons.php", ] results: List[Result] = [] for url in urls: target = urljoin(wp_url, url) head = network.http_head(target, False) if head.status_code != 404: resp = network.http_get(target, False) if resp.status_code < 300 or resp.status_code >= 500: # we have some kind of response that could be useful if "<b>Fatal error</b>:" in resp.text: # we have an error pattern = r"<b>((\/|[A-Z]:\\).*.php)<\/b>" if re.search(pattern, resp.text): try: path = re.findall(pattern, resp.text)[0][0] results.append( Result.from_evidence( Evidence.from_response( resp, {"path": path}), f"WordPress File Path Disclosure: {target} ({path})", Vulnerabilities. APP_WORDPRESS_PATH_DISCLOSURE, )) except Exception: output.debug_exception() results += response_scanner.check_response(target, resp) return results
def check_cve_2019_5418(url: str) -> List[Result]: global _checked # this only applies to controllers, so skip the check unless the link ends with '/' if not url.endswith("/") or url in _checked: return [] results: List[Result] = [] _checked.append(url) try: res = network.http_get( url, False, {"Accept": "../../../../../../../../../e*c/p*sswd{{"} ) body = res.text req = network.http_build_raw_request(res.request) results += response_scanner.check_response(url, res) pattern = r"root:[a-zA-Z0-9]+:0:0:.+$" mtch = re.search(pattern, body) if mtch: results.append( Result( f"Rails CVE-2019-5418: File Content Disclosure: {url} - {mtch.group(0)}", Vulnerabilities.SERVER_RAILS_CVE_2019_5418, url, [body, req], ) ) except Exception: output.debug_exception() return results
def check_cve_2019_0232(links: List[str]) -> List[Result]: results: List[Result] = [] targets: List[str] = [] for link in links: if "cgi-bin" in link: if "?" in link: targets.append(f"{link}&dir") else: targets.append(f"{link}?dir") for target in targets: res = network.http_get(target, False) body = res.text if "<DIR>" in body: # we have a hit results.append( Result( f"Apache Tomcat RCE (CVE-2019-0232): {target}", Vulnerabilities.SERVER_TOMCAT_CVE_2019_0232, target, [ network.http_build_raw_request(res.request), network.http_build_raw_response(res), ], )) results += response_scanner.check_response(target, res) return results
def check_status(url: str) -> List[Result]: results: List[Result] = [] search = ["status/", "stats/"] for path in search: target = urljoin(url, path) res = network.http_get(target, False) body = res.text if res.status_code == 200 and "Active connections:" in body: results.append( Result( f"Nginx status page found: {target}", Vulnerabilities.SERVER_NGINX_STATUS_EXPOSED, target, [ network.http_build_raw_request(res.request), network.http_build_raw_response(res), ], )) results += response_scanner.check_response(target, res) return results
def get_version(url: str, res: Response, method: Optional[str] = None) -> List[Result]: """Check a server response to see if it contains a Tomcat version. :param method: :param url: :param res: :return: """ results: List[Result] = [] body = res.text ver = _get_version_from_body(body, res.status_code) if ver is not None: msg = f"Apache Tomcat version exposed: {ver}" if method is not None: msg += f" (Via {method})" results.append( Result( msg, Vulnerabilities.SERVER_TOMCAT_VERSION, url, [ ver, network.http_build_raw_request(res.request), network.http_build_raw_response(res), ], )) results += _check_version_outdated(ver, url, body) return results
def _is_port_open(url: str, ip: str, rec, queue): sock = socket.socket() port = rec["port"] # set a timeout - this has a huge speed impact sock.settimeout(0.75) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) connected = False try: connected = sock.connect_ex((ip, port)) is 0 sock.close() except Exception: # this shouldn't happen, but just in case output.debug_exception() if connected: queue.put( Result( f"Open Port: IP: {ip} - Port: {port} (Assigned Service: {rec['name']} - {rec['desc']})", Vulnerabilities.NETWORK_OPEN_PORT, url, { "ip": ip, "port": port }, ))
def _process(url: str, res: Response): nonlocal results if res.status_code == 200 and '<h1 class="p">PHP Version' in res.text: results.append( Result.from_evidence( Evidence.from_response(res), f"PHP Info Found: {url}", Vulnerabilities.SERVER_PHP_PHPINFO, ))
def _process(url: str, res: Response): nonlocal results if res.status_code == 200 and res.content.startswith( b"\0\0\0\1Bud1\0"): results.append( Result.from_evidence( Evidence.from_response(res), f".DS_Store File Found: {url}", Vulnerabilities.HTTP_DS_STORE_FILE, ))
def check_for_jira(session: Session) -> Tuple[List[Result], Union[str, None]]: # this checks for an instance of Jira relative to the session URL results: List[Result] = [] jira_url = None try: targets = [ f"{session.url}secure/Dashboard.jspa", f"{session.url}jira/secure/Dashboard.jspa", ] for target in targets: res = network.http_get(target, False) if ( res.status_code == 200 and 'name="application-name" content="JIRA"' in res.text ): # we have a Jira instance jira_url = target # try to get the version ver_str = "unknown" try: ver_pattern = ( r"<meta name=\"ajs-version-number\" content=\"([\d\.]+)\">" ) version = re.search(ver_pattern, res.text).group(1) build_pattern = ( r"<meta name=\"ajs-build-number\" content=\"(\d+)\">" ) build = re.search(build_pattern, res.text).group(1) ver_str = f"v{version}-{build}" except: output.debug_exception() results.append( Result.from_evidence( Evidence.from_response(res), f"Jira Installation Found ({ver_str}): {target}", Vulnerabilities.APP_JIRA_FOUND, ) ) results += response_scanner.check_response(target, res) break except Exception: output.debug_exception() return results, jira_url
def check_cve_2019_11043(session: Session, links: List[str]) -> List[Result]: min_qsl = 1500 max_qsl = 1950 qsl_step = 5 results = [] targets = [] if session.args.php_page is not None and len(session.args.php_page) > 0: php_page = str(session.args.php_page) if php_page.startswith("http://") or php_page.startswith("https://"): targets.append(urljoin(session.url, php_page)) elif php_page.startswith(session.url): targets.append(php_page) for link in links: if link.endswith(".php"): targets.append(link) elif link.endswith("/"): targets.append(f"{link}index.php") def _get_resp(url: str, q_count: int) -> Response: path_info = "/PHP\nindex.php" u = urlparse(url) orig_path = quote(u.path) new_path = quote(u.path + path_info) delta = len(new_path) - len(path_info) - len(orig_path) prime = q_count - delta / 2 req_url = urljoin(url, new_path + "?" + "Q" * int(prime)) return network.http_get(req_url, False) for target in targets: # start by making sure that we have a valid target if network.http_head(target, False).status_code < 400: # get our baseline status code res = _get_resp(target, 1500) base_status_code = res.status_code for qsl in range(min_qsl + qsl_step, max_qsl, qsl_step): res = _get_resp(target, qsl) if res.status_code != base_status_code: results.append( Result.from_evidence( Evidence.from_response(res, {"qsl": qsl}), f"Detected susceptibility to PHP Remote Code Execution (CVE-2019-11043) (QSL {qsl})", Vulnerabilities.SERVER_PHP_CVE_2019_11043, ) ) break return results
def _process(url: str, result: Tuple[bool, Response]): nonlocal results found, res = result if found and '<h1 class="p">PHP Version' in res.text: results.append( Result.from_evidence( Evidence.from_response(res), f"PHP Info Found: {url}", Vulnerabilities.SERVER_PHP_PHPINFO, ) )
def check_response(url: str, res: Response, body: Union[str, None] = None) -> List[Result]: global _data, _reports results = [] try: # make sure we actually have something if res is None: return [] if _data is None or len(_data) == 0: _get_data() if body is None: body = res.text for rule in _data: rule = cast(_MatchRule, rule) mtch = re.search(rule.pattern, body) if mtch: val = mtch.group(int(rule.match_group)) err_start = body.find(val) # get the error, plus 25 characters on each side err = body[err_start - 25:err_start + len(val) + 25] msg = (f"Found error message (confidence: {rule.confidence}) " f"on {url} ({res.request.method}): ...{err}...") if msg not in _reports: results.append( Result.from_evidence( Evidence.from_response(res), msg, Vulnerabilities.HTTP_ERROR_MESSAGE, )) _reports.append(msg) break else: output.debug(f"Ignored duplicate error message: {msg}") except Exception: output.debug_exception() return results
def _process(url: str, res: Response): nonlocal results, new_links if res.status_code == 200: # we found something! new_links.append(url) results.append( Result.from_evidence( Evidence.from_response(res), f"Found backup file: {url}", Vulnerabilities.HTTP_BACKUP_FILE, )) results += response_scanner.check_response(target, res)
def check_banner(banner: str, raw: str, url: str) -> List[Result]: if not banner.startswith("Python/"): return [] results = [] # we've got a version results.append( Result( f"Python Version Exposed: {banner}", Vulnerabilities.HTTP_BANNER_PYTHON_VERSION, url, raw, )) return results
def check_banner(banner: str, raw: str, url: str) -> List[Result]: if not banner.startswith("Python/"): return [] results = [ Result( f"Python Version Exposed: {banner}", Vulnerabilities.HTTP_BANNER_PYTHON_VERSION, url, { "response": raw, "banner": banner }, ) ] return results
def get_results(soup: BeautifulSoup, url: str, res: Response) -> List[Result]: global _reports results: List[Result] = [] try: parsed = urlparse(url) domain = utils.get_domain(parsed.netloc) issues, r = _get_retirejs_results(soup, url, domain, res) results += r for js_url, issue in issues: comp = issue["component"] ver = issue["version"] if "vulnerabilities" in issue: for vuln in issue["vulnerabilities"]: info = ( f'Vulnerable JavaScript: {comp}-{ver} ({js_url}): Severity: {vuln["severity"]} - ' f'Info: {" ".join(vuln["info"])}' ) # make sure we haven't reported this issue before if info not in _reports: _reports.append(info) results.append( Result.from_evidence( Evidence.from_response( res, { "js_file": js_url, "js_lib": comp, "js_lib_ver": ver, "vuln_info": list(vuln["info"]), "vuln_sev": vuln["severity"], }, ), info, Vulnerabilities.JS_VULNERABLE_VERSION, ) ) except Exception: output.debug_exception() return results
def _check_version_outdated(ver: str, url: str, body: str) -> List[Result]: results: List[Result] = [] # parse the version, and get the latest version - see if the server is up to date ver = cast(version.Version, version.parse(ver)) curr_version = version_checker.get_latest_version("apache_tomcat", ver) if curr_version is not None and curr_version > ver: results.append( Result( f"Apache Tomcat Outdated: {ver} - Current: {curr_version}", Vulnerabilities.SERVER_TOMCAT_OUTDATED, url, [ver, curr_version, body], )) return results
def check_trace(url: str) -> List[Result]: results: List[Result] = [] res = network.http_custom("TRACE", url) body = res.text if res.status_code == 200 and "TRACE / HTTP/1.1" in body: results.append( Result.from_evidence( Evidence.from_response(res), "HTTP TRACE Enabled", Vln.HTTP_TRACE_ENABLED, )) results += response_scanner.check_response(url, res) return results
def _get_result(client, prt): req, resp = _send_http_10_get(client) ip = _get_ip(resp) if ip is not None: results.append( Result( f"Private IP Found: {ip} via HTTP 1.0 Redirect", Vln.SERVER_INT_IP_EXP_HTTP10, session.url, { "request": req, "response": _resp_to_str(resp), "ip": {ip}, "port": prt, }, ))