Feature Unlocked
Writeup for Feature Unlocked (Web) - CyberSpace CTF (2024) ๐
Video walkthrough
Description
The world's coolest app has a brand new feature! Too bad it's not released until after the CTF..
I decided to make web challenge for this CTF. The first part was inspired by a recent challenge I completed in the Wani CTF; one day, one letter so shout-out to the creator, KowerKoint ๐
Solution
Site functionality
When we open the website, we are welcomed to the "World's Coolest App".
If we check the status of the "new feature", we'll see a countdown timer indicating that the feature will be unlocked in 7 days time (after the CTF has ended).
Players may dig into the JavaScript, but it's merely an animation - the releaseTimestamp is provided by the server.
Source code
The challenge comes with source code, but let's break it down rather than dumping it all here.
Starting with the /release route in main.py, we see that we need a valid access_token to access the new feature.
So, how is the date validated? In the code above, you'll see the validation_server is hardcoded into the application but if the debug GET parameter is set to true and the preferences cookie contains a validation_server, it will be used instead. These elements are defined at the top of the script.
Breaking down the validation, we first have a validate_server function that returns whether or not the current date is greater than (or equal to) the scheduled release date.
The validate_access function will first get the public key from the /pubkey endpoint on the specified validation_server.
Next, it will issue a request to the / endpoint on the validation_server and use the public key to verify that the returned date signature is valid.
The validation_server is running on a different port and will generate a private/public keypair, then sign the current date with it.
Before formulating our attack plan, let's look at the final piece of the puzzle; the /feature route that will be unlocked.
It's a simple word-counting feature that takes user input and passes it directly to a command without sanitisation.
Exploit
Based on our initial review of the application, we can outline the following plan of action:
Set
debug=true, so the server will attempt to read thevalidation_serverfrom thepreferencescookie.Base64 decode the
preferencescookie, addvalidation_server: https://ATTACKER_SERVER, and re-encode.Generate a public/private keypair on the attacker server.
Configure the attacker server to sign an arbitrary date with the private key and return the corresponding public key when the
/pubkeyendpoint is requested.Once granted access to the new feature, probe for command injection vulnerabilities.
Forging a signature
Let's start by configuring the attacker server. We can generate a keypair with the following script.
We configure server.py to do the same thing as the real validation_server, except that the returned date will be 7+ days in the future.
Start the server, then use some service like ngrok to expose the local port 1337 to the internet.
Update the preferences cookie, e.g.
Finally, visit the challenge URL with the debug GET parameter set to true.
We get a hit in the server logs!
The feature is now unlocked!
Command injection
If we visit the new feature endpoint, we'll find the [much anticipated] word counting feature.
If we provide some input, we'll see it does indeed count the words.
Since it only returns a decimal value as the output, any command injection vulnerability will be "blind" so we will need to find a way to exfiltrate data.
One common way to do this is to find a writeable web directory, write the command output (or copy files), and browse them directly.
Another option is using standard tools to return the data, e.g., curl. The following input will convert the flag to base64 (so we don't lose special characters) and then make a HTTP request to our attacker server, with that encoded value as a GET parameter.
We can recheck our server logs to find the base64 encoded flag ๐
Flag: CSCTF{d1d_y0u_71m3_7r4v3l_f0r_7h15_fl46?!}
Last updated
