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

2020-10-15 15:32:126910人阅读

简介

在这篇文章中,我们将为读者深入讲解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认证机制就是这种技术的一个简单变体。 

1602312751165625.png

客户端向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的漏洞的过程。


本文转载自:嘶吼

作者:fanyeee

原文地址:https://www.4hou.com/posts/LnEp

本文翻译自:https://googleprojectzero.blogspot.com/2020/10/enter-the-vault-auth-issues-hashicorp-vault.html

0
现金券
0
兑换券
立即领取
领取成功