diff --git a/.github/demo.mp4 b/.github/demo.mp4 new file mode 100644 index 0000000..4799ca7 Binary files /dev/null and b/.github/demo.mp4 differ diff --git a/apachetomcatscanner/Config.py b/apachetomcatscanner/Config.py index a30a6f1..77f3cc9 100644 --- a/apachetomcatscanner/Config.py +++ b/apachetomcatscanner/Config.py @@ -15,7 +15,7 @@ class Config(object): request_timeout = 1 request_proxies = {} - request_no_check_certificate = False + request_no_check_certificate = True request_available_schemes = ["http"] diff --git a/apachetomcatscanner/Reporter.py b/apachetomcatscanner/Reporter.py index ce7dc18..7d958f2 100644 --- a/apachetomcatscanner/Reporter.py +++ b/apachetomcatscanner/Reporter.py @@ -17,22 +17,70 @@ class Reporter(object): data = {} - def __init__(self, config): + def __init__(self, config, vulns_db): super(Reporter, self).__init__() self.config = config + self.vulns_db = vulns_db + self._new_results = [] - def report_result(self, computer_ip, computer_port, tomcat_version, manager_accessible, default_credentials, cves): + def report_result(self, computer_ip, computer_port, tomcat_version, manager_accessible, credentials_found): computer_port = str(computer_port) + + finding = {} + finding["computer_ip"] = computer_ip + finding["computer_port"] = computer_port + finding["tomcat_version"] = tomcat_version + finding["manager_accessible"] = manager_accessible + finding["credentials_found"] = credentials_found + if computer_ip not in self.data.keys(): self.data[computer_ip] = {} if str(computer_port) not in self.data[computer_ip].keys(): self.data[computer_ip][computer_port] = {} - self.data[computer_ip][computer_port]["computer_ip"] = computer_ip - self.data[computer_ip][computer_port]["computer_port"] = computer_port - self.data[computer_ip][computer_port]["tomcat_version"] = tomcat_version - self.data[computer_ip][computer_port]["manager_accessible"] = manager_accessible - self.data[computer_ip][computer_port]["default_credentials"] = default_credentials - self.data[computer_ip][computer_port]["cves"] = cves + self.data[computer_ip][computer_port] = finding + self._new_results.append(finding) + + def print_new_results(self): + try: + for finding in self._new_results: + + # List of cves + cve_str = "" + if self.config.list_cves_mode == True: + cve_list = self.vulns_db.get_vulnerabilities_of_version_sorted_by_criticity(finding["tomcat_version"], colors=True, reverse=True) + if len(cve_list) != 0: + cve_str = "CVEs: %s" % ', '.join(cve_list) + + # credentials_str = "username:%s\npassword:%s" % (credentials_found[0][1]["username"], credentials_found[0][1]["password"]) + + if finding["manager_accessible"]: + print("[>] [Apache Tomcat/\x1b[1;95m%s\x1b[0m] on \x1b[1;93m%s\x1b[0m:\x1b[1;93m%s\x1b[0m (manager:\x1b[1;92maccessible\x1b[0m) %s\x1b[0m " % ( + finding["tomcat_version"], + finding["computer_ip"], + finding["computer_port"], + cve_str + ) + ) + if len(finding["credentials_found"]) != 0: + for statuscode, creds in finding["credentials_found"]: + if len(creds["description"]) != 0: + print(" | Valid user: \x1b[1;92m%s\x1b[0m | password:\x1b[1;92m%s\x1b[0m | \x1b[94m%s\x1b[0m" % (creds["username"], creds["password"], creds["description"])) + else: + print(" | Valid user: \x1b[1;92m%s\x1b[0m | password:\x1b[1;92m%s\x1b[0m" % (creds["username"], creds["password"])) + + else: + print("[>] [Apache Tomcat/\x1b[1;95m%s\x1b[0m] on \x1b[1;93m%s\x1b[0m:\x1b[1;93m%s\x1b[0m (manager:\x1b[1;91mnot accessible\x1b[0m) %s\x1b[0m " % ( + finding["tomcat_version"], + finding["computer_ip"], + finding["computer_port"], + cve_str + ) + ) + + self._new_results.remove(finding) + except Exception as e: + if self.config.debug_mode: + print("[Error in %s] %s" % (__name__, e)) def export_xlsx(self, path_to_file): basepath = os.path.dirname(path_to_file) @@ -58,13 +106,16 @@ def export_xlsx(self, path_to_file): for computername in self.data.keys(): computer = self.data[computername] for port in computer.keys(): + cve_list = self.vulns_db.get_vulnerabilities_of_version_sorted_by_criticity(computer[port]["tomcat_version"], colors=False, reverse=True) + cve_str = ', '.join([cve["cve"]["id"] for cve in cve_list]) + data = [ computer[port]["computer_ip"], computer[port]["computer_port"], computer[port]["tomcat_version"], str(computer[port]["manager_accessible"]).upper(), computer[port]["default_credentials"], - computer[port]["cves"] + cve_str ] worksheet.write_row(row_id, 0, data) row_id += 1 @@ -100,13 +151,16 @@ def export_sqlite(self, path_to_file): for computername in self.data.keys(): computer = self.data[computername] for port in computer.keys(): + cve_list = self.vulns_db.get_vulnerabilities_of_version_sorted_by_criticity(computer[port]["tomcat_version"], colors=False, reverse=True) + cve_str = ', '.join([cve["cve"]["id"] for cve in cve_list]) + cursor.execute("INSERT INTO results VALUES (?, ?, ?, ?, ?, ?)", ( computer[port]["computer_ip"], computer[port]["computer_port"], computer[port]["tomcat_version"], str(computer[port]["manager_accessible"]).upper(), computer[port]["default_credentials"], - computer[port]["cves"] + cve_str ) ) conn.commit() diff --git a/apachetomcatscanner/__main__.py b/apachetomcatscanner/__main__.py index a4c98f1..c7831ae 100755 --- a/apachetomcatscanner/__main__.py +++ b/apachetomcatscanner/__main__.py @@ -3,6 +3,7 @@ # File name : __main__.py # Author : Podalirius (@podalirius_) # Date created : 24 Jul 2022 +import threading import argparse import os @@ -11,14 +12,14 @@ from apachetomcatscanner.Reporter import Reporter from apachetomcatscanner.Config import Config from apachetomcatscanner.VulnerabilitiesDB import VulnerabilitiesDB -from apachetomcatscanner.utils.scan import scan_worker +from apachetomcatscanner.utils.scan import scan_worker, monitor_thread from sectools.windows.ldap import get_computers_from_domain, get_servers_from_domain, get_subnets from sectools.network.domains import is_fqdn from sectools.network.ip import is_ipv4_cidr, is_ipv4_addr, is_ipv6_addr, expand_cidr, expand_port_range from concurrent.futures import ThreadPoolExecutor -VERSION = "2.3.5" +VERSION = "3.0" banner = """Apache Tomcat Scanner v%s - by @podalirius_\n""" % VERSION @@ -90,8 +91,6 @@ def load_targets(options, config): # Sort uniq on targets list targets = sorted(list(set(targets))) - print(targets) - final_targets = [] # Parsing target to filter IP/DNS/CIDR for target in targets: @@ -128,12 +127,12 @@ def parseArgs(): parser.add_argument("-v", "--verbose", default=False, action="store_true", help="Verbose mode. (default: False)") parser.add_argument("--debug", default=False, action="store_true", help="Debug mode, for huge verbosity. (default: False)") parser.add_argument("-C", "--list-cves", default=False, action="store_true", help="List CVE ids affecting each version found. (default: False)") - parser.add_argument("-T", "--threads", default=8, type=int, help="Number of threads (default: 5)") + parser.add_argument("-T", "--threads", default=250, type=int, help="Number of threads (default: 250)") parser.add_argument("-s", "--servers-only", default=False, action="store_true", help="If querying ActiveDirectory, only get servers and not all computer objects. (default: False)") parser.add_argument("--only-http", default=False, action="store_true", help="Scan only with HTTP scheme. (default: False, scanning with both HTTP and HTTPs)") parser.add_argument("--only-https", default=False, action="store_true", help="Scan only with HTTPs scheme. (default: False, scanning with both HTTP and HTTPs)") - parser.add_argument("--no-check-certificate", default=False, action="store_true", help="Do not check certificate. (default: False)") + # parser.add_argument("--no-check-certificate", default=False, action="store_true", help="Do not check certificate. (default: False)") group_export = parser.add_argument_group("Export results") group_export.add_argument("--export-xlsx", dest="export_xlsx", type=str, default=None, required=False, help="Output XLSX file to store the results in.") @@ -152,7 +151,7 @@ def parseArgs(): group_targets_source = parser.add_argument_group("Targets") group_targets_source.add_argument("-tf", "--targets-file", default=None, type=str, help="Path to file containing a line by line list of targets.") group_targets_source.add_argument("-tt", "--target", default=[], type=str, action='append', help="Target IP, FQDN or CIDR") - group_targets_source.add_argument("-tp", "--target-ports", default="80,443,8080", type=str, help="Target ports to scan top search for Apache Tomcat servers.") + group_targets_source.add_argument("-tp", "--target-ports", default="80,443,8080,8081,9080,9081,10080", type=str, help="Target ports to scan top search for Apache Tomcat servers.") group_targets_source.add_argument("-ad", "--auth-domain", default="", type=str, help="Windows domain to authenticate to.") group_targets_source.add_argument("-ai", "--auth-dc-ip", default=None, type=str, help="IP of the domain controller.") group_targets_source.add_argument("-au", "--auth-user", default=None, type=str, help="Username of the domain account.") @@ -189,14 +188,13 @@ def main(): config.set_request_available_schemes(only_http=options.only_http, only_https=options.only_https) config.set_request_timeout(options.request_timeout) config.set_request_proxies(options.proxy_ip, options.proxy_port) - config.set_request_no_check_certificate(options.no_check_certificate) + # config.set_request_no_check_certificate(options.no_check_certificate) config.set_list_cves_mode(options.list_cves) config.load_credentials_from_options(options.tomcat_username, options.tomcat_password, options.tomcat_usernames_file, options.tomcat_passwords_file) - reporter = Reporter(config=config) - vulns_db = VulnerabilitiesDB(config=config) + reporter = Reporter(config=config, vulns_db=vulns_db) # Parsing targets and ports targets = load_targets(options, config) @@ -211,10 +209,12 @@ def main(): # Exploring targets if len(targets) != 0 and options.threads != 0: print("[+] Searching for Apache Tomcats servers on specified targets ...") - with ThreadPoolExecutor(max_workers=min(options.threads, len(targets))) as tp: + monitor_data = {"actions_performed": 0, "total": (len(targets)*len(ports)), "lock": threading.Lock()} + with ThreadPoolExecutor(max_workers=min(options.threads, 1+monitor_data["total"])) as tp: + tp.submit(monitor_thread, reporter, config, monitor_data) for target in targets: for port in ports: - tp.submit(scan_worker, target, port, reporter, vulns_db, config) + tp.submit(scan_worker, target, port, reporter, config, monitor_data) print("[+] All done!") if options.export_xlsx is not None: diff --git a/apachetomcatscanner/data/credentials.json b/apachetomcatscanner/data/credentials.json index 8252a38..b298552 100644 --- a/apachetomcatscanner/data/credentials.json +++ b/apachetomcatscanner/data/credentials.json @@ -5,11 +5,21 @@ "password": "admin", "description": "" }, + { + "username": "admin", + "password": "", + "description": "" + }, { "username": "admin", "password": "tomcat", "description": "" }, + { + "username": "tomcat", + "password": "", + "description": "" + }, { "username": "tomcat", "password": "admin", diff --git a/apachetomcatscanner/test_environement/versions/6.0.43/tomcat-users.xml b/apachetomcatscanner/test_environement/versions/6.0.43/tomcat-users.xml index e3debc9..c486880 100644 --- a/apachetomcatscanner/test_environement/versions/6.0.43/tomcat-users.xml +++ b/apachetomcatscanner/test_environement/versions/6.0.43/tomcat-users.xml @@ -29,5 +29,7 @@ + + diff --git a/apachetomcatscanner/utils/network.py b/apachetomcatscanner/utils/network.py new file mode 100644 index 0000000..68bbeb1 --- /dev/null +++ b/apachetomcatscanner/utils/network.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# File name : network.py +# Author : Podalirius (@podalirius_) +# Date created : 17 Jan 2023 + +import socket + +import requests +# Disable warnings of insecure connection for invalid certificates +requests.packages.urllib3.disable_warnings() +# Allow use of deprecated and weak cipher methods +requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' +try: + requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' +except AttributeError: + pass + + +def is_target_a_windows_machine(target) -> bool: + # if port 135 and 445 open + if is_port_open(target, 135) and is_port_open(target, 445): + return True + else: + return False + + +def is_target_a_windows_domain_controller(target) -> bool: + # if port 135 and 445 and 88 open + if is_target_a_windows_machine(target) and is_port_open(target, 88): + return True + else: + return False + + +def is_port_open(target, port) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1) + return s.connect_ex((target, port)) == 0 + + +def is_http_accessible(target, port, config, scheme="http"): + url = "%s://%s:%d/" % (scheme, target, port) + try: + r = requests.get( + url, + timeout=config.request_timeout, + proxies=config.request_proxies, + verify=(not (config.request_no_check_certificate)) + ) + return True + except Exception as e: + config.debug("Error in is_http_accessible('%s', %d, '%s'): %s " % (target, port, scheme, e)) + return False \ No newline at end of file diff --git a/apachetomcatscanner/utils/scan.py b/apachetomcatscanner/utils/scan.py index 3b30a15..b5104a4 100644 --- a/apachetomcatscanner/utils/scan.py +++ b/apachetomcatscanner/utils/scan.py @@ -5,35 +5,23 @@ # Date created : 29 Jul 2022 import base64 +import datetime import re -import requests - +import time +from apachetomcatscanner.utils.network import is_port_open, is_http_accessible -def is_target_a_windows_machine() -> bool: - # if port 135 and 445 open - pass - -def is_target_a_windows_domain_controller() -> bool: - # if port 135 and 445 and 88 and ldap/ldaps open +import requests +# Disable warnings of insecure connection for invalid certificates +requests.packages.urllib3.disable_warnings() +# Allow use of deprecated and weak cipher methods +requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' +try: + requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' +except AttributeError: pass -def is_http_accessible(target, port, config, scheme="http"): - url = "%s://%s:%d/" % (scheme, target, port) - try: - r = requests.get( - url, - timeout=config.request_timeout, - proxies=config.request_proxies, - verify=(not (config.request_no_check_certificate)) - ) - return True - except Exception as e: - config.debug("Error in is_http_accessible('%s', %d, '%s'): %s " % (target, port, scheme, e)) - return False - - def is_tomcat_manager_accessible(target, port, config, scheme="http"): path = "/manager/html" url = "%s://%s:%d%s" % (scheme, target, port, path) @@ -97,56 +85,64 @@ def try_default_credentials(target, port, config, scheme="http"): return found_credentials -def scan_worker(target, port, reporter, vulns_db, config): - config.debug("scan_worker('%s', %d, ...)" % (target, port)) - result = {"target": target} - for scheme in config.get_request_available_schemes(): - if is_http_accessible(target, port, config, scheme): - result["version"] = get_version_from_malformed_http_request(target, port, config, scheme) - if result["version"] is not None: - config.debug("Found version %s" % result["version"]) - - result["manager_accessible"] = is_tomcat_manager_accessible(target, port, config, scheme) - - credentials = [] - if result["manager_accessible"]: - config.debug("Manager is accessible") - # Test for default credentials - credentials = try_default_credentials(target, port, config, scheme) - - str_found_creds = [] - if len(credentials) != 0: - for statuscode, creds in credentials: - str_found_creds.append("(username:\x1b[1;92m%s\x1b[0m password:\x1b[1;92m%s\x1b[0m)" % (creds["username"], creds["password"])) - - # List of cves - cve_str = "" - if config.list_cves_mode == True: - cve_list = vulns_db.get_vulnerabilities_of_version_sorted_by_criticity(result["version"], colors=True, reverse=True) - if len(cve_list) != 0: - cve_str = "CVEs: %s" % ', '.join(cve_list) - - print("[>] [Apache Tomcat/\x1b[1;95m%s\x1b[0m] on \x1b[1;93m%s\x1b[0m:\x1b[1;93m%d\x1b[0m (manager:%s) %s %s\x1b[0m " % ( - result["version"], - target, - port, - ("\x1b[1;92maccessible\x1b[0m" if result["manager_accessible"] else "\x1b[1;91mnot accessible\x1b[0m"), - ' '.join(str_found_creds), - cve_str - ) - ) - - cve_list = vulns_db.get_vulnerabilities_of_version_sorted_by_criticity(result["version"], colors=False, reverse=True) - credentials_str = "username:%s\npassword:%s" % (credentials[0][1]["username"], credentials[0][1]["password"]) - cve_str = ', '.join([cve["cve"]["id"] for cve in cve_list]) - - reporter.report_result( - target, - port, - result["version"], - result["manager_accessible"], - credentials_str, - cve_str - ) +def scan_worker(target, port, reporter, config, monitor_data): + try: + result = {"target": target} + + if is_port_open(target, port): + for scheme in config.get_request_available_schemes(): + if is_http_accessible(target, port, config, scheme): + result["version"] = get_version_from_malformed_http_request(target, port, config, scheme) + if result["version"] is not None: + config.debug("Found version %s" % result["version"]) + + result["manager_accessible"] = is_tomcat_manager_accessible(target, port, config, scheme) + + credentials_found = [] + if result["manager_accessible"]: + config.debug("Manager is accessible") + # Test for default credentials + credentials_found = try_default_credentials(target, port, config, scheme) + + reporter.report_result( + target, + port, + result["version"], + result["manager_accessible"], + credentials_found + ) + + monitor_data["lock"].acquire() + monitor_data["actions_performed"] = monitor_data["actions_performed"] + 1 + # print("Updated for port %d" % port) + monitor_data["lock"].release() + + except Exception as e: + if config.debug_mode: + print("[Error in %s] %s" % (__name__, e)) + + +def monitor_thread(reporter, config, monitor_data): + last_check, monitoring = 0, True + while monitoring: + new_check = monitor_data["actions_performed"] + rate = (new_check - last_check) + if not config.debug_mode: + print("\r", end="") + reporter.print_new_results() + print("[%s] Status (%d/%d) %5.2f %% | Rate %d tests/s " % ( + datetime.datetime.now().strftime("%Y/%m/%d %Hh%Mm%Ss"), + new_check, monitor_data["total"], (new_check/monitor_data["total"])*100, + rate + ), + end=("" if not config.debug_mode else "\n") + ) + last_check = new_check + time.sleep(1) + if rate == 0 and monitor_data["actions_performed"] == monitor_data["total"]: + monitoring = False + if len(reporter._new_results) != 0: + reporter.print_new_results() + print() \ No newline at end of file diff --git a/setup.py b/setup.py index 146730b..9a912a4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setuptools.setup( name="apachetomcatscanner", - version="2.3.5", + version="3.0", description="", url="https://github.com/p0dalirius/ApacheTomcatScanner", author="Podalirius",