
Palo Alto Networks PAN-OS contains an authentication bypass caused by flaws in the GlobalProtect portal and gateway, letting attackers establish unauthorized VPN connections, exploit requires network access to the portal or gateway.
This exploit achieves unauthenticated VPN access to a Palo Alto GlobalProtect gateway/portal by forging an authentication cookie using only the server's publicly available TLS certificate.
GlobalProtect uses a pre-authentication cookie (portal-userauthcookie) to allow clients to authenticate. Here's the fatal flaw:
Normal Flow:
1. Client authenticates (username + password)
2. Server generates a cookie → encrypts it with server's RSA PUBLIC key
3. Client stores the encrypted cookie
4. On reconnect, client sends the cookie → server decrypts with PRIVATE key → trusts it
The Bug:
The server ONLY checks if the cookie decrypts successfully with its private key.
It does NOT verify WHO encrypted it or if the plaintext content is legitimate.
Since the RSA public key is embedded in the server's TLS certificate (publicly accessible to anyone who connects), any attacker can:
This is a textbook broken authentication flaw — using encryption where a digital signature or HMAC was needed.
flowchart TD
A["Step 1: Raw TCP Connect"] --> B["Step 2: Send Crafted TLS ClientHello"]
B --> C["Step 3: Parse ServerHello → Extract DER Certificates"]
C --> D["Step 4: Walk ASN.1 to Extract RSA Public Key"]
D --> E["Step 5: Forge PKCS#1 v1.5 Encrypted Cookie"]
E --> F["Step 6: POST to /ssl-vpn/login.esp"]
F --> G{"Server Decrypts Cookie"}
G -->|"Valid plaintext"| H[" Auth Bypass — VPN Access Granted"]
G -->|"Invalid"| I[" Rejected"]
Why raw? We need the server's certificate in DER (raw binary) format. Python's
sslmodule completes the full TLS handshake internally and doesn't expose the raw cert bytes the same way. By doing a raw TCP connection and sending a hand-crafted ClientHello, we can intercept the server's response at the byte level.
A TLS record looks like this:
┌──────────────────────────────────────────────────┐
│ TLS Record Header (5 bytes) │
│ ┌──────┬──────────┬────────────┐ │
│ │ Type │ Version │ Length │ │
│ │ 0x16 │ 0x03 01 │ 2 bytes │ │
│ │(Hshk)│(TLS 1.0) │ │ │
│ └──────┴──────────┴────────────┘ │
│ │
│ Handshake Message │
│ ┌──────┬────────────┬─────────────────────────┐ │
│ │ Type │ Length │ Body │ │
│ │ 0x01 │ 3 bytes │ (ClientHello) │ │
│ │(CHlo)│ │ │ │
│ └──────┴────────────┴─────────────────────────┘ │
└──────────────────────────────────────────────────┘
The ClientHello body contains:
| Field | Value | Purpose |
|---|---|---|
| Version | 0x03 0x03 (TLS 1.2) | Tell server we speak TLS 1.2 |
| Random | 4-byte timestamp + 28 random bytes | Nonce for the handshake |
| Session ID | 0x00 (empty) | No session resumption |
| Cipher Suites | 9 suites including TLS_RSA_WITH_AES_128_CBC_SHA | Key: we include RSA-only ciphers to force the server to use its RSA cert |
| Compression | 0x00 (none) | Required |
Extensions included:
| Extension | ID | Purpose |
|---|---|---|
| SNI (Server Name Indication) | 0x0000 | Tell the server which hostname we're connecting to |
| Signature Algorithms | 0x000D | Advertise which sig algorithms we support |
| Supported Groups | 0x000A | EC curves we support (P-256, P-384, P-521) |
| EC Point Formats | 0x000B | Uncompressed EC points |
[!NOTE] The cipher suites intentionally include RSA key exchange ciphers (
0x002F=TLS_RSA_WITH_AES_128_CBC_SHA). This nudges the server to respond with its RSA certificate rather than an ECDSA one — which is critical because the exploit only works with RSA.
After sending the ClientHello, the server sends back multiple TLS records:
Server Response:
┌─────────────────┐
│ ServerHello │ (handshake type 2)
├─────────────────┤
│ Certificate │ (handshake type 11) ← WE WANT THIS
├─────────────────┤
│ ServerKeyExchange│ (handshake type 12, optional)
├─────────────────┤
│ ServerHelloDone │ (handshake type 14) ← STOP SIGNAL
└─────────────────┘
Phase 1 — Strip TLS record headers:
Each TLS record has a 5-byte header: [type(1)] [version(2)] [length(2)]. The code scans through all records, and for any with type == 22 (Handshake), it concatenates their payloads:
while i + 5 <= len(data):
t = data[i] # content type
rl = (data[i + 3] << 8) | data[i + 4] # record length
if t == 22: # Handshake
hs.extend(data[i + 5: i + 5 + rl]) # grab payload
i += 5 + rl # next record
Phase 2 — Find the Certificate message (type 11):
Inside the handshake stream, each message has a 4-byte header: [type(1)] [length(3)]. We scan for type == 11:
while j + 4 <= len(hs):
ht = hs[j] # handshake type
hl = (hs[j+1] << 16) | (hs[j+2] << 8) | hs[j+3] # 3-byte length
if ht == 11: # Certificate!
# Parse the certificate list inside
Phase 3 — Extract individual DER certificates:
The Certificate message contains a list of certificates, each prefixed by a 3-byte length:
Certificate Message Body:
┌───────────────────────────────────┐
│ Total Certs Length (3 bytes) │
├───────────────────────────────────┤
│ Cert 1 Length (3 bytes) │
│ Cert 1 DER data (variable) │
├───────────────────────────────────┤
│ Cert 2 Length (3 bytes) │
│ Cert 2 DER data (variable) │
├───────────────────────────────────┤
│ ... │
└───────────────────────────────────┘
In our test run, we got 3 certificates (leaf cert, intermediate CA, root CA).
This function scans for ServerHelloDone (handshake type 14), which tells us the server is done sending and we can stop reading.
X.509 certificates are encoded in DER (Distinguished Encoding Rules), which is a binary format based on ASN.1 (Abstract Syntax Notation One).
Every element in DER is:
┌─────┬────────┬───────────────────┐
│ Tag │ Length │ Value (payload) │
│ 1B │ 1-5B │ variable │
└─────┴────────┴───────────────────┘
Length encoding:
0x80: length is that byte directly (short form)0x80: low 7 bits = number of following bytes that encode the length (long form)# Example: length byte = 0x82 → 2 more bytes follow
# Next 2 bytes: 0x06 0x4F → length = 0x064F = 1615 bytes