[[346414]]
簡介
在這篇文章中,我們將為讀者深入講解HashiCorp Vault中的兩個身份驗證漏洞。實際上,我們不僅會介紹這兩個漏洞的利用方法,同時,還會演示如何在“云原生”軟件中找到這種類型的安全漏洞。這兩個漏洞(CVE-2020-16250/16251)均已得到了HashiCorp公司的妥善處理,并在8月份發(fā)布的1.2.5,1.3.8,1.4.4和1.5.1版本Vault中進行了修復。
Vault是一種廣泛使用的工具,用于安全地存儲、生成和訪問API密鑰、密碼或證書等機密信息。盡管它也能夠用作人類用戶的共享密碼管理器,但是它的功能卻主要是針對基于API的訪問進行優(yōu)化的。Vault的應用場景包括為某些服務(Web服務器、數(shù)據(jù)庫或第三方資源(如AWS S3 bucket)等)提供臨時的登錄憑據(jù)。
使用像Vault這樣的中心化機密信息存儲設施能夠帶來許多安全優(yōu)勢,例如集中審計,強制憑證輪換或加密數(shù)據(jù)存儲。然而,對于攻擊者來說,中心化的機密信息存儲也是一個非常值得關注的攻擊目標——一旦得手,攻擊者就能訪問各種重要的機密信息,從而可以訪問大部分的目標基礎設施。
在深入研究這些漏洞的技術細節(jié)之前,下一節(jié)將概述Vault的身份驗證架構及其與云提供商集成的方式。熟悉Vault的讀者可以跳過本節(jié)。
基于Vault的身份驗證架構
與Vault進行交互時,首先需要進行身份驗證;Vault支持基于角色的訪問控制,以管理對存儲的機密信息的訪問權限。在身份驗證方面,它支持可插拔的auth方法,范圍從靜態(tài)憑證、LDAP或Radius到完全集成到第三方OpenID Connect (OIDC)提供商或云身份訪問管理(IAM)平臺。對于在支持的云提供商上運行的基礎設施來說,使用云提供商的IAM平臺進行身份驗證是一個非常合乎邏輯的選擇。
下面,我們將以AWS為例進行介紹。我們知道,幾乎每一個在AWS中運行的工作負載都是以特定的AWS IAM用戶的身份來執(zhí)行的。通過啟用和配置aws auth方法,您可以在某些IAM用戶或角色與Vault角色之間創(chuàng)建相應的映射。
想象一下,如果您有一個AWS Lambda函數(shù),并希望讓它訪問存儲在Vault中的數(shù)據(jù)庫密碼。Vault管理員可以使用vault CLI為Lambda函數(shù)的執(zhí)行角色分配一個vault角色,而不是在函數(shù)代碼中存儲硬編碼的憑證。
- 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之間創(chuàng)建一個映射。這樣,就可以通過vault策略來授予dbclient角色對數(shù)據(jù)庫秘密的訪問權了。
當lambda函數(shù)執(zhí)行時,它通過向/v1/auth/aws/login API端點發(fā)送請求,以通過Vault進行身份驗證。我將在后面介紹這個請求的具體結構,但現(xiàn)在只是假設該請求允許Vault驗證調用者的AWS IAM角色。如果驗證成功,Vault會將dbclient角色的臨時API令牌返回給lambda函數(shù)。現(xiàn)在,就可以使用該令牌從Vault獲取數(shù)據(jù)庫密碼了。根據(jù)數(shù)據(jù)庫后端的不同,這個密碼可以是一個靜態(tài)的用戶密碼組合,一個臨時的客戶端證書,甚至是一個動態(tài)創(chuàng)建的證書對。
以這種方式使用Vault有一些不錯的安全優(yōu)勢:lambda函數(shù)本身不需要包含引導憑證,而且每次訪問數(shù)據(jù)庫的憑證都是可以審計的。輪換舊的或被破壞的數(shù)據(jù)庫憑證非常簡單,并且可以集中執(zhí)行。
然而,這種操作上的簡單性,完全是將復雜性隱藏在AWS iam auth方法中結果。那么,/v1/auth/aws/login API端點究竟是如何工作的,未經(jīng)認證的攻擊者是否有辦法冒充隨機的AWS IAM角色呢?
sts:GetCallerIdentity
在其內部,Vault的aws auth方法支持兩種不同的認證機制:iam和ec2。在這里,我們感興趣的是iam,我們之前的Lambda示例中曾用過該機制。Iam認證機制是建立在名為GetCallerIdentity的AWS API方法之上的,它是AWS安全令牌服務(STS)的一部分。
顧名思義,GetCallerIdentity將返回IAM角色或用戶的詳細信息,其憑證被用于調用API。要了解Vault如何使用該方法對客戶進行身份驗證,我們需要了解AWS API如何進行身份驗證的。
AWS不是將某種形式的身份驗證令牌或憑據(jù)附加到API請求中,而是要求客戶端使用調用者的秘密訪問密鑰為(規(guī)范化的)請求計算HMAC簽名,并將此簽名附加到請求中。這種機制使得預先對請求進行簽名并將其轉發(fā)給另一方,從而實現(xiàn)一定程度的身份冒充成為可能。一個流行的用例是,賦予客戶端S3的文件上傳權限,而無需授予他們訪問具有寫權限的憑據(jù)的權限。
實際上,Vault aws認證機制就是這種技術的一個簡單變體。

客戶端向STS GetCallerIdentity方法預先對一個HTTP請求進行簽名,并將其序列化版本發(fā)送給Vault服務器。Vault服務器將預簽名的請求發(fā)送到STS主機,并從結果中提取AWS IAM信息。這個流程的服務器端部分是由builtin/credential/aws/path_login.go文件的pathLoginUpdate函數(shù)實現(xiàn)的。
- 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)
該函數(shù)從存儲在數(shù)據(jù)中的請求正文中提取HTTP方法、URL、正文和標頭。然后調用submitCallerIdentity將請求轉發(fā)到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函數(shù)會根據(jù)用戶提供的參數(shù)創(chuàng)建一個http.Request對象,并使用硬編碼常量https://sts.amazonaws.com來構建目標URL。
如果沒有這個限制,我們可以簡單地觸發(fā)對我們控制的服務器的請求,并返回調用者身份。
然而,由于完全缺乏對URL路徑、查詢、POST正文和HTTP標頭的驗證,所以這看起來仍然是一個非常有希望的攻擊面。下一節(jié)將介紹如何將這個安全缺陷變成一個認證繞過漏洞。
STS(調用方)身份盜用
我們的目標是欺騙Vault的submitCallerIdentityRequest函數(shù),使其返回一個攻擊者控制的調用方身份。實現(xiàn)這個目標的方法之一是操縱Vault服務器,使其向我們控制的主機發(fā)送請求,從而繞過硬編碼的端點主機。通過查看buildHttpRequest方法的源代碼,我想到了兩種方法:
· 用于計算targetUrl的代碼,即targetUrl := fmt.Sprintf("%s/%s", endpoint, parsedUrl.RequestURI()) ,看起來在URL解析問題方面并不是很健壯。但是,嵌入偽造的用戶信息(https://sts.amazonaws.com/:foo@example.com/test)之類的技巧和類似的想法對健壯的Go URL解析器是行不通的。
· 即使Vault將始終創(chuàng)建一個指向硬編碼端點的HTTPS請求,攻擊者也可以完全控制Host http標頭(request.Host = parsedUrl.Host)。如果STS API前面的負載平衡器根據(jù)Host標頭做出路由決策的話,這可能就是一個問題,但針對STS主機的盲測并沒有取得成功。
在排除了簡單的方法后,我們還有另一種方法可以使用。Vault并沒有限制URL查詢參數(shù)。這意味著,我們不僅可以創(chuàng)建GetCallerIdentity的預簽名請求,還可以對STS API的任何操作創(chuàng)建請求。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"`
-
- }
我們可以看到,只要狀態(tài)代碼為200,就會對從STS接收到的每個響應調用parseGetCeller IdentityResponse。該函數(shù)將使用Golang標準XML庫將XML響應解碼成GetCallerIdentityResponse結構,如果解碼失敗則返回錯誤。
這個代碼有一個容易被忽略的問題:Vault從未強制驗證STS響應是否為XML編碼。雖然STS響應在默認情況下是XML編碼的,但是對于發(fā)送Accept:Application/json HTTP標頭的客戶端來說,它也能夠支持JSON編碼。
但是對于Vault來說,這就變成了一個安全問題,因為go XML解碼器有一個驚人的特性:解碼器會悄悄地忽略預期的XML根之前和之后的非XML內容。這意味著使用(JSON編碼的)服務器響應(如‘{“abc” : “xzy}’)調用parseGetCallIdentityResponse函數(shù)將會成功,并返回一個(空的)CallIdentityResponse結構。
小結
在本文中,我們?yōu)樽x者介紹了Vault的身份驗證架構,以及冒用調用方身份的方法,在下一篇文章中,我們將繼續(xù)為讀者介紹利用Vault-on-GCP的漏洞的過程。
本文翻譯自:https://googleprojectzero.blogspot.com/2020/10/enter-the-vault-auth-issues-hashicorp-vault.html如若轉載,請注明原文地址。