文章总结: 本文揭示AWSSDK客户端在错误处理中将凭据设为nil时会回退使用EC2实例IAM角色的安全风险,通过真实漏洞案例展示攻击者如何利用S3数据导入功能读取内部私有桶数据。文章详细分析了三种凭据初始化方式的差异,并为安全审计和开发人员提供了具体的检查建议,强调避免凭据回退到高权限系统角色的重要性。 综合评分: 85 文章分类: 云安全,漏洞分析,实战经验,安全开发,应用安全
当 AWS SDK 客户端跌落系统角色——一个被忽视的提权陷阱
幻泉之洲
2026年5月17日 09:26 北京
在小说阅读器读本章
去阅读
云安全不止是看 S3 桶有没有公开、VPC 隔没隔离。真正致命的问题往往藏在应用如何调用云服务的逻辑里面。这篇文章通过一个真实的“从 S3 导入数据”漏洞,拆解 AWS SDK 凭据初始化的坑:一旦错误处理把凭据设为 nil,客户端会顺着凭据链一路滑到底层 EC2 实例的高权限 IAM 角色,直接让攻击者读走所有内部桶。文章还给出了安全审计和开发中必须盯紧的三条检查点。
先问几个不一样的问题
说起云安全,大部分人一上来就会问:基础设施怎么配的?有没有公开桶?VPC 隔离了吗?IAM 角色用得对不对?
做了这么多年应用安全,我们更关心另一类问题:这个平台到底用了云厂商的哪些服务?哪些服务直接嵌在业务逻辑里?Web 应用怎么调用这些服务的?普通用户有没有机会摸到这些调用?云服务和平台逻辑搅在一起,会不会冒出意料之外的行为?
顺着这些问题往下挖,基本都能找到 bug。
这就是我们开这个“CloudSecTidbits”系列的原因。每期分享一个在云安全测试里发现的真实漏洞,而且前提往往是基础设施配置本身没有问题,出岔子的是应用层对服务的调用方式。每篇文章都会附带一个可部署的 Terraform 实验环境[1],方便你亲手复现。
▲ CloudSecTidbits 系列:当基础设施配置正确,但应用层把事情搞砸了
一个导入 S3 的功能,怎么就成提权口子了
AWS 提供的 SDK 很丰富,几乎所有语言都能用。但你要用它调 AWS 的服务,总得先告诉它是用什么身份在操作——也就是凭据怎么传。
AWS SDK 在初始化客户端的时候,如果你不显式指定凭据来源,它会按照一套固定的“凭据提供程序链”自动去找。不同语言的 SDK 链的顺序可能稍有区别,以 Go 的 SDK 为例:
- 环境变量
- 共享凭据文件
- 如果跑在 ECS 任务上,就用任务的 IAM 角色
- 如果跑在 EC2 实例上,就用 EC2 绑定的 IAM 角色
链上哪个先拿到有效凭据就用哪个。下面的代码片段展示了 SDK 如何遍历这条链:
func (c *ChainProvider) Retrieve() (Value, error) { var errs []error for _, p := range c.Providers { creds, err := p.Retrieve() if err == nil { c.curr = p return creds, nil } errs = append(errs, err) } c.curr = nil var err error err = ErrNoValidProvidersFoundInChain if c.VerboseErrors { err = awserr.NewBatchError(“NoCredentialProviders”, “no valid providers in chain”, errs) } return Value{}, err }
现在我们来看一个具体的漏洞。我们在测试某个 Web 平台时,发现它用 AWS SDK for Go (v1) 实现了一个“从 S3 导入数据”的功能。用户可以选择两种模式:
- 只给桶名——从公开桶导入;
- 给桶名加 AWS 凭据——从私有桶导入。
核心逻辑大致长这样:
func getObjectsList(session *Session, config *aws.Config, bucket_name string){ S3svc := s3.New(session, config) objectsList, err := S3svc.ListObjectsV2(&s3.ListObjectsV2Input{ Bucket: bucket_name }) return objectsList, err }
func importData(req *http.Request) (success bool) { srcConfig := &aws.Config{ Region: &config.Config.AWS.Region, } req.ParseForm() bucket_name := req.Form.Get(“bucket_name”) accessKey := req.Form.Get(“access_key”) secretKey := req.Form.Get(“secret_key”) region := req.Form.Get(“region”) session_init, err := session.NewSession() if err != nil { return err, nil } aws_config = &aws.Config{ Region: region, } if len(accessKey) > 0 { aws_config.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, “”) } else { aws_config.Credentials = credentials.AnonymousCredentials } objectList, err := getObjectsList(session_init, aws_config, bucket_name) …
用户没给凭据时,代码用了 credentials.AnonymousCredentials,本该以匿名身份去访问公开桶。但有意思的地方在错误处理:
if err != nil { if err, awsError := err.(awserr.Error); awsError { aws_config.credentials = nil getObjectsList(session_init, aws_config, bucket_name) } }
一旦 ListObjectsV2 返回错误,这段逻辑会把 aws_config.credentials 直接设为 nil,然后重试。这一下,事情的性质就变了。
▲ 错误处理中显式将凭据置 nil,触发凭据链回退
凭据置 nil 之后,权限直接拉满
当 aws_config.credentials = nil,SDK 内部就会启动我们前面说的凭据提供程序链。既然应用跑在 EC2 上,链的尽头就是那个挂给实例的 IAM 角色。在我们的案例里,这个角色拥有对所有内部 S3 桶的完全访问权限。
攻击路径就清晰了:
- 如果平台在客户端流量中暴露了内部桶名(这事其实很常见,内部数据处理经常在前端请求里露出桶名),攻击者直接拿这些桶名作为“导入 S3”功能的输入,就能在界面里堂而皇之浏览内部桶的所有内容。
▲ 从 Burp Suite 历史记录中提取到的内部桶名列表
就这一步,私有桶数据全被“导入”成了用户自己的数据。基础设施配置没毛病,桶没有公开,IAM 角色策略也没写错,可应用层一个不信道的错误回退逻辑,直接让系统角色替攻击者干了脏活。
三种凭据初始化方式,坑位就在这
AWS SDK 客户端初始化时,Session 对象里夹带的 Credentials 对象怎么设,直接决定代码走哪条路。我们把这三种方式掰开看。
NewStaticCredentials
最直白的方式。把 AccessKey、SecretKey 甚至 SessionToken 硬传进去。当然,谁也不该把凭据写死在代码里,但从逻辑角度讲,这是控制力最强的一种。
var session = session.Must(session.NewSession(&aws.Config{ Credentials: credentials.NewStaticCredentials(“AKIA….”, “Secret”, “Session”), Region: aws.String(“us-east-1”), }))
{ nil | 未指定 } Credentials 对象
不传 Credential 对象,或者显式传 nil,效果一样——SDK 走上凭据提供程序链,最终大概率拿到实例角色的高权限身份。
// 不传凭据 svc := s3.New(session.Must(session.NewSession(&aws.Config{ Region: aws.String(“us-west-2”), })))
// 显式传 nil svc := s3.New(session.Must(session.NewSession(&aws.Config{ Credentials: nil, Region: aws.String(“us-west-2”), })))
这就是前面漏洞案例里发生的事情。开发者可能完全没意识到,一行 credentials = nil 能把权限从“匿名”一把拉到“实例角色全权”。
AnonymousCredentials
AWS 官方文档写得很清楚:这个对象就是给不需要签名请求的场景准备的,比如访问公开的 S3 桶。它本质上就是一个空的静态凭据对象:
var AnonymousCredentials = NewStaticCredentials(“”, “”, “”)
在公开资源访问的场景下,该用这个就用这个。但如果代码里有额外的错误处理逻辑去覆盖它,把它设成 nil……结果跟上面第二种一样,又跌回系统角色了。
审计和开发,各记几条顺手的检查项
给云安全审计的人
别只盯着桶策略和 IAM 角色绑了啥。审计那种跟云服务深度集成的 Web 平台时,看到任何一处 AWS SDK 客户端初始化的代码路径,拿出这三个问题对着来:
- 这个代码路径是不是用户能直接触达的?比如用户页面上填个凭据就能触发导入导出,或者传个公共资源链接就让平台去抓。
- 客户端的凭据是怎么初始化的?是走的凭据提供程序链——那就要查链上最终拿到的机器角色权限有多大;有回退条件吗?用户能不能通过特定输入走进那个回退分支;默认就走链的情况,直接去扒那个角色的权限。
- 用户能不能滥用这个功能,让平台用高权限身份去访问内部私有资源?就像“从 S3 导入”被滥用去读内部桶那样。
给开发者
处理公开资源时,老老实实用 AnonymousCredentials。AWS 文档说了,用它发出去的请求不会签名,碰到不接受未签名请求的服务自然会报错。
如果你的平台允许用户提供凭据去对接外部云服务,千万别设计那种“用户凭据不行就回落系统角色”的回退模式。保证用户提供的凭据按预期设进去,绝不能出现把 aws.Config.Credentials 干成 nil 的路径——不然客户的 SDK 客户端就会替你走凭据链,捡起你服务器的 IAM 角色去干活。
一条简单的原则:任何时候,只要
Credentials字段变成 nil 或者没设,就要立刻警觉——你的代码正在把身份交给运行环境去决定,而那个身份往往比你想象的大得多。
动手试试
照例,我们在 GitHub 上用 Terraform 搭了一个有漏洞的实验环境[1],复现整个“导入 S3 功能跌落系统角色”的场景。部署起来很简单,试过你就知道这种 bug 在实际环境里多容易踩到。
这个系列会继续写下去,每次解剖一个云和 Web 搅在一起生出的安全问题。下一期见。
参考资料
[1] https://github.com/doyensec/cloudsec-tidbits/
[2] https://blog.doyensec.com/2022/10/18/cloudsectidbit-dataimport.html
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:幻泉之洲 《当 AWS SDK 客户端跌落系统角色——一个被忽视的提权陷阱》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论