Skip to content Skip to footer

Tales from DEF CON 33: Starpwn CTF 2025

Visionspace is proud to be part of the great team contributing to Starpwn CTF 2025, a space-focused CTF hosted by the Aerospace Village during DEF CON 33. We wanted to come up with challenges for all skill levels but ultimately provide a realistic challenge for all teams. This would require players to dive deep into space-related applications, study how space infrastructure works, and dissect the protocols used to communicate with spacecraft.

Overall, 1500 players and ~950 teams engaged in the challenges, and the positive feedback and energy surrounding the event are already pushing us to develop something bigger and better for next year.

Figure 1 – CTF Landing page

Results and highlights

Overall, of the 15 challenges available, six were left unsolved. Perhaps a puzzle for a later time *wink wink*. The top 3 teams scored pretty closely, with the tie breaking coming down to faster solve:

Figure 2 – Final top 3 scoreboard

A massive shoutout to team Lil L3ak for winning the competition and displaying excellent skills when faced with unfamiliar challenges and environments.

That said, you can find the write-ups for our challenges below.

A Scream in the Void

Description

Figure 3 – Challenge description

Solution

This challenge starts with a single WAV file and some indication that it is from a transmission on the surface. Looking at its spectrogram, it shows strong tones around 1900 and 1200 Hz, plus frequency sweeping at ~1500–2300 Hz. In addition, the file length is ~36s, which matches Robot 36. At that point, most SSTV decoder tools should be able to decode the file, even identifying the right mode automatically. We successfully used https://github.com/colaclanth/sstv.

Chapter 1 – Operation Dark Night

Figure 4 – Chapter 1 phase 1 description

This challenge required you to thoroughly read the documentation for fprime and get familiar with how it operates. While this becomes a high gatekeep right from the start, once that foothold is obtained, it can be reused throughout the challenge, and it becomes similar to a more typical web challenge.

So first, visiting the link provided would show a small webpage with a project description and some important information:

Figure 5 – Challenge main page

The most important points would be to notice the information about their tech stack, that mentions the use of fprime and fprime-gds. That’s your flight software and ground station software. While not absolutely required, installing and running the template project available on the fprime website could make this challenge much easier to solve, given that most of the attack chain is blind, and you never get direct access to either during the challenge.

Briefly about fprime

NASA’s fprime (F’) is an open-source software framework for developing and deploying embedded systems, particularly for spaceflight missions. It is maintained and developed by NASA’s Jet Propulsion Laboratory (JPL), and allows for modular system development by defining the exact interactions of components using a standard interface. On the other hand, the fprime-gds is a collection of classes and utilities that provide an interface for fprime deployments to show telemetry and events and to issue commands. Even though it doesn’t require a UI, the default installation spawns an instantaneous web UI for ease of use. The UI utilizes Vue.js for the front end and Python/Flask for the back end and network interaction. While it’s not required to dive too much into the internal workings of fprime itself, it’s important to understand how its infrastructure is defined and how the communication protocol works.

First, infrastructure. In this challenge, fprime-gds abstracts the ground segment of the mission, that is, it handles all the logic behind receiving packets and telemetry, parsing and collecting the information in them, and preparing and sending commands to the spacecraft. On the other side of the equation, fprime is the onboard software running on the spacecraft. It includes all the logic behind what to do when each command is received and the necessary operational functions.

Based on the description of the challenge, our setup allows us to inject information into the ground station, as shown below.

Figure 6 – Simplified schematic of the operational architecture and attack path

A second important piece of information is the pcap file. This contains the first command sent to the spacecraft, that just echoes back whatever text is sent. It also contains the command id for that echo command, as well as the payload format (the packet format for fprime). Either reading the documentation, or dumping a few packets from a running fprime instance, it would be possible to determine the correct packet format needed to inject into fprime-gds.

An fprime packet follows a simple format. For example, for the event received after the echo command from the challenge:

Where the args can then be further divided into:

The third important detail is understanding what is being revealed in the `/log` page. Again, by searching, reading the docs, or installing and running fprime, one could notice that this directly exposes the events log file from fprime-gds. This is the only direct output available, and thus something important to monitor.

Figure 7 – Screenshot of the /log endpoint on the challenge webpage, exposing the event.log file

The final piece of information required was the version of fprime-gds in use, displayed on the main page of the project. A Google search for CVEs present in fprime-gds would reveal a previous writeup. While the post mentions an earlier version, checking the fprime-gds patch notes, you’d find no mentions of any security fixes for this CVE, making the later versions still vulnerable.

This is everything you’d need to exploit all episodes for chapter 1 successfully.


Phase 1 – A crack appears

Description

“We’ve observed some weird behavior from one of their satellite demons, but we have no idea what its capabilities are. Your first goal is to extract information regarding the Nightfall mission, find what that blasted thing can do.“

The description tells you to find all other capabilities of Cacodemsat. Enumerating the main website would show that it is mostly static content, and we can only inject things into the ground station using the port provided. The only course of action is figuring out how to determine functionalities from fprime-gds. A good place to start would be the command dictionary, which lists all commands available for that instance.

How can we access it without direct access? Reading the CVE descriptions, we know that the event fields on fprime-gds are vulnerable to XSS. And we also have an injection point into gds, allowing us to essentially spoof satellite responses. We can assume that somewhere there is a system running fprime-gds for telemetry, based on the descriptions. With this, we can generate a packet that mimics an event, with a field long enough to contain an XSS payload, that would then be triggered on that system. The first command sent (pcap) has a text input that is 400+ chars long, this should be enough for most XSS payloads.

Packet format

The simplest method here would be to grab the ETCH field from the pcap, understand what fields it contains, and which need to be modified, and generate a new packet from there. The 2nd easiest method would be to install fprime and capture all packets while sending a few commands, a sequence of packets can make more obvious what fields change and what they represent. One could also grab the fprime source code and documentation and retrieve it from there, though the documentation is a bit sparse on details. A short description of the packet format was mentioned above.

At this point, it would be advisable to build a small packet builder function, as it will also be used for all the other episodes.

At this stage, either just trying to replay the commands from the pcap and injecting them, or creating a packet with the same command id and custom content, would reveal the full command name on the logs.

Figure 8 – Log output from replaying the command shown in the pcap

This would be important for the next step. (It is also possible to brute force all commands id’s in this way, though that wouldn’t be very useful as you’d trigger their event message as if it was executed on the satellite when nothing happened).

XSS payload and data exfiltration

Our first target is the JSON dictionary file containing all available commands. This is exposed as JSON on `/dictionary/commands` on the fprime-gds main UI.

Figure 9 – JSON dictionary containing all commands available to fprime

Our only output is the events.log exposed on the main webpage, so that will be our exfil method. We only know one command id, from the pcap, that echoes whatever is sent. This will create an event with its contents, that is logged and thus available to us.

So now the goal becomes crafting a packet that contains an XSS payload that can read the JSON dictionary file and send it out with the echo command.

Turning once again to either docs or a running instance of fprime-gds, commands are sent with a PUT request to `/commands/FULL_COMMAND_NAME`, with a JSON structure containing `{key:4276996862,arguments:[command arguments]}`, using the command name obtained from the pcap `/commands/CacodemSat.BeholdersEye.ECHO_NTATION`.

The final tricks are just to make sure that we can get the entire file without hitting the command’s character limit. There are multiple ways to do this. We went with b64 encoding the JSON contents, then split it into 400-character blocks and sent each block using the ECHO_NTATION command. The JS payload was minimized with jsmin to keep the size in check before sending, and a 1.5s timeout was included to avoid any rate limit issues.

And the final payload looks like this: `<img/src/onerror=eval(atob(“{cmd_string}”))>`.

So, taking the previous packet format, the main packet looks like this:

deadbeef 00000229 00000002 0000066a args..

And arguments:

0002 006859722f000af721 0214 3c696d672f.. CRC

We then prepare and send this packet as bytes to the available port, investigate the logs page, and you’ll see base64 encoded strings starting to appear. The echo command tags all events with a unique string to make it easier to parse. One of the commands will have a flag in its description.

Figure 10 – Expected exfiltration using the log file, data is sent out base64 encoded in the satellite logs

While this could be hard to debug blindly, the first tutorial for fprime should take relatively little time and guide the user on setting up an echo command similar to the one used in the challenge (although slightly less permissive lengthwise). The best way to increase the chances of success would be to take a red-team approach and set up a semi-twin system to test the payloads before using them on the live systems.

From then on, all episodes reuse the same entry point, with different targets to obtain other information and potential lateral movement so that we will skip the packet format setup.

Phase 2 – A Flying Watcher

Figure 11 – Chapter 1 phase 2 description

You’ve obtained information that what Nightfall was showing us was not the real satellite capture.

This time, your goal is to trigger one of the other commands and obtain the real image. From the command dictionary obtained in episode 1, we know that one of the commands forces Cacodemsat to blink and take another capture. Modifying the previous payload to trigger that command:

Now there are two ways to notice what happened next:

  1. If you’re monitoring the logs, you’ll notice a new capture being downloaded with the name `flag_me.png`, which should make you look at the captures page.
  2. If you monitor the captures page, you’ll see a new image without clouds but with a flag overlaying the field of view.

Figure 12 – Flag location for phase 2

Phase 3 – Careless Keeper

Description

Figure 13 – Chapter 1 phase 3 description

This challenge would require some operational knowledge about fprime-gds. The hint that the intern was bragging about his custom approach to telemetry was meant to guide the players towards custom dashboards and scripting. From either the docs or booting up a local instance of fprime-gds, we could find that it allows for custom dashboards to be loaded on the webui.

Figure 14 – Custom dashboard location in the fprime-gds webui

These are saved as a variable “dashboardConfigurationText” in LocalStorage, which is readable by JS. Modifying the payload from phase 1 to now reads that variable and sends it out via the echo command:

And the flag would be readable from the log page as before.

Phase 4 – Shadowstep

Description

Figure 15 – Chapter 1 phase 4 description

The final challenge adds a slight twist to the method used so far. The hint points out that there’s more to their environment and to look further. Since we have full access to the satellite at this point, we can even read the intern’s notes from their custom dashboard. The only thing remaining is the /internal endpoint available from the webpage.

While fprime-gds has CORS enabled, it allows everything from its own domain. While slightly guesstimate, it’s not too big a stretch to think that the website and fprime-gds could be hosted on the same host. Captures, pcap, logs, ports, and endpoints all could be small hints that this is a possibility.

XSS payload

This allows us to create an SSRF payload to first enumerate the `/internal` endpoint from 127.0.0.1:80. This contained a simple directory with 2 files, and the payload could be adjusted to read the contents of each file. The `note` file contains the final flag.

Figure 16 – Obtaining the final flag by retrieving folder and file contents on the /internal endpoint

Take aways

This chapter had a high gatekeeper in the form of the fprime knowledge required to perform the injection. While tricky at first, the goal was to lead the player to investigate how to set up fprime and run through their first tutorial, as well as inspect the traffic between fprime and fprime-gds. After learning more about the fprime protocol and its structure, it became a common web-based challenge that reflected through the satellite for data exfil.

Chapter 2 – Exo Infernum: Black Signal

Figure 17 – Chapter 2 introduction and storyline

This challenge focuses heavily on YAMCS and using standard Red Team methods against a space mission.

Briefly about YAMCS

YAMCS is an open-source software framework for real-time monitoring, control, and data processing in aerospace and related fields. It provides tools for telemetry acquisition, commanding, archiving, and data visualization, allowing operators to track spacecraft, satellites, or other complex systems during missions. The system supports configurable displays, automated procedures, and alarm handling, making it suitable for operations and testing environments. Its modular architecture allows integration with different communication protocols and data sources, adapting to a wide range of mission requirements.

YAMCS is used by organizations that operate spacecraft, satellites, and ground stations, as well as by teams conducting system validation and testing before deployment. Beyond space operations, it is also applied in scenarios requiring real-time system monitoring and control, such as industrial test benches or simulation environments. Its adoption is driven by the need for flexible, cost-effective mission control software to handle small-scale CubeSat missions and more complex satellite constellations.

Phase 1 – The Ghost is Calling Home

Figure 18 – Chapter 2 phase 1 description

You’re given access to a system on port `80`, which provides you with a login screen.

Figure 19 – YAMCS login screen

You should be able to fuzz the following:

`http://localhost/xyz -> 404`

`http://localhost/vault -> 403`

`http://localhost/vault/index.html -> 200`

Figure 20 – Finding and enumerating the hidden Team page

From the Team page, you could use a number of common password list building tools to try to gain access to the applications, or try to use their favorite quotes:

Figure 21 – Team bio containing critical information for password list building

Username: leif

Password: Telemetry is truth

Using these creds, log in to the mission control system:

Figure 22 – YAMCS main page

This indicates that there’s one spacecraft configured there, SV-09. The challenge description shows that this is a Relay Node SV-09 orbiting Velis-12.

At this point, if you didn’t notice it from the URL, you could also confirm that the mission control system is YAMCS (https://yamcs.org).

You should be able to confirm the following:

  • Your system is connected to SV-09 spacecraft
  • Spacecraft is sending Telemetry back

Figure 23 – Current working links to the spacecraft

It looks like you have, in this instance, the commanding capabilities. Since your objective is to get the spacecraft status report, you should find a corresponding telecommand and send it to the spacecraft (hopefully, at this point, you’ve checked the YAMCS documentation to at least understand what the system does):

Figure 24 – YAMCS commanding UI

This should return the status (and the flag) in spacecraft telemetry:

Figure 25 – Retrieving the flag over spacecraft telemetry

Phase 2 –  The Infernum Core

Figure 26 – Chapter 2 phase 2 description

Something happened with your terminal, but your team brought up another one. This one, however, doesn’t have commanding capabilities for the user you managed to log in as in the previous challenge.

Hopefully, you searched for known YAMCS vulnerabilities at this point, and you know there’s a path traversal at your disposal!

Equipped with the information about where the user credentials are stored (based on YAMCS docs), you can grab the content of the `users.yaml` file:

Figure 27 – Using the path traversal vulnerability to obtain the file containing user credentials

To understand the encryption algorithm, examine the `security.yaml`:

Figure 28 – Using the path traversal vulnerability to obtain the file containing the security settings information

Having the usernames, hashes, algorithm, and knowing that the password of the `leif`user was embedded in the team’s profile, you go back to the team’s page and prepare a wordlist. You quickly realize that the hash you got is in hex, while hashcat expects base64 (something to account for):

Figure 29 – hashcat output for the initial cracking attempt

Of course, you could also try to guess the credentials, since you know the username.

With the new credentials, you can now log in as: `administrator` / `Nullsux`.

Now that we have admin access and commanding capabilities, we can try to enable the Infernum Core by sending the appropriate telecommand to the spacecraft:

Figure 30 – YAMCS command UI showing the command to initiate the infernum core

Unfortunately, this command requires a secret to be accepted by the onboard software (otherwise it remains in PENDING state):

Figure 31 – The command requires a secret to activate

Going through the documentation left for us on the mission control system instance:

Figure 32 – Using the previous path traversal to find mission documentation present in the system

You find the Infernum Core Initialization procedure:

Figure 33 – Enumerating the system for relevant information

This indicates that the SPACON (spacecraft controller) keeps their secret in their “secret folder”. So, you find it:

Figure 34 – Enumerating the spacecraft controller secrets

And read the content:

Figure 35 – Finding the secret required to activate the next stage

With this information, we can now enable the Infernum Core:

Figure 36 – YAMCS showing that the command was successfully executed with the correct secret

This, of course, gives you the flag in spacecraft telemetry:

Figure 37 – obtaining the flag from telemetry

You’re also reviewing any other documentation you found with the path traversal (just as any good hacker would)!

Phase 3: Keys to Shadow Kingdom

Figure 38 – Chapter 2 phase 3 description

With the Infernum Core up and running, you know the lost XS Annihilex ship is still alive, and our SV-09 Relay Node can talk to it. You aim to understand how to do this and search for the keys in the ship’s archives.

At this point, you have administrative credentials, the knowledge of path traversal, and the documentation location. You don’t know how to use the relay node to communicate with the ship, so we should review the documentation.

Reviewing `sv-09_infernum_core_module_specs.txt`, we know that the spacecraft can enter SAFE MODE.

Reviewing `sv-09_internal_manual-safemode_initiation.txt`, we know the following:

  • Relay Node in SAFE MODE enables advanced spacecraft reporting
  • The spacecraft enters the SAFE MODE only when 2 out of 3 batteries are off
  • There’s an automated recovery procedure that puts the spacecraft back to its NOMINAL MODE 60 seconds after entering the SAFE MODE

You also see a command to read the file content from XS:

Figure 39 – YAMCS ui with a command to read XS File Content

You’re guessing it is about the XS Annihilex ship. But you don’t know which file to get.

Asking for a random file, we see which new telemetry parameter reports the content:

Figure 40 – Command takes a file path as an argument

Figure 41 – Command telemetry output

Let’s ask again for the spacecraft status report. You see that the spacecraft is not in SAFE MODE:

Figure 42 – Spacecraft status report telemetry

From the telemetry parameters, you can see that all three batteries report some voltage:

Figure 43 – Spacecraft battery telemetry parameters

You also see that there’s a way to disable the battery with a specific telecommand:

Figure 44 – Switch_battery_off command arguments

This command takes the battery #, which is a value between one and three (you guessed it’s the battery id). The command also takes a secret, and you assume that the secret is the one you’ve found in the previous challenge.

Unfortunately, it doesn’t seem to work.

You review the documentation and the procedures again  (`sv-09_infernum_core_initialization_procedure.txt` specifically), and you see that the secret is generated weekly.

You go back to the path traversal vulnerability and find an email that contains the secret for this week:

Figure 45 – Using the path traversal to enumerate communications within the system further

Let’s try to disable two batteries, which should put the spacecraft in SAFE MODE:

Figure 46 – Disabling two batteries to engage safe mode

Once you disable two batteries, you send another command to get the status report.

You, of course, remember the auto-recovery procedure, which puts the spacecraft back in NOMINAL mode, so you hurry (perhaps even automate the process)!

The spacecraft status reports give you the file location:

Figure 47 – File location is revealed in the spacecraft status report telemetry

You request the content:

Figure 48 – Retrieving the flag from the XS File

And it gives you back the flag.

Phase 4 – Understanding came. So did screams

Figure 49 – Chapter 2 phase 4 description

You get another job – understand the unknown telemetry that suddenly started coming from the XS Annihilex ship. You notice many new telemetry packets you didn’t see before, called `DeepSpaceData`. You load all of them and export to a file.

Figure 50 – List of unknown telemetry packets

You look at the file and understand its content, which is as follows:

Figure 51 – Telemetry packet structure

This looks like the content of multiple files split into packets, which were then stitched together when you exported all as one raw telemetry file.

You process it accordingly, and you get the sound files:

Figure 52 – Automating the reconstruction and recovery of the files from the telemetry packets

Of course, you listen to all the files to see when weird things start happening. At the same time, you very much enjoy the music.

Looking closer at blocks 1 and 3, you might notice that they have some weird noise at the start of the track. So you take the first N points from the left channel, and map positive values to 1, non-positive to 0, and can figure out that there is an ASCII message in one of them, “Within me a key.Gather the artifacts.Arise!”.

Then you notice that block three contains a block of noise with exactly the same length, so you XOR two-bit arrays at the bit level and return the result as a flat array of bits (0 or 1).

This gives you the flag:

Figure 53 – Extracting the final flag from the sound files

As for block two, not used at this point, you noticed something strange in the middle of it, and opened it in Audacity to take a closer look at the spectrogram. There you find this:

Figure 54 – Spectrogram detail of the unused soundtrack file

But where to use it? Well, some mysteries are still not ready to be revealed.