在这篇博客文章中,我将讨论HashiCorp Vault中的两个漏洞,以及它与亚马逊网络服务(AWS)和谷歌云平台(GCP)的集成。这些问题可能导致在使用AWS和GCP身份验证方法的配置中绕过身份验证,并展示了您可以在现代“云本地”软件中发现的问题类型。这两个漏洞(CVE-2020-16250/16251)都已由HashiCorp解决,并在8月份发布的Vault版本1.2.5、1.3.8、1.4.4和1.5.1中进行了修复。
Vault是一种广泛使用的工具,用于安全地存储、生成和访问API密钥、密码或证书等机密。它可以用作人类用户的共享密码管理器,但其功能集针对其他服务基于API的访问进行了优化。Vault的一个示例使用案例是提供您的一项服务,例如Web服务器、数据库的短期凭证或AWS S3存储桶等第三方资源。
使用中央秘密存储(如Vault)可提供集中审核、强制凭证轮换或加密数据存储等安全优势。但是,中央存储也是攻击者非常感兴趣的目标。利用Vault中的漏洞可以让攻击者完全访问大量重要机密和目标基础设施的大部分。
在深入讨论漏洞的技术细节之前,下一节将概述Vault的身份验证体系结构及其与云提供商的集成方式。熟悉Vault的读者可以随意跳过本部分。
与Vault交互需要身份验证,并且Vault支持基于角色的访问控制来管理对存储机密的访问。对于身份验证,它支持可插拔的身份验证方法,范围从静态凭据、LDAP或Radius,到完全集成到第三方OpenID Connect(OIDC)提供商或云身份访问管理(IAM)平台。对于在受支持的云提供商上运行的基础架构,使用提供商的IAM平台进行身份验证是合乎逻辑的选择。
以AWS为例:您可以在AWS中运行的几乎每个工作负载都在特定AWS IAM用户的环境中执行。通过启用和配置AWS身份验证方法,您可以在某些IAM用户或角色与Vault角色之间创建映射。
假设您有一个AWS Lambda函数,并希望授予它对存储在Vault中的数据库密码的访问权限。Vault管理员可以使用Vault CLI将Vault角色分配给Lambda功能执行角色,而不是将硬编码凭据存储在功能代码中:
这将在名为dbclient的Vault角色和AWS IAM角色lambda-Role之间创建映射。现在可以使用保险存储策略来授予dbclient角色对数据库机密的访问权限。
当lambda函数执行时,它通过向/v1/auth/aws/login API端点发送请求来验证Vault。稍后我将在POST中讨论此请求的确切布局,但现在只需假设该请求允许Vault验证调用者的AWS IAM角色即可。如果身份验证成功,Vault会将dbclient角色的短期API令牌返回给lambda函数。现在可以使用此令牌从Vault获取数据库密码。根据数据库后端的不同,此密码可能是静态用户密码组合、短期客户端证书,甚至可能是动态创建的凭据对。
以这种方式使用Vault有一些很好的安全好处:lambda函数本身不需要包含引导凭据,并且对数据库凭据的每次访问都是可审计的。轮换旧的或受损的数据库凭据非常简单,并且可以集中实施。
但是,只有由于AWS IAM身份验证方法中隐藏的复杂性,才有可能实现这种操作简单性。/v1/auth/aws/login API端点实际是如何工作的,是否存在未经身份验证的攻击者可以模拟随机AWS IAM角色的方法?我们来看一下。
Vault的AWS身份验证方法在内部支持两种不同的身份验证机制:IAM和EC2。我们对IAM机制很感兴趣,它是推荐的变体,也在前面的Lambda示例中使用。IAM身份验证构建在名为GetCeller Identity的AWS API方法之上,该方法是AWS安全令牌服务(STS)的一部分。
顾名思义,GetCeller Identity返回其凭据用于调用API的IAM角色或用户的详细信息。要了解Vault如何使用此方法对客户端进行身份验证,我们需要了解AWS API如何执行身份验证:
AWS要求客户端使用调用方的秘密访问密钥计算(规范化)请求的HMAC签名,并将此签名附加到请求,而不是将某种形式的身份验证令牌或凭据附加到API请求。此机制使预先签名请求并将其转发给另一方以允许有限形式的模拟成为可能。一个常见的示例用例是,让客户端能够将文件上传到S3,而不允许其访问具有写入权限的凭据。
客户端对发送给STS GetCeller Identity方法的HTTP请求进行预签名,并将其序列化版本发送到Vault服务器。Vault服务器将预先签名的请求发送到STS主机,并从结果中提取AWS IAM信息。此流程的服务器端部分在builtin/Credential/aws/path_login.go中的pathLoginUpdate中实现:
该函数从提供的请求正文中提取HTTP方法、URL、正文和标头,该正文存储在数据中。然后,它调用submitCeller Identity将请求转发到STS服务器,并获取并解析parseGetCeller IdentityResponse中的结果:
Func submitCeller IdentityRequest(CTX上下文。Context、maxRetries int、方法、端点字符串、parsedUrl*url。URL、正文字符串、标题http。Header)(*GetCeller IdentityResult,Error){。
BuildHttpRequest根据用户提供的参数创建一个http.Request对象,但是使用硬编码常量https://sts.amazonaws.com来构建目标URL。
如果没有这个限制,我们可以简单地触发对我们控制的服务器的请求,并返回一个假的呼叫者身份。
然而,完全缺乏对URL路径、查询、POST正文和HTTP头的验证看起来仍然是一个有希望的攻击面。下一节将介绍如何将此差距转变为完全的身份验证绕过。
我们的目标是欺骗Vault的submitCeller IdentityRequest函数返回攻击者控制的调用者身份。实现此目的的一种方法是操作Vault服务器以绕过硬编码端点主机向我们控制的主机发送请求。查看buildHttpRequest方法,脑海中浮现出两种方法:
针对URL解析问题,用于计算targetUrl targetUrl:=fmt.Sprintf(";%s/%s";,端点,parsedUrl.RequestURI())的代码看起来不太健壮。然而,像嵌入假用户信息(https://sts.amazonaws.com/:[email protected]/test)和类似想法这样的技巧对健壮的go URL解析器无效。
即使Vault将始终创建指向硬编码端点的HTTPS请求,攻击者也可以完全控制主机http标头(request.Host=parsedUrl.Host)。如果STS API前面的负载均衡器根据Host报头做出路由决策,但针对STS主机的盲测不会成功,则可能会出现问题。
在排除了简单的前进方式之后,我们仍然有另一种方法可用:Vault不限制我们的URL查询参数。这意味着我们不限于对GetCeller Identity的预签名请求,并且可以创建对STS API的任何操作的请求。STS支持8种不同的操作,但都不能让我们完全控制响应。在这一点上,我慢慢地感到沮丧,并决定看看Vault的响应解析代码:
只要状态代码为200,就会对从STS接收到的每个响应调用parseGetCeller IdentityResponse。该函数使用Golang标准XML库将XML响应解码为GetCeller IdentityResponse结构,如果解码失败则返回错误。
这段代码有一个容易遗漏的问题:Vault从不强制或验证STS响应实际上是XML编码的。虽然STS响应在默认情况下是XML编码的,但是对于发送Accept:Application/json HTTP头的客户端,它也支持JSON编码。
对于Vault,这变成了一个安全问题,这是由于go XML解码器的一个有点令人惊讶的特性:解码器会悄悄地忽略预期的XML根之前和之后的非XML内容。这意味着使用(JSON编码的)服务器响应(如‘{“abc”:“xzy<;GetCallerIdentityResponse>;<;/GetCallerIdentityResponse>;}’)调用parseGetCallIdentityResponse将会成功,并返回(空)CallIdentityResponse结构。
这使我们真正接近了欺骗任意调用者身份的目标:我们只需要找到一个反映攻击者控制的文本作为其API响应的一部分的STS操作。序列化对它的请求,同时包含一个Accept:Application/json头,并将一个任意的GetCeller IdentityResponse XML blob放入反射的有效负载中。
事实证明,查找不限于字母数字字符的反射参数很困难。经过反复试验,我决定将AssumeRoleWithWebIdentity操作及其SubjectFromWebIdentityToken响应元素作为目标。AssumeRoleWithWebIdentity用于将OpenID Connect(OIDC)提供商签名的JSON Web令牌(JWT)转换为AWS IAM身份。
向此操作发送带有有效签名JWT的请求将在SubjectFromWebIdentityToken字段中返回令牌的子字段。
当然,普通的OIDC提供程序不会在主题字段中使用XML有效负载签署JWT。尽管如此,攻击者只需创建自己的OIDC身份提供商(IDP),在自己拥有的AWS帐户上注册,然后使用自己的密钥签署任意令牌即可。
创建最小的OIDC IdP。这归结为生成RSA密钥对,创建OIDC discovery.json和key.json文档,并在Web服务器上托管json文件(有关使用S3的设置示例,请参阅此处)。
使用您自己的AWS帐户注册OID IdP-&>AWS IAM角色映射。请务必注意,用于此操作的AWS帐户不需要与我们的目标有任何关系。
我们现在可以使用我们的OIDP来签署一个JWT,该JWT包含一个任意的GetCeller IdentityResponse作为其主题声明的一部分。解码后的示例令牌可能如下所示:iss、azp和aud与步骤2中指定的详细信息相匹配。sub包含我们的欺骗响应,将我们标识为AWS IAM帐户arn:aws:iam::supervileged-aws-account。
我们可以通过使用步骤3中的(签名)令牌和步骤2中使用的RoleArn向STS AssumeRoleWithWebIdentity操作发送直接请求来测试是否正确设置了所有内容:
如果一切按计划进行,STS将把令牌主题反映为其JSON编码响应的一部分。如上所述,Go XML解码器将跳过GetCeller IdentityResponse对象前后的所有内容,从而使Vault将其视为有效的STS Celler Identity响应。
最后一步是将此请求转换为Vault期望的格式(例如,对所有必需的头、URL和空的POST正文进行Base64编码),并将其作为/v1/auth/aws/login上的登录请求发送到目标Vault服务器。Vault将对请求进行反序列化,并将其发送到STS并曲解响应。如果我们的假GetCeller IdentityResponse中的AWS ARN/userid在Vault服务器上拥有权限,我们就会得到一个有效的会话令牌,我们可以用它与Vault服务器交互来获取一些机密。
我编写了一个概念验证漏洞,负责JWT创建和序列化方面的大部分繁忙工作。虽然OIDC提供程序设置增加了一些复杂性,但我们最终可以很好地绕过任意启用AWS的角色的身份验证。唯一的要求是攻击者知道目标Vault服务器中的特权AWS角色的名称。
这里出了什么问题?从攻击者的角度来看,整个身份验证机制似乎很聪明,但容易出错。将HTTP请求转发放入安全产品未经身份验证的外部攻击面需要对实现和底层HTTP库有很强的信心。这变得更加困难,因为安全性取决于Security Token Service的实现细节,该细节可能在未来的任何时候更改。例如,AWS可能决定将STS放在负载平衡前端之后,该前端使用Host标头进行路由决策。如果不对Vault代码库进行任何更改,这可能会从某个时刻严重降低此身份验证机制的安全性。
当然,身份验证如此工作是有原因的:AWS IAM没有一种直接的方法来向其他非AWS服务证明服务的身份。第三方服务无法轻松验证预签名请求,并且AWS IAM不提供任何可用于实现基于证书的身份验证或JWT的标准签名原语。
最后,Hashicorp通过强制执行HTTP标头的允许列表、限制对GetCeller Identity操作的请求以及更强的STS响应验证来修复该漏洞,希望这足以防止STS实现的意外更改或STS和Golang之间的HTTP解析器差异。
在AWS身份验证模块中发现此问题后,我决定查看其GCP等效项。下一节将介绍如何实现Vault的GCP身份验证,以及一个简单的逻辑缺陷如何在许多配置中导致绕过身份验证。
Vault支持在Google Cloud上展开的GCP身份验证方法。与其对应的AWS类似,auth方法支持两种不同的身份验证机制:IAM和GCE。IAM机制支持任意服务帐户,并且可以通过App Engine或Cloud Functions等服务使用,而GCE只能用于对运行在Google Compute Engine上的虚拟机进行身份验证。尽管如此,它还是有一些有趣的优势。GCE不仅可以根据服务帐户身份做出身份验证决策,还可以根据多个VM属性授予访问权限。例如,配置可以只向特定地区(欧洲-西部-6)的VM授予访问某些机密的权限,允许xyz-prod GCP项目中的所有VM访问,或者使用实例组对其进行进一步限制。
IAM和GCE都构建在JWT之上。要执行以下操作的Vault客户端。
身份验证,创建签名令牌以证明其身份并将其发送到Vault。
服务器来取回会话令牌。对于IAM机制,客户端直接对令牌签名。
使用其控制下的服务帐户私钥或使用projects.serviceAccounts.signJwt IAM API方法。
对于GCE,客户端应该在授权的GCE VM上运行。它通过向GCP元数据服务器的实例标识端点发送请求来获取签名令牌。与服务帐户令牌不同,此令牌由Google官方证书签名。除了普通的JWT声明(SUB、AUD、IAT、EXP)之外,元数据服务器返回的令牌还包含一个特殊的COMPUTE_ENGINE声明,该声明列出了有关实例的详细信息,这些细节将作为身份验证过程的一部分进行处理:
JWT有许多设计选择,这使得它非常容易出现实现错误(有关典型问题的概述,请参见Securitum发布的这篇博客文章),因此我决定花一天时间检查Vault的令牌处理。
它首先在不验证签名的情况下解析令牌,并将解码后的令牌传递给getSigningKey帮助器方法:
GetSigningKey从令牌头提取密钥id声明(KID),并尝试查找具有相同标识符的Google范围内的OAuth密钥。这将适用于GCE元数据令牌,但不适用于由服务帐户签名的令牌:
如果此方法失败,Vault服务器将从提供的令牌中提取主题(子)声明。对于有效令牌,此声明包含签名服务帐户的电子邮件地址。知道令牌的密钥ID和主题后,Vault将使用服务帐户GCP API获取用于签名的公钥:
返回值为零,错误包装。Wrapf(FMT.。Sprintf(";无法获取JWT主题%q的公钥%q:{{err}}";,KID,say),saErr)。
在这两种情况下,Vault服务器现在都可以访问可以验证JWT签名的公钥:
如果验证成功,Vault将填写loginInfo结构,稍后将使用该结构授予或拒绝访问权限。如果令牌包含COMPUTE_ENGINE声明,则会将其复制到loginInfo.GceMetada字段中:
如上所述,所有这些代码都在IAM和GCE auth方法之间共享。这里的问题是,没有检查强制由任意服务帐户签名的令牌不包含GCE COMPUTE_ENGINE声明。虽然GCE元数据令牌中的内容值得信任并由Google控制,但服务帐户令牌完全由服务帐户的所有者控制,因此可以包含任意声明。
如果我们沿着GCE方法的控制流走到最后,我们可以看到,如果满足两个条件,Vault将使用loginInfo.GceMetadata作为其在pathGceLogin中的身份验证决策的一部分:
元数据部分中描述的VM需要存在。这是使用GCE API验证的,需要攻击者模拟活动运行的VM。实际上,只有project_id、zone和instance_name需要验证,并且需要设置为有效值。
JWT令牌的Subject声明中的服务帐户需要存在。这是使用ServiceAccount GCP API进行验证的,该API需要托管服务帐户的项目中的iam.serviceAccounts.get权限。由于攻击者只能在自己的项目中使用服务帐户,因此只需将此权限授予运行在甚至所有用户下的GCP Identity Vault就很简单了。
具有正确属性(项目、标签、区域..)的GCE实例。一切都很顺利,而且。
攻击者会得到一个有效的会话令牌。唯一不能绕过的身份验证限制是硬编码的服务帐户名,因为此值将等于攻击者帐户,而不是预期的VM帐户名。
使用gcloud在您控制的GCP项目中创建服务帐户并生成私钥:gcloud IAM service-account key create key.json--iam-account [email protected] t.com。
使用描述现有特权VM的虚假COMPUTE_ENGINE声明签署JWT。请参阅此处获取一个简单的概念验证脚本,该脚本负责大部分细节。
现在只需使用令牌登录到Vault:cURL--请求发布数据';{";角色";:";my-gce-角色";,";jwt";:";....";}';http://vault:8200/v1/auth/gcp/login
这是一个有趣的错误,需要一些GCP IAM知识才能发现。根本原因似乎是在parseAndValidateJwt函数中将两个独立的身份验证流合并到单个代码路径中,这使得在编写或审查代码时很难对所有安全要求进行推理。同时,GCP通过提供两种具有完全不同安全属性的JWT令牌,使您很容易自食其果。
这篇博文描述了HashiCorp Vault中的两个身份验证漏洞,HashiCorp Vault是一个用于秘密管理的“云本地”软件。在存储时。
.