Cloud Nine
Advanced Track
Scenario
My Dearest Hacker,
This Valentine's Day, Cupid has gone digital with Cupid's Arrow - a revolutionary web application that lets users shoot virtual arrows across a world map to forge connections between people. But Cupid has had a change of heart.
Tired of playing matchmaker, the legendary deity has gone rogue and twisted their own creation into something sinister. What was meant to spread love is now being weaponized to break relationships apart. Couples worldwide are mysteriously drifting apart after their locations are targeted on the map, and Cupid is watching gleefully from above.
Your mission: Investigate the Cupid's Arrow application, discover how this fallen angel is manipulating the system, and find the flag hidden in Cloud Nine - Cupid's secret administrative sanctuary where all relationships are controlled.
Can you outsmart a rogue deity and stop this Valentine's Day catastrophe? Or will you fall victim to Cupid's corrupted arrows?
http://54.205.77.77:8080/
Summary
Summary
In Cloud Nine we begin by attacking a Flask-based web application running on port 8080. Initial testing shows no SQL injection on login, but brute-forcing default credentials reveals access as guest. Inspecting the Flask session cookie exposes that it contains serialized JSON with user and admin fields. Directory enumeration uncovers /status/check, which is vulnerable to SSRF. By querying the EC2 task metadata endpoint (169.254.170.2), we discover the public ECR image used to build the application. Pulling and running the container locally reveals the Flask secret_key, allowing us to forge an admin session cookie using flask-unsign and gain access to the /admin panel.
Inside the admin panel, we identify a DynamoDB-backed user management interface using PartiQL. The lookup query directly concatenates user input into the statement:
This enables PartiQL injection. Although automated tools like sqlmap do not support PartiQL, manual boolean-based blind injection succeeds. By leveraging DynamoDB functions such as begins_with(password, '<prefix>'), we build a character-by-character extraction method. Using crafted payloads that trigger different application responses (“User loaded” vs. “User not found”), we enumerate usernames and reconstruct user passwords through blind prefix testing.
Automating the process with a custom extraction script allows us to recover multiple credentials including the flag.
In Cloud Nine, we have specified the web service on port 8080. We visit the page and see a login screen. Initial tests for SQL injection show no effect.

User enumeration is not possible based on the messages displayed when an incorrect entry is made.

Access as guest
The next thing we can try is default credentials, such as admin:admin. We intercept a login request to derive our hydra command from it.

We use the following hydra command and are able to get the credentials fpr the user guest.

We log in, but can't find anything on the dashboard. We do not have access to the admin panel.

Access as gues (admin privileges)
We look at our session cookie, which is a Flask cookie...

... that contains our user and role.

Since we cannot find any other pages besides the login page, we will first try a directory scan. The pages /status, /status/env, and admin stand out here. Unfortunately, rate limiting failed. We could have seen more here, but more on that later.

Via /status/env, we can see the hostname of the machine ip-172-31-93-102.ec2.internal. The hostname indicates that this is an AWS EC2 instance.
That doesn't help us yet. If we already had internal access, we could access the AWS metadata of the instance and possibly obtain valuable environment variables.
AWS metadata is available via 169.254.169.254, a link-local address that can only be accessed within the EC2 instance.

We perform another directory scan on status. Previously, we saw that our requests were limited. We will proceed directly to bnei /status. And we have a hit on /status/check

When visiting the site, we are asked to provide a URL as a parameter.

We ask directly for the metadata as mentioned before.

And we see a public image.
We pull this...
... and run it in a safe environment to interact with it. In the source, we discover the secret that is used to sign the flask cookies. This could give us access to the admin panel.

We also find another username and password, possibly the actual initial access to the web application and the first flag.

We use Flask-Unsing to build an admin cookie using the secrets we found.

We replace our session cookie with the one crafted, reload the page...

... and visit the admin page.
Data Exfiltration
Here we have a user control.We can load users and edit their profiles. From the source previously gathered, we see that the app uses DynamoDB to store user data. To query DynamoDB PartiQL is used a AWS's SQL-like language for DynamoDB:
Furthermore, we also see the second flag.

We ceck for non existing users...

... and existing ones and see different behaivoir.

When we test for SQL injection, we receive a server error, which strongly suggests that it is vulnerable to SQLI.


We capture the request for sqlmap.

We also receive proof that the site is vulnerable to Boolean-based blind injection. However, SQLMap does not support PartiQL, so we must proceed manually.

We create an SQL injection framework that still outputs the valid user guest
payload:
original query:
resulting query

How can we leverate this to enumerate further users. We could query for the next smallest username after X. Unfortunately, we do not receive the usernames directly, but we do receive the emails from which we can derive them. After each username, we replace X with the username we believe we found in the email.
We start with an empty user... and find bsmith.

We move on with bsmith and find demo.

With guest we are able to detect cupidtest.

The following username was subsequently checked from the variants of the usernames from the email. The user cupid is also a valid candidate.

With the error-based approach, we could now test the entries in the user database.
We have the following query from the source:
So we still inject into
With
The query becomes
Probing if the passwords starts with the given token for username guest.
If guest password starts with g → condition true → row returned → "User loaded."
If it does NOT start with g → condition false → no row → "User not found."


From this approach, we generate a script that ultimately probes the password based on errors. The script performs blind boolean extraction of a user's password by abusing a DynamoDB PartiQL injection in the username parameter. It incrementally tests prefixes using begins_with(password, '<prefix>') and detects correctness by checking whether the application returns User loaded reconstructing the password character by character.
We run the script for our guest account and are able to retrieve the password.

We do this also for the ther accounts and it turns out that the password for the user cupid is actually the third flag.

Last updated
Was this helpful?