2020-10-15 15:32:128212人阅读
简介
在这篇文章中,我们将为读者深入讲解HashiCorp Vault中的两个身份验证漏洞。实际上,我们不仅会介绍这两个漏洞的利用方法,同时,还会演示如何在“云原生”软件中找到这种类型的安全漏洞。这两个漏洞(CVE-2020-16250/16251)均已得到了HashiCorp公司的妥善处理,并在8月份发布的1.2.5,1.3.8,1.4.4和1.5.1版本Vault中进行了修复。
Vault是一种广泛使用的工具,用于安全地存储、生成和访问API密钥、密码或证书等机密信息。尽管它也能够用作人类用户的共享密码管理器,但是它的功能却主要是针对基于API的访问进行优化的。Vault的应用场景包括为某些服务(Web服务器、数据库或第三方资源(如AWS S3 bucket)等)提供临时的登录凭据。
使用像Vault这样的中心化机密信息存储设施能够带来许多安全优势,例如集中审计,强制凭证轮换或加密数据存储。然而,对于攻击者来说,中心化的机密信息存储也是一个非常值得关注的攻击目标——一旦得手,攻击者就能访问各种重要的机密信息,从而可以访问大部分的目标基础设施。
在深入研究这些漏洞的技术细节之前,下一节将概述Vault的身份验证架构及其与云提供商集成的方式。熟悉Vault的读者可以跳过本节。
基于Vault的身份验证架构
与Vault进行交互时,首先需要进行身份验证;Vault支持基于角色的访问控制,以管理对存储的机密信息的访问权限。在身份验证方面,它支持可插拔的auth方法,范围从静态凭证、LDAP或Radius到完全集成到第三方OpenID Connect (OIDC)提供商或云身份访问管理(IAM)平台。对于在支持的云提供商上运行的基础设施来说,使用云提供商的IAM平台进行身份验证是一个非常合乎逻辑的选择。
下面,我们将以AWS为例进行介绍。我们知道,几乎每一个在AWS中运行的工作负载都是以特定的AWS IAM用户的身份来执行的。通过启用和配置aws auth方法,您可以在某些IAM用户或角色与Vault角色之间创建相应的映射。
想象一下,如果您有一个AWS Lambda函数,并希望让它访问存储在Vault中的数据库密码。Vault管理员可以使用vault CLI为Lambda函数的执行角色分配一个vault角色,而不是在函数代码中存储硬编码的凭证。
vault write auth/aws/role/dbclient auth_type=iam \ bound_iam_principal_arn=arn:aws:iam::123456789012:role/lambda-role policies=prod,dev max_ttl=10m
这将在名为dbclient的vault角色和AWS IAM角色lambda-role之间创建一个映射。这样,就可以通过vault策略来授予dbclient角色对数据库秘密的访问权了。
当lambda函数执行时,它通过向/v1/auth/aws/login API端点发送请求,以通过Vault进行身份验证。我将在后面介绍这个请求的具体结构,但现在只是假设该请求允许Vault验证调用者的AWS IAM角色。如果验证成功,Vault会将dbclient角色的临时API令牌返回给lambda函数。现在,就可以使用该令牌从Vault获取数据库密码了。根据数据库后端的不同,这个密码可以是一个静态的用户密码组合,一个临时的客户端证书,甚至是一个动态创建的证书对。
以这种方式使用Vault有一些不错的安全优势:lambda函数本身不需要包含引导凭证,而且每次访问数据库的凭证都是可以审计的。轮换旧的或被破坏的数据库凭证非常简单,并且可以集中执行。
然而,这种操作上的简单性,完全是将复杂性隐藏在AWS iam auth方法中结果。那么,/v1/auth/aws/login API端点究竟是如何工作的,未经认证的攻击者是否有办法冒充随机的AWS IAM角色呢?
sts:GetCallerIdentity
在其内部,Vault的aws auth方法支持两种不同的认证机制:iam和ec2。在这里,我们感兴趣的是iam,我们之前的Lambda示例中曾用过该机制。Iam认证机制是建立在名为GetCallerIdentity的AWS API方法之上的,它是AWS安全令牌服务(STS)的一部分。
顾名思义,GetCallerIdentity将返回IAM角色或用户的详细信息,其凭证被用于调用API。要了解Vault如何使用该方法对客户进行身份验证,我们需要了解AWS API如何进行身份验证的。
AWS不是将某种形式的身份验证令牌或凭据附加到API请求中,而是要求客户端使用调用者的秘密访问密钥为(规范化的)请求计算HMAC签名,并将此签名附加到请求中。这种机制使得预先对请求进行签名并将其转发给另一方,从而实现一定程度的身份冒充成为可能。一个流行的用例是,赋予客户端S3的文件上传权限,而无需授予他们访问具有写权限的凭据的权限。
实际上,Vault aws认证机制就是这种技术的一个简单变体。
客户端向STS GetCallerIdentity方法预先对一个HTTP请求进行签名,并将其序列化版本发送给Vault服务器。Vault服务器将预签名的请求发送到STS主机,并从结果中提取AWS IAM信息。这个流程的服务器端部分是由builtin/credential/aws/path_login.go文件的pathLoginUpdate函数实现的。
func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { method := data.Get("iam_http_request_method").(string) ... // In the future, might consider supporting GET if method != "POST" { return logical.ErrorResponse(...), nil } rawUrlB64 := data.Get("iam_request_url").(string) ... rawUrl, err := base64.StdEncoding.DecodeString(rawUrlB64) ... parsedUrl, err := url.Parse(string(rawUrl)) if err != nil { return logical.ErrorResponse(...), nil } bodyB64 := data.Get("iam_request_body").(string) ... bodyRaw, err := base64.StdEncoding.DecodeString(bodyB64) ... body := string(bodyRaw) headers := data.Get("iam_request_headers").(http.Header) endpoint := "https://sts.amazonaws.com" ... callerID, err := submitCallerIdentityRequest(ctx, maxRetries, method, endpoint, parsedUrl, body, headers)
该函数从存储在数据中的请求正文中提取HTTP方法、URL、正文和标头。然后调用submitCallerIdentity将请求转发到STS服务器,并利用ParseGetCallerIdentityResponse来获取和解析结果:
func submitCallerIdentityRequest(ctx context.Context, maxRetries int, method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) (*GetCallerIdentityResult, error) { ... request := buildHttpRequest(method, endpoint, parsedUrl, body, headers) retryableReq, err := retryablehttp.FromRequest(request) ... response, err := retryingClient.Do(retryableReq) responseBody, err := ioutil.ReadAll(response.Body) ... if response.StatusCode != 200 { return nil, fmt.Errorf(..) } callerIdentityResponse, err := parseGetCallerIdentityResponse(string(responseBody)) if err != nil { return nil, fmt.Errorf("error parsing STS response") } return &callerIdentityResponse.GetCallerIdentityResult[0], nil } func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) *http.Request { ... targetUrl := fmt.Sprintf("%s/%s", endpoint, parsedUrl.RequestURI()) request, err := http.NewRequest(method, targetUrl, strings.NewReader(body)) ... request.Host = parsedUrl.Host for k, vals := range headers { for _, val := range vals { request.Header.Add(k, val) } } return request }
buildHttpRequest函数会根据用户提供的参数创建一个http.Request对象,并使用硬编码常量https://sts.amazonaws.com来构建目标URL。
如果没有这个限制,我们可以简单地触发对我们控制的服务器的请求,并返回调用者身份。
然而,由于完全缺乏对URL路径、查询、POST正文和HTTP标头的验证,所以这看起来仍然是一个非常有希望的攻击面。下一节将介绍如何将这个安全缺陷变成一个认证绕过漏洞。
STS(调用方)身份盗用
我们的目标是欺骗Vault的submitCallerIdentityRequest函数,使其返回一个攻击者控制的调用方身份。实现这个目标的方法之一是操纵Vault服务器,使其向我们控制的主机发送请求,从而绕过硬编码的端点主机。通过查看buildHttpRequest方法的源代码,我想到了两种方法:
· 用于计算targetUrl的代码,即targetUrl := fmt.Sprintf("%s/%s", endpoint, parsedUrl.RequestURI()) ,看起来在URL解析问题方面并不是很健壮。但是,嵌入伪造的用户信息(https://sts.amazonaws.com/:foo@example.com/test)之类的技巧和类似的想法对健壮的Go URL解析器是行不通的。
· 即使Vault将始终创建一个指向硬编码端点的HTTPS请求,攻击者也可以完全控制Host http标头(request.Host = parsedUrl.Host)。如果STS API前面的负载平衡器根据Host标头做出路由决策的话,这可能就是一个问题,但针对STS主机的盲测并没有取得成功。
在排除了简单的方法后,我们还有另一种方法可以使用。Vault并没有限制URL查询参数。这意味着,我们不仅可以创建GetCallerIdentity的预签名请求,还可以对STS API的任何操作创建请求。STS支持8个不同的操作,但没有一个操作能让我们完全控制响应。这时,我开始感到沮丧,于是决定看看Vault的响应解析代码。
func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, error) { decoder := xml.NewDecoder(strings.NewReader(response)) result := GetCallerIdentityResponse{} err := decoder.Decode(&result) return result, err } type GetCallerIdentityResponse struct { XMLName xml.Name `xml:"GetCallerIdentityResponse"` GetCallerIdentityResult []GetCallerIdentityResult `xml:"GetCallerIdentityResult"` ResponseMetadata []ResponseMetadata `xml:"ResponseMetadata"` }
我们可以看到,只要状态代码为200,就会对从STS接收到的每个响应调用parseGetCeller IdentityResponse。该函数将使用Golang标准XML库将XML响应解码成GetCallerIdentityResponse结构,如果解码失败则返回错误。
这个代码有一个容易被忽略的问题:Vault从未强制验证STS响应是否为XML编码。虽然STS响应在默认情况下是XML编码的,但是对于发送Accept:Application/json HTTP标头的客户端来说,它也能够支持JSON编码。
但是对于Vault来说,这就变成了一个安全问题,因为go XML解码器有一个惊人的特性:解码器会悄悄地忽略预期的XML根之前和之后的非XML内容。这意味着使用(JSON编码的)服务器响应(如‘{“abc” : “xzy}’)调用parseGetCallIdentityResponse函数将会成功,并返回一个(空的)CallIdentityResponse结构。
小结
在本文中,我们为读者介绍了Vault的身份验证架构,以及冒用调用方身份的方法,在下一篇文章中,我们将继续为读者介绍利用Vault-on-GCP的漏洞的过程。
本文翻译自:https://googleprojectzero.blogspot.com/2020/10/enter-the-vault-auth-issues-hashicorp-vault.html