Palo Alto Networks PAN-OS contiene una omisión de autenticación causada por fallos en el portal y la puerta de enlace de GlobalProtect, lo que permite a los atacantes establecer conexiones VPN no autorizadas; la explotación requiere acceso de red al portal o a la puerta de enlace.
Este exploit consigue acceso VPN sin autenticación a una pasarela/portal GlobalProtect de Palo Alto falsificando una cookie de autenticación usando únicamente el certificado TLS disponible públicamente del servidor.
GlobalProtect utiliza una cookie pre-autenticación (portal-userauthcookie) para permitir que los clientes se autentiquen. Aquí está el fallo fatal:
Flujo Normal:
1. El cliente se autentica (usuario + contraseña)
2. El servidor genera una cookie → la cifra con la clave PÚBLICA RSA del servidor
3. El cliente guarda la cookie cifrada
4. En la reconexión, el cliente envía la cookie → el servidor la descifra con la clave PRIVADA → confía en ella
El Bug:
El servidor SOLO comprueba si la cookie se descifra correctamente con su clave privada.
NO verifica QUIÉN la cifró ni si el contenido en texto plano es legítimo.
Dado que la clave pública RSA está incrustada en el certificado TLS del servidor (accesible públicamente para cualquiera que se conecte), cualquier atacante puede:
Esto es un fallo de autenticación rota de manual — usar cifrado donde se necesitaba una firma digital o un HMAC.
flowchart TD
A["Paso 1: Conexión TCP Cruda"] --> B["Paso 2: Enviar ClientHello TLS Manipulado"]
B --> C["Paso 3: Parsear ServerHello → Extraer Certificados DER"]
C --> D["Paso 4: Recorrer ASN.1 para Extraer la Clave Pública RSA"]
D --> E["Paso 5: Falsificar Cookie Cifrada PKCS#1 v1.5"]
E --> F["Paso 6: POST a /ssl-vpn/login.esp"]
F --> G{"El Servidor Descifra la Cookie"}
G -->|"Texto plano válido"| H[" Omisión de Autenticación — Acceso VPN Concedido"]
G -->|"Inválido"| I[" Rechazado"]
¿Por qué crudo? Necesitamos el certificado del servidor en formato DER (binario crudo). El módulo
sslde Python completa el handshake TLS internamente y no expone los bytes crudos del certificado de la misma manera. Al hacer una conexión TCP cruda y enviar un ClientHello hecho a mano, podemos interceptar la respuesta del servidor a nivel de bytes.
Un registro TLS tiene este aspecto:
┌──────────────────────────────────────────────────┐
│ Cabecera de Registro TLS (5 bytes) │
│ ┌──────┬──────────┬────────────┐ │
│ │ Tipo │ Versión │ Longitud │ │
│ │ 0x16 │ 0x03 01 │ 2 bytes │ │
│ │(Hshk)│(TLS 1.0) │ │ │
│ └──────┴──────────┴────────────┘ │
│ │
│ Mensaje de Handshake │
│ ┌──────┬────────────┬─────────────────────────┐ │
│ │ Tipo │ Longitud │ Cuerpo │ │
│ │ 0x01 │ 3 bytes │ (ClientHello) │ │
│ │(CHlo)│ │ │ │
│ └──────┴────────────┴─────────────────────────┘ │
└──────────────────────────────────────────────────┘
El cuerpo del ClientHello contiene:
| Campo | Valor | Propósito |
|---|---|---|
| Versión | 0x03 0x03 (TLS 1.2) | Indicar al servidor que hablamos TLS 1.2 |
| Aleatorio | Marca de tiempo de 4 bytes + 28 bytes aleatorios | Nonce para el handshake |
| ID de Sesión | 0x00 (vacío) | Sin reanudación de sesión |
| Suites de Cifrado | 9 suites incluyendo TLS_RSA_WITH_AES_128_CBC_SHA | Clave: incluimos cifrados solo-RSA para forzar al servidor a usar su certificado RSA |
| Compresión | 0x00 (ninguna) | Obligatorio |
Extensiones incluidas:
| Extensión | ID | Propósito |
|---|---|---|
| SNI (Indicación de Nombre de Servidor) | 0x0000 | Indicar al servidor a qué hostname nos conectamos |
| Algoritmos de Firma | 0x000D | Anunciar qué algoritmos de firma soportamos |
| Grupos Soportados | 0x000A | Curvas EC que soportamos (P-256, P-384, P-521) |
| Formatos de Punto EC | 0x000B | Puntos EC sin comprimir |
[!NOTE] Las suites de cifrado incluyen intencionalmente cifrados de intercambio de claves RSA (
0x002F=TLS_RSA_WITH_AES_128_CBC_SHA). Esto empuja al servidor a responder con su certificado RSA en lugar de uno ECDSA — lo cual es crítico porque el exploit solo funciona con RSA.
Después de enviar el ClientHello, el servidor envía de vuelta múltiples registros TLS:
Respuesta del Servidor:
┌─────────────────┐
│ ServerHello │ (tipo de handshake 2)
├─────────────────┤
│ Certificate │ (tipo de handshake 11) ← ESTE NOS INTERESA
├─────────────────┤
│ ServerKeyExchange│ (tipo de handshake 12, opcional)
├─────────────────┤
│ ServerHelloDone │ (tipo de handshake 14) ← SEÑAL DE PARADA
└─────────────────┘
Fase 1 — Quitar las cabeceras de registro TLS:
Cada registro TLS tiene una cabecera de 5 bytes: [tipo(1)] [versión(2)] [longitud(2)]. El código escanea todos los registros y, para cualquier registro con type == 22 (Handshake), concatena sus cargas útiles:
while i + 5 <= len(data):
t = data[i] # tipo de contenido
rl = (data[i + 3] << 8) | data[i + 4] # longitud del registro
if t == 22: # Handshake
hs.extend(data[i + 5: i + 5 + rl]) # tomar la carga útil
i += 5 + rl # siguiente registro
Fase 2 — Encontrar el mensaje Certificate (tipo 11):
Dentro del flujo de handshake, cada mensaje tiene una cabecera de 4 bytes: [tipo(1)] [longitud(3)]. Escaneamos buscando type == 11:
while j + 4 <= len(hs):
ht = hs[j] # tipo de handshake
hl = (hs[j+1] << 16) | (hs[j+2] << 8) | hs[j+3] # longitud de 3 bytes
if ht == 11: # Certificate!
# Parsear la lista de certificados dentro
Fase 3 — Extraer los certificados DER individuales:
El mensaje Certificate contiene una lista de certificados, cada uno prefijado por una longitud de 3 bytes:
Cuerpo del Mensaje Certificate:
┌───────────────────────────────────┐
│ Longitud Total de Certificados (3 bytes) │
├───────────────────────────────────┤
│ Longitud del Cert 1 (3 bytes) │
│ Datos DER del Cert 1 (variable) │
├───────────────────────────────────┤
│ Longitud del Cert 2 (3 bytes) │
│ Datos DER del Cert 2 (variable) │
├───────────────────────────────────┤
│ ... │
└───────────────────────────────────┘
En nuestra ejecución de prueba, obtuvimos 3 certificados (certificado hoja, CA intermedia, CA raíz).
Esta función escanea buscando ServerHelloDone (tipo de handshake 14), que nos indica que el servidor ha terminado de enviar y podemos dejar de leer.
Los certificados X.509 están codificados en DER (Distinguished Encoding Rules), que es un formato binario basado en ASN.1 (Abstract Syntax Notation One).
Cada elemento en DER es:
┌─────┬────────┬───────────────────┐
│ Tag │ Longitud │ Valor (payload) │
│ 1B │ 1-5B │ variable │
└─────┴────────┴───────────────────┘