From f2cad8841570e212d748d88c9b74ac7d1ec027d3 Mon Sep 17 00:00:00 2001 From: ekultek Date: Sun, 12 Nov 2017 19:13:00 -0600 Subject: [PATCH] initial push for issue #142, created a few WAF scripts to detect, will also save the fingerprint of the WAF script if the protection is declared to be generic --- etc/checksum/md5sum.md5 | 13 +++- lib/attacks/xss_scan/__init__.py | 3 +- lib/core/settings.py | 22 +++++- lib/firewall/__init__.py | 0 lib/firewall/anquanbao.py | 19 +++++ lib/firewall/cloudflare.py | 20 +++++ lib/firewall/generic.py | 21 ++++++ lib/firewall/modsecurity.py | 16 ++++ lib/firewall/paloalto.py | 16 ++++ lib/firewall/sucuri.py | 16 ++++ lib/firewall/webseal.py | 14 ++++ lib/header_check/__init__.py | 123 +++++++++++++++++++++++++++++-- 12 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 lib/firewall/__init__.py create mode 100644 lib/firewall/anquanbao.py create mode 100644 lib/firewall/cloudflare.py create mode 100644 lib/firewall/generic.py create mode 100644 lib/firewall/modsecurity.py create mode 100644 lib/firewall/paloalto.py create mode 100644 lib/firewall/sucuri.py create mode 100644 lib/firewall/webseal.py diff --git a/etc/checksum/md5sum.md5 b/etc/checksum/md5sum.md5 index ccf4ea0..f0003f0 100644 --- a/etc/checksum/md5sum.md5 +++ b/etc/checksum/md5sum.md5 @@ -21,6 +21,13 @@ be18faeea6e7db9db6990d8667e2298f ./bin/drivers/geckodriver-v0.17.0-linux64.tar. ca6935a72fd0527d15a78a17a35e56e8 ./bin/drivers/geckodriver-v0.19.0-linux64.tar.gz 4ccb56fb3700005c9f9188f84152f21a ./bin/drivers/geckodriver-v0.18.0-linux64.tar.gz 07cd383c8aef8ea5ef194a506141afd6 ./bin/drivers/geckodriver-v0.19.0-linux32.tar.gz +6ea65a0160c21e144e92334acc2e3667 ./lib/firewall/anquanbao.py +34b946ab1f9aaac397ba77d5f8c132b1 ./lib/firewall/cloudflare.py +1b5a2a7161db77ba570a629d25cb4055 ./lib/firewall/modsecurity.py +6b370050b40d8c1d2221424f756c7842 ./lib/firewall/paloalto.py +60973a0c2e34108dfb32c89ad46477b6 ./lib/firewall/sucuri.py +ea6e622b8eaf83b4f717020b91530e71 ./lib/firewall/webseal.py +783973a4c6af58907f6dbfe1b274c59c ./lib/firewall/generic.py 785c28da8b681a7e23964f99118b5aab ./lib/tamper_scripts/obfuscateordinal_encode.py 10bf1bc4ef0287d31633148fab557e8a ./lib/tamper_scripts/uppercase_encode.py 5b68de0ce3a783b870921b09b5222146 ./lib/tamper_scripts/hex_encode.py @@ -45,13 +52,13 @@ aa7268a8f085734a6c577c86440f7a1b ./lib/attacks/sqlmap_scan/sqlmap_opts.py d41d8cd98f00b204e9800998ecf8427e ./lib/attacks/whois_lookup/__init__.py d8fab18b15d1546f6585fe926c27868f ./lib/attacks/whois_lookup/whois.py 3917cdce61918f3992fc682105367ce8 ./lib/attacks/admin_panel_finder/__init__.py -77a8c4b040680cc00fcfe7924b1a4b1b ./lib/attacks/xss_scan/__init__.py +b50e4b7fb9fdef374c00b7ab0ef913e9 ./lib/attacks/xss_scan/__init__.py 27358f26bda30d7356143c3ea1fa99c5 ./lib/attacks/nmap_scan/__init__.py 21faf4679cdeaa731029a48f8963d6e7 ./lib/attacks/nmap_scan/nmap_opts.py 1faa2b5dfad6eb538bbfe42942d2a9da ./lib/core/errors.py d41d8cd98f00b204e9800998ecf8427e ./lib/core/__init__.py -3dbc63c72d9e6b75630899561f333ab4 ./lib/core/settings.py -ad6622a5170e4ec74b5172f12ddcba9f ./lib/header_check/__init__.py +bb55719f0ac5853a4bef43a76378348f ./lib/core/settings.py +e25de81c51e5572d29e3e161dd35f41d ./lib/header_check/__init__.py d41d8cd98f00b204e9800998ecf8427e ./var/google_search/__init__.py 670fb7fa0c618ce9712e0f73f7752925 ./var/google_search/search.py d41d8cd98f00b204e9800998ecf8427e ./var/__init__.py diff --git a/lib/attacks/xss_scan/__init__.py b/lib/attacks/xss_scan/__init__.py index cc4773f..5f06203 100644 --- a/lib/attacks/xss_scan/__init__.py +++ b/lib/attacks/xss_scan/__init__.py @@ -1,6 +1,5 @@ import os import re -import threading import tempfile import importlib try: @@ -118,9 +117,9 @@ def main_xss(start_url, proxy=None, agent=None, **kwargs): """ main attack method to be called """ - batch = kwargs.get("batch", False) tamper = kwargs.get("tamper", None) verbose = kwargs.get("verbose", False) + batch = kwargs.get("batch", False) if tamper: lib.core.settings.logger.info(lib.core.settings.set_color( diff --git a/lib/core/settings.py b/lib/core/settings.py index ecdd920..79b6754 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -47,12 +47,15 @@ except NameError: # get the master patch ID when a patch is pushed to the program PATCH_ID = str(subprocess.check_output(["git", "rev-parse", "origin/master"]))[:6] -# clone link +# clone link CLONE = "https://github.com/ekultek/zeus-scanner.git" +# issue link +ISSUE_LINK = "https://github.com/ekultek/zeus-scanner/issues" + # current version -VERSION = "1.2.7.{}".format(PATCH_ID) +VERSION = "1.2.8".format(PATCH_ID) # colors to output depending on the version VERSION_TYPE_COLORS = {"dev": 33, "stable": 92, "other": 30} @@ -159,6 +162,9 @@ SPIDER_LOG_PATH = "{}/log/blackwidow-log".format(os.getcwd()) # cookies log path COOKIE_LOG_PATH = "{}/log/cookies".format(os.getcwd()) +# unknown firewall log path +UNKNOWN_FIREWALL_FINGERPRINT_PATH = "{}/log/unknown-firewall".format(os.getcwd()) + # the current log file being used CURRENT_LOG_FILE_PATH = "{}/log".format(os.getcwd()) @@ -180,6 +186,16 @@ URL_REGEX = re.compile(r"((https?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#! # regex to discover if there are any results on the page NO_RESULTS_REGEX = re.compile("did not match with any results.", re.IGNORECASE) +# WAF/IDS/IPS checking payload +PROTECTION_CHECK_PAYLOAD = ( + "AND 1=1 UNION ALL SELECT 1,NULL,''," + "table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC " + "xp_cmdshell('cat ../../../etc/passwd')#" +) + +# scripts to detect the WAF/IDS/IPS +DETECT_FIREWALL_PATH = "{}/lib/firewall".format(os.getcwd()) + # search engines that the application can use AUTHORIZED_SEARCH_ENGINES = { "aol": "http://aol.com", @@ -215,7 +231,7 @@ URL_EXCLUDES = ( "www.google", "mail.google", "accounts.google", "schema.org", "www.Virus.Spyware.Download.Blocked<.+.>") + ) + for detection in detection_schemas: + if detection.search(content) is not None: + return True + diff --git a/lib/firewall/sucuri.py b/lib/firewall/sucuri.py new file mode 100644 index 0000000..4bea066 --- /dev/null +++ b/lib/firewall/sucuri.py @@ -0,0 +1,16 @@ +import re + + +__item__ = "Sucuri Firewall (Sucuri Cloudproxy)" + + +def detect(content, **kwargs): + content = str(content) + detection_schema = ( + re.compile(r"Access Denied - Sucuri Website Firewall"), + re.compile(r"Sucuri WebSite Firewall - CloudProxy - Access Denied"), + re.compile(r"Questions\?.+cloudproxy@sucuri\.net") + ) + for detection in detection_schema: + if detection.search(content) is not None: + return True diff --git a/lib/firewall/webseal.py b/lib/firewall/webseal.py new file mode 100644 index 0000000..20afee4 --- /dev/null +++ b/lib/firewall/webseal.py @@ -0,0 +1,14 @@ +import re + + +__item__ = "IBM Security Access Manager WebSEAL" + + +def detect(content, **kwargs): + content = str(content) + detection_schema = ( + re.compile(r"\bWebSEAL\b"), re.compile(r"\bIBM\b") + ) + for detection in list(detection_schema): + if detection.search(content) is not None: + return True diff --git a/lib/header_check/__init__.py b/lib/header_check/__init__.py index 9c0dcb8..a4205c5 100644 --- a/lib/header_check/__init__.py +++ b/lib/header_check/__init__.py @@ -1,3 +1,7 @@ +import os +import re +import importlib + import requests from xml.dom import minidom from requests.exceptions import ConnectionError @@ -9,13 +13,105 @@ from lib.core.settings import ( proxy_string_to_dict, create_random_ip, write_to_log_file, - HEADER_RESULT_PATH, replace_http, - shutdown, - COOKIE_LOG_PATH + HEADER_RESULT_PATH, + COOKIE_LOG_PATH, + PROTECTION_CHECK_PAYLOAD, + DETECT_FIREWALL_PATH, + ISSUE_LINK, + DBMS_ERRORS, + UNKNOWN_FIREWALL_FINGERPRINT_PATH ) +def detect_protection(url, **kwargs): + verbose = kwargs.get("verbose", False) + agent = kwargs.get("agent", None) + proxy = kwargs.get("proxy", None) + xforward = kwargs.get("xforward", False) + + if xforward: + ip_list = ( + create_random_ip(), + create_random_ip(), + create_random_ip() + ) + headers = { + "Connection": "close", + "user-agent": agent, + "X-Forward-From": "{}, {}, {}".format(ip_list[0], ip_list[1], ip_list[2]) + } + else: + headers = { + "Connection": "close", + "user-agent": agent + } + + url = "{} {}".format(url.strip(), PROTECTION_CHECK_PAYLOAD) + + if verbose: + logger.debug(set_color( + "attempting connection to '{}'...".format(url), level=10 + )) + try: + protection_check_req = requests.get( + url, params=headers, proxies=proxy_string_to_dict(proxy), timeout=20 + ) + + html, status, headers = protection_check_req.content, protection_check_req.status_code, protection_check_req.headers + + for dbms in DBMS_ERRORS: # make sure there are no DBMS errors in the HTML + for regex in DBMS_ERRORS[dbms]: + if re.compile(regex).search(html) is not None: + logger.info(set_color( + "it appears that the WAF/IDS/IPS check threw a DBMS error and may be vulnerable " + "to SQL injection attacks. it appears the backend DBMS is '{}'...".format(dbms), level=25 + )) + + retval = [] + if status != 200 and "not found" not in html.lower(): + file_list = [f for f in os.listdir(DETECT_FIREWALL_PATH) if not any(ex in f for ex in ["__init__", ".pyc"])] + for item in file_list: + item = item[:-3] + detection_name = "lib.firewall.{}" + detection_name = detection_name.format(item) + detection_name = importlib.import_module(detection_name) + if detection_name.detect(html, headers=headers, status=status): + retval.append(detection_name.__item__) + if len(retval) > 1: + if "Generic (Unknown)" in retval: + item = retval.index("Generic (Unknown)") + del retval[item] + else: + if retval[0] == "Generic (Unknown)": + logger.warning(set_color( + "identified WAF/IDS/IPS is unknown to Zeus, if you know the firewall and the context " + "of the firewall, please create an issue ({}), fingerprint of the firewall will be " + "written to a log file...".format(ISSUE_LINK), level=30 + )) + full_finger_print = "HTTP/1.1 {}\n{}\n{}".format(status, headers, html) + write_to_log_file(full_finger_print, UNKNOWN_FIREWALL_FINGERPRINT_PATH, "{}-fingerprint.html".format( + replace_http(url) + )) + else: + retval = None + + return ''.join(retval) if isinstance(retval, list) else retval + + except Exception as e: + if "Read timed out." or "Connection reset by peer" in str(e): + logger.warning(set_color( + "detection request timed out, assuming no protection and continuing...", level=30 + )) + return None + else: + logger.exception(set_color( + "Zeus ran into an unexpected error '{}'...".format(e), level=50 + )) + request_issue_creation() + return None + + def load_xml_data(path, start_node="header", search_node="name"): """ load the XML data @@ -52,7 +148,7 @@ def load_headers(url, **kwargs): ip_list[0], ip_list[1], ip_list[2] ) } - req = requests.get(url, params=header_value, proxies=proxy) + req = requests.get(url, params=header_value, proxies=proxy, timeout=10) if len(req.cookies) > 0: logger.info(set_color( "found a request cookie, saving to file...", level=25 @@ -91,6 +187,7 @@ def main_header_check(url, **kwargs): agent = kwargs.get("agent", None) proxy = kwargs.get("proxy", None) xforward = kwargs.get("xforward", False) + identify = kwargs.get("identify", True) protection = {"hostname": url} definition = { @@ -100,6 +197,21 @@ def main_header_check(url, **kwargs): "x-content": ("protection against MIME type attacks", "MIME"), "content-security": ("protection against multiple attacks", "ALL") } + + if identify: + logger.info(set_color( + "checking if target URL is protected by some kind of WAF/IPS/IDS..." + )) + identified = detect_protection(url, proxy=proxy, agent=agent, verbose=verbose, xforward=xforward) + if identified is None: + logger.info(set_color( + "no WAF/IDS/IPS has been identified on target URL...", level=25 + )) + else: + logger.warning(set_color( + "the target URL WAF/IDS/IPS has been identified as '{}'...".format(identified), level=30 + )) + if verbose: logger.debug(set_color( "loading XML data...", level=10 @@ -111,14 +223,13 @@ def main_header_check(url, **kwargs): try: found_headers = load_headers(url, proxy=proxy, agent=agent, xforward=xforward) except (ConnectionError, Exception) as e: - if "Max retries exceeded with url:" in str(e): + if "Read timed out." or "Connection reset by peer" in str(e): found_headers = None else: logger.exception(set_color( "Zeus has hit an unexpected error and cannot continue '{}'...".format(e), level=50 )) request_issue_creation() - shutdown() if found_headers is not None: if verbose: