当AWSSDK客户端跌落系统角色——一个被忽视的提权陷阱

admin 2026-05-19 06:20:38 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文揭示AWSSDK客户端在错误处理中将凭据设为nil时会回退使用EC2实例IAM角色的安全风险,通过真实漏洞案例展示攻击者如何利用S3数据导入功能读取内部私有桶数据。文章详细分析了三种凭据初始化方式的差异,并为安全审计和开发人员提供了具体的检查建议,强调避免凭据回退到高权限系统角色的重要性。 综合评分: 85 文章分类: 云安全,漏洞分析,实战经验,安全开发,应用安全


cover_image

当 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 为例:

  1. 环境变量
  2. 共享凭据文件
  3. 如果跑在 ECS 任务上,就用任务的 IAM 角色
  4. 如果跑在 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 客户端初始化的代码路径,拿出这三个问题对着来:

  1. 这个代码路径是不是用户能直接触达的?比如用户页面上填个凭据就能触发导入导出,或者传个公共资源链接就让平台去抓。
  2. 客户端的凭据是怎么初始化的?是走的凭据提供程序链——那就要查链上最终拿到的机器角色权限有多大;有回退条件吗?用户能不能通过特定输入走进那个回退分支;默认就走链的情况,直接去扒那个角色的权限。
  3. 用户能不能滥用这个功能,让平台用高权限身份去访问内部私有资源?就像“从 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 客户端跌落系统角色——一个被忽视的提权陷阱》

评论:0   参与:  0