Disobey 2024 was a blast! Now that everyone has hopefully survived back home and the worst withdrawal symptoms have past, it’s a good time to take a look back on the Gofore CTF challenges and see how they could have been solved.
In this blog post I’ll go through the four challenges step-by-step and hopefully give some closure to anyone who struggled with them.
But before we get to it, I want to praise our puzzle masters Anzhelika Kettunen, Lassi Riihelä and Riku Järvinen, who created these challenges, and Katarina Partti, who shared valuable insights on what makes a good CTF challenge and designed Seppo’s PI table at the venue.
There was a story linking the challenges together, but each challenge was a standalone puzzle. As something has to go sideways, the story was not visible to the competitors anywhere in the event, so I’ll quickly recap the story here.
Last year (Disobey 2023) Seppo made an epic mistake that cost him his job. Dismissed and disillusioned, he embraces the shadows, becoming a private investigator in search of redemption. PI Seppo Sorsa rose from the ashes and found a clue to something massive.
His latest pursuit: an enigmatic tractor conglomerate, shrouded in whispers of deceit. But as Seppo delves deeper, the company’s secrets close in around him.
Then, he vanishes without a trace. Yet, in the darkness, clues linger like echoes of a forgotten song. Can you decipher the cryptic messages he left behind? Is it possible to unravel the mystery and find Seppo before it’s too late? What on earth have tractors to do with anything?
The flags are in the format: GFLAG{…}.
Now that we know what we are looking for, let’s start going through the challenges.
Stegano
(This challenge was supposed to be called ‘Hidden Messages, Hidden Truths’, but copy-paste was not stronk.)
Seppo found out that Gonsse is developing a new tractor which is much quieter than the previous ones. He was able to secretly record the sound. An audio file ‘what_tractor_is_this.wav’ was attached to the challenge.
The challenge name gives us a hint that this is a steganographic challenge. In other words, there is likely something more than just the audible audio hidden in the file. Let’s start by checking that the file actually is a sound file:
Looks good. We can then listen to the file. The track sounds like some kind of an engine, but there is a high-pitched extra sound in it. Let’s investigate that using audio editor Audacity.
The normal view does not tell us much:
Let’s switch to spectrogram mode and see if there is something there.
There we go, looks like a starting part of a flag: GFLAG{MUCH_AUDIO. But as we know, the flag should end in a curly bracket, so there should be something more. Let’s see if we can get something out of the file with steghide info.
Nothing new there, and we would need a passphrase to try to extract anything hidden in the file. Audio files can have metadata, or tags, in them. Let’s check if this file has any.
Let’s use MusicBrainz Picard for checking the tags. For command line-oriented individuals, another option could be Mutagen. When we open the file in MusicBrainz Picard, we can see there is a tag called ‘interesting’, that says ‘iliketractors’:
Let’s try that as a passphrase for steghide.
Steghide manages to extract a file named part2.txt, which has the ending of our flag: _SUCH_FLAG}.
Combining the two parts gets us the final flag: GFLAG{MUCH_AUDIO_SUCH_FLAG}.
Journey into the 8-Bit Abyss
As Seppo was investigating Gonsse he saw a lot of game development books lying around. Could Gonsse be branching out to game development as well?
This time, there’s a file called ‘buggyinvaders’. The first step is to find out what kind of file it is. Fortunately, the file command helps us with this. The file is a NES ROM image. Seems like we get to learn something about the original Nintendo Entertainment System!
As most of us do not carry our NES consoles with us, we will need an emulator. There’s two options: Mesen, and FCEUX. Here I will be using FCEUX.
The game seems to start normally, but when we try to start the gameplay, nothing happens on the screen and the music gets stuck. It seems the file name did not lie. Let’s open the debugger and see what happens in the program code.
Using the debugger, we can see that the program is stuck in an endless loop, where it checks that the value on memory address ‘$00’ matched the hex value of 43 and moves back to the beginning if it does not. We can use the emulator hex editor to manipulate the memory and add the needed value on the correct position:
The following instructions do the same thing, just the memory address and the value to compare to change. Going through the same steps, we find out that the first 5 memory address need to have values 43 48 45 41 54 in them. Or ‘CHEAT’ in plain text. When the values are added to memory, we can see that the game starts and the beginning of the flag is rendered on the game screen: ‘GFLAG{SUPER’.
But how about the second part? Maybe it’s visible after you play the game for a bit? But nobody has time for that, we have a bunch of other challenges to figure out too. Maybe there is a way to check out all the graphical components of the game?
From the debug-menu we can find a tool called PPU Viewer, and PPU seems to be an acronym of Picture Processing Unit. Most of the stuff seems to be normal graphical assets needed in the game, but the end of the list seems to have a message for us: ‘REST OF FLAG: YAYNES}’.
There we go, now we have the whole thing: ‘GFLAG{SUPERYAYNES}’.
Threads of Deception
Gonsse has developed an Intelligent Industry IoT Solution for measuring and keeping track of the TRACTORS. Seppo has found the Dashboard at [IP:port].
When the provided address is opened on a browser, a web page hosting the dashboard can be found. The site itself seems pretty simple; the main page has some fancy slide-show functionality. Inspecting the page HTML does not reveal anything interesting. The Dashboard-page seems to retrieve the sensor data from API endpoints:
Quick test with a browser confirms that the API exists:
As the API is our best lead at the moment, let’s see if we can find some other endpoints. Time to FUZZ! Using FFUF and SecList API endpoint lists we can’t find anything new. The only hit we get is ‘temp’, which we already knew.
Ok, so no luck there. But maybe there’s some other service to be found from the web server. Let’s fuzz for other content next using SecList’s Web-Content discovery list common.txt.
Ok, a couple of new things: git and gitweb. Let’s try to open them in the browser.
Ok, there’s something, but we need to get around the authorization check. As there is no apparent way to provide credentials for the service, maybe the authorization is in some other way. One option would be some header that relays something that will be used for authorization to the service.
Let’s investigate the page again, maybe we could find something that we could use. One other thing we saw in the content fuzzing results, was img, a typical name for a folder storing images on web sites. Opening that folder with a browser shows us three files, the images used in the front-page slideshow component. The URLs for these images are also visible on the page HTML code.
The third image has a laptop with its screen visible:
Zooming the picture and checking out the screen shows us a username and an IP address the user is trying to connect to, with the last 5 digits missing: jumper@172.16.1XX.YYY.
Ok, this hints us that there might be a jump server that might provide access to the git service. Maybe we could bypass the authorization by providing the IP address in a HTTP request header. Seems like we will be fuzzing some more. First, we need the correct header, and secondly, we need to go through all the possible IP addresses that start with 172.16.1XX.YYY. The first part has two digits, so numbers 00…99, and the second one has three, so the full octet 0…255.
For headers we can use the SecList’s http-request-headers-common-non-standard-fields.txt list. For the IP address octets, we are going to need separate lists. Fast way to create the lists is using ‘seq’ and saving the result into separate files:
$ seq 0 99 > octet-one.txt
$ seq 0 255 > octet-two.txt
Now that we have the wordlists set up, we can let ffuf take over and go get some coffee. Note, that fuff will generate 870 400 requests with these settings, so filtering the results is highly recommended.
After some time and staring into the void, we finally got a hit: the header needs to be ‘X-Forwarded-For: 172.16.199.252’. Time to break out Burp Suite and check what we can get as a response. Using Burp interceptor:
With this we get into the git folder, which has two folders:
The api folder has a source code for the API. It seems that the API makes a POST call to another port on the server, but that port seems to be open only to the loopback address of the server.
From the nginx [n-ginks] folder we find a nginx.conf file, that tells us how the http server routes incoming traffic. There’s a /assets endpoint, that uses a regex matching pattern to the request and passes the results directly to a proxy pass function.
Putting these two facts together we might be able to use the /assets endpoint to carry out a Server-Side Request Forgery attack against whatever service the API endpoints are calling.
First testing with same kind of a call that the site makes:
Let’s then provide something else in the data part:
Ok, so the endpoint we are calling seems to be using cat to read files from the server file system, and more specifically from a folder named /opt/server/. And whatever we send in the request will be concatenated to the command. Let’s assume that there is no sanitation for the inputs, so it’s likely that we can use path traversal to move to almost anywhere in the file system.
I’ll assume that the flag file is called ‘flag’. For the path traversal I’ll create a wordlist that has a couple of traversals to common linux directories:
Then we can fuzz once more:
Seems that ‘../../flag’ returns something. Let’s curl it:
There we go: ‘GFLAG{cr1t1c4l_pr0xy_m1sc0nfigurations}’.
Into the Heart of the Machine
Seppo has gone missing while investigating Gonsse and the only trace of him is the ECU on the table.
To the final challenge! This challenge had a physical component to it, there was a table with all kinds of stuff on it in the corner of the CTF space. The ECU (Engine Control Unit) with three connectors was obviously the thing we would need to get into, but how? There were two items on the table that would help us with this.
First, Seppo’s notes, that gave a list of things to do:
The correct drivers for the PCAN-USB adapters were needed. Those could be found from the PEAK Systems webpage by searching for ‘pcan usb driver’.
It seemed Seppo had written some code already, and the github repository URL was on his notes:
The repo had a 7z-file in it, but when the file was downloaded, we found out that it is password protected. There is a password in Seppo’s notes but using it as a password did not open the file. This is where the second interesting item on the desk comes in: the keyboard with both Latin and Cyrillic letters.
Using the keyboard, the password can be decoded to: ‘gfhjkm(‘. This password opened the 7z-file, and revealed two python scripts. PCANBasic.py was a basic API implementation by the PCANUsb-adapter provider, and the second one was Seppo’s script for reading the ECU values.
In his notes, Seppo had noticed that Vehicle Model and Vehicle Identification Number DID values were corrupted. That hints us what to look for.
In the beginning of the python script, there are several example DIDs listed and the values we were interested in could be found there:
If the code was run against the ECU as it was, nothing meaningful would come out. Couple of things had to be fixed:
First the can_id and the used bitrate had to be fixed. Both values could be found written on the ECU itself:
can_id was defined in the beginning of the file. Below it, the two different bit rates were defined to variables:
After fixing can_id, the used bit rate was found on the send_can_message() function. In the original script file bit rate was set to 500k, so that needed to be changed to 250k:
After the connection configuration was fixed, it was time to set the message to be sent to the ECU. The data is set as a hex array. The first value defines the message length, the second one represents the request operation and last two define the DID. Message length and request values were already set correctly, which left the DID values to be fixed:
We needed to read the DID values Seppo mentions in his notes. First 0xF1A/:
Received data was presented in hexadecimal, so we needed to convert that to ASCII with CyberChef:
The ASCII seemed to be readable text with some extra data. After clean-up we used a small moment to be outraged by this spiteful message:
From the second DID (0xF190) we got this:
Converting the Hex values to ASCII gave us this:
And after cleaning out the padding, we got:
There’s the final flag: GFLAG{HeRE_It_iS}.
But wait, what happened to Seppo?
We found a bunch of flags, but none of them give us any information about Seppo’s whereabouts. Did Gonsse, the tractor company, find him before he found what he was looking for? Or did he find something that scared him to go into hiding? Someone of his neighbors had seen a white flash from his apartment before he vanished. Maybe Gonsse was working on some kind of a highly advanced logistics solution, like a teleportation device or a time machine?
Maybe we can get some answers to these questions in Disobey 2025. See you there, hackers!