admin

深入剖析HashiCorp Vault中的身份验证漏洞(上篇)

admin 安全防护 2022-11-24 437浏览 0

深入剖析HashiCorp Vault中的身份验证漏洞(上篇)

简介

在这篇文章中,我们将为读者深入讲解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角色,而不是在函数代码中存储硬编码的凭证。

vaultwriteauth/aws/role/dbclientauth_type=iam\

bound_iam_principal_arn=arn:aws:iam::123456789012:role/lambda-rolepolicies=prod,devmax_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认证机制就是这种技术的一个简单变体。

深入剖析HashiCorp Vault中的身份验证漏洞(上篇)

客户端向STS GetCallerIdentity方法预先对一个HTTP请求进行签名,并将其序列化版本发送给Vault服务器。Vault服务器将预签名的请求发送到STS主机,并从结果中提取AWS IAM信息。这个流程的服务器端部分是由builtin/credential/aws/path_login.go文件的pathLoginUpdate函数实现的。

func(b*backend)pathLoginUpdateIam(ctxcontext.Context,req*logical.Request,data*framework.FieldData)(*logical.Response,error){

method:=data.Get("iam_http_request_method").(string)

...

//Inthefuture,mightconsidersupportingGET

ifmethod!="POST"{

returnlogical.ErrorResponse(...),nil

}

rawUrlB64:=data.Get("iam_request_url").(string)

...

rawUrl,err:=base64.StdEncoding.DecodeString(rawUrlB64)

...

parsedUrl,err:=url.Parse(string(rawUrl))

iferr!=nil{

returnlogical.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来获取和解析结果:

funcsubmitCallerIdentityRequest(ctxcontext.Context,maxRetriesint,method,endpointstring,parsedUrl*url.URL,bodystring,headershttp.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)

...

ifresponse.StatusCode!=200{

returnnil,fmt.Errorf(..)

}

callerIdentityResponse,err:=parseGetCallerIdentityResponse(string(responseBody))

iferr!=nil{

returnnil,fmt.Errorf("errorparsingSTSresponse")

}

return&callerIdentityResponse.GetCallerIdentityResult[0],nil

}



funcbuildHttpRequest(method,endpointstring,parsedUrl*url.URL,bodystring,headershttp.Header)*http.Request{

...

targetUrl:=fmt.Sprintf("%s/%s",endpoint,parsedUrl.RequestURI())

request,err:=http.NewRequest(method,targetUrl,strings.NewReader(body))

...

request.Host=parsedUrl.Host

fork,vals:=rangeheaders{

for_,val:=rangevals{

request.Header.Add(k,val)

}

}

returnrequest

}

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的响应解析代码。

funcparseGetCallerIdentityResponse(responsestring)(GetCallerIdentityResponse,error){

decoder:=xml.NewDecoder(strings.NewReader(response))

result:=GetCallerIdentityResponse{}

err:=decoder.Decode(&result)

returnresult,err

}

typeGetCallerIdentityResponsestruct{

XMLNamexml.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的漏洞的过程。

本文翻

继续浏览有关 安全 的文章
发表评论