AWS SDK客户端系统角色回退风险:从S3导入功能的安全漏洞分析

本文深入分析AWS SDK Go客户端在凭证初始化时的安全风险,揭示当用户未提供凭证时系统自动回退到高权限IAM角色的漏洞,并通过实际代码案例展示如何利用该漏洞访问内部S3存储桶。

系列介绍

在云安全领域,人们通常首先关注以下问题:

  • 基础设施如何配置?
  • 是否存在公开的存储桶?
  • VPC网络是否隔离?
  • 是否使用正确的IAM设置?

作为应用安全工程师,我们认为更有趣且与上下文相关的问题包括:

  • 使用了云供应商提供的哪些服务?
  • 在使用的服务中,哪些直接集成在Web平台逻辑中?
  • Web应用程序如何使用这些服务?
  • 它们如何组合以支持内部逻辑?
  • 服务的使用是否曾暴露给最终用户或可被其访问?
  • Web平台内是否存在由云服务引起的意外行为?

通过回答这些问题,我们通常会发现漏洞。今天我们推出“CloudSecTidbits”系列,分享关于这些问题的想法和知识。

CloudSec Tidbits是一个博客系列,展示Doyensec在云安全测试活动中发现的有趣漏洞。我们将重点关注云基础设施配置正确但Web应用程序未能正确使用服务的情况。每篇博客将讨论由Web和云相关技术不安全组合导致的特定漏洞,并包含一个可轻松部署的基础设施即代码(IaC)实验室,用于实验所述漏洞。

Tidbit #1 - AWS SDK客户端陷入系统角色的危险

Amazon Web Services提供全面的SDK以与其云服务交互。首先我们检查凭证如何配置。AWS SDK要求用户传递访问/秘密密钥以验证对AWS的请求。根据不同的用例,凭证可以以不同方式指定。

当AWS客户端初始化时未直接提供凭证源,AWS SDK使用明确定义的逻辑。AWS SDK根据基础语言使用不同的凭证提供者链。凭证提供者链是一个有序的源列表,AWS SDK将尝试从中获取凭证。链中第一个返回凭证而无错误的提供者将被使用。

例如,Go语言的SDK将使用以下链:

  1. 环境变量
  2. 共享凭证文件
  3. 如果应用程序使用ECS任务定义或RunTask API操作,则为任务的IAM角色
  4. 如果应用程序在Amazon EC2实例上运行,则为Amazon EC2的IAM角色

以下代码片段显示SDK如何检索第一个有效的凭证提供者:

来源:aws-sdk-go/aws/credentials/chain_provider.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Retrieve返回凭证值或错误,如果没有提供者返回而无错误。
//
// 如果找到提供者,它将被缓存,并且对IsExpired()的任何调用
// 将返回缓存提供者的过期状态。
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
}

在初步了解AWS SDK凭证后,我们可以直接进入tidbit案例。

面向用户功能中的不安全AWS SDK客户端初始化 - 从S3导入案例

通过测试多个Web平台,我们注意到从外部云服务导入数据是一个经常出现的功能。例如,一些Web平台允许从第三方云存储服务(如AWS S3)导入数据。

在这个特定案例中,我们将重点关注在一个使用AWS SDK for Go (v1)实现“从S3导入数据”功能的Web应用程序中发现的漏洞。用户能够通过提供以下输入使平台从S3获取数据:

  • S3存储桶名称 - 从公开源导入案例;
  • S3存储桶名称 + AWS凭证 - 从私有源导入案例;

代码路径由类似于以下结构的函数处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func getObjectsList(session *Session, config *aws.Config, bucket_name string){

	//初始化或重新初始化S3客户端
	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,但当ListObjectsV2返回错误时,函数有一个有趣的代码路径:

1
2
3
4
5
6
7
...
if err != nil {
		if err, awsError := err.(awserr.Error); awsError {
			aws_config.credentials = nil
			getObjectsList(session_init, aws_config, bucket_name)
		}
}

错误处理设置aws_config.credentials = nil并再次尝试列出存储桶中的对象。

查看aws_config.credentials = nil

在这种情况下,将使用凭证提供者链,并最终假定实例的IAM角色。在我们的案例中,自动检索的凭证具有对内部S3存储桶的完全访问权限。

简单推论

如果内部S3存储桶名称通过平台暴露给最终用户(例如,通过网络流量),用户可以将它们用作“从S3导入”功能的输入,并直接在UI中检查其内容。

事实上,在应用程序的流量中看到内部存储桶名称并不罕见,因为它们通常用于内部数据处理。总之,提供内部存储桶名称会导致它们从导入功能中获取并添加到平台用户的数据中。

不同的客户端凭证初始化,不同的结果

AWS SDK客户端需要一个包含凭证对象的Session对象进行初始化。下面描述了设置客户端所需凭证的三种主要方式:

NewStaticCredentials

在凭证包中,NewStaticCredentials函数返回一个指向包装静态凭证的新Credentials对象的指针。

使用NewStaticCredentials的客户端初始化示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package testing

import (
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
)

var session = session.Must(session.NewSession(&aws.Config{
	Credentials: credentials.NewStaticCredentials("AKIA….", "Secret", "Session"),
	Region:      aws.String("us-east-1"),
}))

注意:凭证不应在代码中硬编码。相反,应在运行时从安全保险库中检索它们。

{ nil | 未指定 } 凭证对象

如果会话客户端初始化时未指定凭证对象,将使用凭证提供者链。同样,如果凭证对象直接初始化为nil,也会发生相同的行为。

未指定凭证对象的客户端初始化示例:

1
2
3
svc := s3.New(session.Must(session.NewSession(&aws.Config{
		Region:      aws.String("us-west-2"),
})))

值为nil的凭证对象的客户端初始化示例:

1
2
3
4
svc := s3.New(session.Must(session.NewSession(&aws.Config{
		Credentials: <nil_object>,
		Region:      aws.String("us-west-2"),
})))

结果:两种初始化方法都将导致依赖凭证提供者链。因此,将从链中检索的凭证(可能非常特权)将被使用。如前述“从S3导入”案例研究所示,不了解这种行为导致了内部存储桶的泄露。

AnonymousCredentials

正确的功能用于正确的任务 ;)

AWS SDK for Go API参考在这里提供帮助:

“AnonymousCredentials是一个空的Credential对象,可用作不需要签名的请求的虚拟占位符凭证。

此AnonymousCredentials对象可用于配置服务在发出服务API调用时不签名请求。例如,当访问公共S3存储桶时。”

1
2
3
4
svc := s3.New(session.Must(session.NewSession(&aws.Config{
  Credentials: credentials.AnonymousCredentials,
})))
// 访问公共S3存储桶。

基本上,AnonymousCredentials对象只是一个空的Credential对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 来源:https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/credentials.go#L60

// AnonymousCredentials是一个空的Credential对象,可用作
// 不需要签名的请求的虚拟占位符凭证。
//
// 这些Credentials可用于配置服务在发出服务API调用时不签名
// 请求。例如,当访问公共
// s3存储桶时。
//
//     svc := s3.New(session.Must(session.NewSession(&aws.Config{
//       Credentials: credentials.AnonymousCredentials,
//     })))
//     // 访问公共S3存储桶。
var AnonymousCredentials = NewStaticCredentials("", "", "")

对于云安全审计员

该漏洞也可能在其他AWS服务的使用中发现。在审计云驱动的Web平台时,寻找涉及AWS SDK客户端初始化的每个代码路径。对于每个代码路径,回答以下问题:

  • 代码路径是否直接从最终用户输入点(功能或暴露的API)可达? 例如,从平台内的用户设置页面获取AWS凭证,或用户提交AWS公共资源以由平台获取/修改。
  • 客户端的凭证如何初始化?
    • 凭证提供者链 - 查找链中机器拥有的角色
    • 是否有回退条件?查找最终用户是否可以通过某些输入到达该代码路径。如果默认使用,继续
      • 查找角色的权限
    • aws.Config结构作为输入参数 - 查找传递的角色的权限
  • 用户是否可以滥用该功能使平台使用特权凭证代表他们指向AWS账户内的私有资源? 例如,滥用“从S3导入”功能以导入基础设施的私有存储桶

对于开发人员

在处理公共资源时,使用AnonymousAWSCredentials配置AWS SDK客户端。来自官方AWS文档:

使用匿名凭证将导致请求在发送到服务之前不签名。任何不接受未签名请求的服务在这种情况下将返回服务异常。

在使用用户提供的凭证与其他云服务集成的情况下,平台应避免实现回退到系统角色模式。确保正确设置用户提供的凭证,避免最终出现aws.Config.Credentials = nil,因为这将导致客户端使用凭证提供者链 → 系统角色。

动手IaC实验室

正如系列介绍中所承诺的,我们开发了一个Terraform(IaC)实验室,用于部署易受攻击的虚拟应用程序并实验该漏洞:https://github.com/doyensec/cloudsec-tidbits/

敬请期待下一集!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计