""" CVE-2026-29145 Vulnerability Testing Script This module tests for authentication bypass vulnerability in Apache Tomcat's Mutual TLS (CLIENT_CERT) implementation when OCSP revocation checking is configured with hard-fail mode. The vulnerability occurs when: 1. CLIENT_CERT authentication is enabled 2. OCSP revocation checking is enabled 3. Soft-fail option is disabled (hard-fail mode) 4. The OCSP responder is unreachable or returns an error Expected behavior (patched systems): - OCSP check failure → HTTP 403 Forbidden (hard denial) - Access is denied if OCSP responder fails Vulnerable behavior (CVE-2026-29145): - OCSP check failure → HTTP 200 OK (authentication bypass) - Access is granted despite OCSP responder failure Author: Security Testing Team CVE: CVE-2026-29145 CVSS Score: 9.1 (Critical) """ import requests import sys import logging from pathlib import Path # Configure logging with timestamp and level information logging.basicConfig( level=logging.INFO, format='[%(levelname)s] %(message)s' ) logger = logging.getLogger(__name__) # --- Exit Codes --- # Define distinct exit codes for test outcomes EXIT_CODE_NOT_VULNERABLE = 0 EXIT_CODE_ERROR = 1 EXIT_CODE_VULNERABLE = 10 # --- Configuration --- # Configuration constants # Use paths relative to the script's location to allow running from any directory SCRIPT_DIR = Path(__file__).resolve().parent TARGET_URL = "https://localhost:8443/protected-resource" CLIENT_CERT = SCRIPT_DIR / "certs/client-cert.pem" CLIENT_KEY = SCRIPT_DIR / "certs/client-key.pem" CA_BUNDLE = SCRIPT_DIR / "certs/ca-chain.pem" TIMEOUT = 15 # Seconds to wait for response MAX_RETRIES = 3 # Number of retry attempts on connection failure def verify_certificates_exist(): """ Verify that all required certificate files exist before attempting connection. Returns: bool: True if all certificates exist, False otherwise. """ certs = [CLIENT_CERT, CLIENT_KEY, CA_BUNDLE] missing = [str(cert) for cert in certs if not cert.exists()] if missing: logger.error(f"Missing certificate files: {', '.join(missing)}") logger.error("Run './setup_certs.sh' to generate certificates") return False return True def check_tomcat_version(headers): """ Checks the Tomcat version from server headers and warns if it appears patched. Note: Default Tomcat configurations may not expose the version number. """ server_header = headers.get("Server") if not server_header: logger.debug("Server header not found, cannot check Tomcat version.") return # This is a best-effort check, as the version is not always present. # Example patched versions from README: 10.1.53+, 9.0.116+, 11.0.20+ import re match = re.search(r'(\d+\.\d+\.\d+)', server_header) if not match: logger.debug(f"Tomcat version not found in Server header ('{server_header}').") return version_str = match.group(1) version_parts = [int(p) for p in version_str.split('.')] major, minor, micro = version_parts[0], version_parts[1], version_parts[2] is_patched = ( (major == 10 and minor == 1 and micro >= 53) or (major == 9 and minor == 0 and micro >= 116) or (major == 11 and minor == 0 and micro >= 20) ) if is_patched: logger.warning(f"Detected Tomcat version {version_str} appears to be patched against CVE-2026-29145.") def test_vulnerability(): """ Test if the target Tomcat server is vulnerable to CVE-2026-29145. Returns: int: An exit code representing the outcome of the test. - 10 (EXIT_CODE_VULNERABLE): Access was granted. - 0 (EXIT_CODE_NOT_VULNERABLE): Access was denied. - 1 (EXIT_CODE_ERROR): The test could not be completed. """ logger.info(f"Attempting connection to {TARGET_URL}...") # Pre-flight check: verify all certificate files exist if not verify_certificates_exist(): return EXIT_CODE_ERROR # Attempt connection with retry logic for transient failures for attempt in range(1, MAX_RETRIES + 1): try: logger.debug(f"Connection attempt {attempt}/{MAX_RETRIES}") # Send HTTPS request with mutual TLS authentication response = requests.get( TARGET_URL, cert=(CLIENT_CERT, CLIENT_KEY), verify=CA_BUNDLE, timeout=TIMEOUT ) # Check server version for known patched versions check_tomcat_version(response.headers) # Analyze response to determine vulnerability status if response.status_code == 200: # VULNERABLE: Access granted despite OCSP failure logger.warning("VULNERABLE: Access granted despite OCSP check failure.") logger.warning(f"Response preview: {response.text[:100]}") return EXIT_CODE_VULNERABLE elif response.status_code in [401, 403]: # NOT VULNERABLE: Authentication/authorization correctly denied logger.info("NOT VULNERABLE: Access denied (Authentication working).") return EXIT_CODE_NOT_VULNERABLE else: # Unexpected status code - log for investigation logger.error(f"Unexpected status code: {response.status_code}") return EXIT_CODE_ERROR except requests.exceptions.SSLError as e: logger.info("SSL Handshake failed (likely hard-fail working correctly).") logger.debug(f"SSL Error details: {e}") return EXIT_CODE_NOT_VULNERABLE except requests.exceptions.ConnectionError as e: if attempt < MAX_RETRIES: logger.warning(f"Connection failed (attempt {attempt}/{MAX_RETRIES}): {e}") continue else: logger.error(f"Failed to connect to {TARGET_URL} after {MAX_RETRIES} attempts.") logger.error("Ensure Tomcat container is running: docker-compose up -d") return EXIT_CODE_ERROR except requests.exceptions.Timeout: if attempt < MAX_RETRIES: logger.warning(f"Connection timeout (attempt {attempt}/{MAX_RETRIES}), retrying...") continue else: logger.error(f"Connection to {TARGET_URL} timed out after {MAX_RETRIES} attempts.") return EXIT_CODE_ERROR except Exception as e: logger.error(f"Unexpected error: {e}") return EXIT_CODE_ERROR if __name__ == "__main__": result = test_vulnerability() sys.exit(result)