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 trueand 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.
defvalidate_server(validation_server):try: date =validate_access(validation_server)return date >= NEW_FEATURE_RELEASEexceptExceptionas e:print(f"Error: {e}")returnFalse
The validate_access function will first get the public key from the /pubkey endpoint on the specified validation_server.
defvalidate_access(validation_server): pubkey =get_pubkey(validation_server)try: response = requests.get(validation_server) response.raise_for_status() data = response.json() date = data['date'].encode('utf-8') signature =bytes.fromhex(data['signature']) verifier = DSS.new(pubkey, 'fips-186-3') verifier.verify(SHA256.new(date), signature)returnint(date)except requests.RequestException as e:raiseException(f"Error validating access: {e}")
defget_pubkey(validation_server):try: response = requests.get(f"{validation_server}/pubkey") response.raise_for_status()return ECC.import_key(response.text)except requests.RequestException as e:raiseException(f"Error connecting to validation server for public key: {e}")
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.
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 the validation_server from the preferences cookie.
Base64 decode the preferences cookie, add validation_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 /pubkey endpoint 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.
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.