AWS SDK客户端中系统角色的安全风险
系列介绍
在云安全领域,常见的问题通常涉及基础设施配置、公开存储桶、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将使用以下链:
- 环境变量
- 共享凭证文件
- 如果应用使用ECS任务定义或RunTask API操作,则使用任务的IAM角色
- 如果应用运行在Amazon EC2实例上,则使用Amazon EC2的IAM角色
以下代码片段显示了SDK如何检索第一个有效的凭证提供者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Retrieve returns the credentials value or error if no provider returned
// without error.
//
// If a provider is found it will be cached and any calls to IsExpired()
// will return the expired state of the cached provider.
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){
//initilize or re-initilize the S3 client
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,
})))
// Access public S3 buckets.
|
基本上,AnonymousCredentials对象只是一个空的Credential对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// source: https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/credentials.go#L60
// AnonymousCredentials is an empty Credential object that can be used as
// dummy placeholder credentials for requests that do not need to be signed.
//
// These Credentials can be used to configure a service not to sign requests
// when making service API calls. For example, when accessing public
// s3 buckets.
//
// svc := s3.New(session.Must(session.NewSession(&aws.Config{
// Credentials: credentials.AnonymousCredentials,
// })))
// // Access public S3 buckets.
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/
敬请期待下一集!