Post

Intercepting AWS CLI and SDK API calls with MITM proxy. How ACCESS_KEY and ACCESS_SECRET_KEY are applied. Signature Version 4 Under the Hood.

Abstract

In the previous post we have checked how to setup AWS API Gateway with AWS IAM authorization. To access its secured endpoint we were using Postman, all we need to call the API is to choose sigv4 algorithm, provide ACCESS_KEY, SECRET_ACCESS_KEY, region and service. Postman under the neath generates all the payload and sends the request.

In this post we will deep dive into AWS Sigv4, see how it is applied, what happens with real ACCESS/SECRET KEYs. We will intercept and inspect the real payload of aws cli and check what is transferred over the network.

This topic pertains not only to the IAM-based endpoint of API Gateway, but also to any invocation using the aws cli or aws sdk library. Because the AWS infrastructure follows a layered architecture, where each service exposes publicly accessible API endpoints. As a result, the security mechanisms for these endpoints remain consistent across AWS services.

Signature Version 4 is the AWS signing protocol. AWS also supports an extension, Signature Version 4A, which supports signatures for multi-Region API requests.

LAB setup for AWS calls inspection

setup.png

Let’s create fake ACCESS/SECRET KEY

To do this, we are updating ~/.aws/config with registering one more profile:

1
2
3
[demo]
region=eu-west-1
output=json

Also, we need to update ~/.aws/credentials

1
2
3
4
[demo]
AWS_ACCESS_KEY_ID=AKIA1111111111111111
AWS_SECRET_ACCESS_KEY=CCCCCCCCCCCCCCCCCdus/1111111111111111111
regions=eu-west-1

Now let’s switch to newly created profile:

1
export AWS_PROFILE=demo

Intercept requests with mitmproxy

Let’s list S3 buckets, but for the endpoint we will forward aws cli requests to mitmproxy:

–endpoint-url Specifies the URL to send the request to. For most commands, the AWS CLI automatically determines the URL based on the selected service and the specified AWS Region. However, some commands require that you specify an account-specific URL. You can also configure some AWS services to host an endpoint directly within your private VPC, which might then need to be specified. For a list of the standard service endpoints available in each Region, see AWS Regions and Endpoints in the Amazon Web Services General Reference.

1
2
3
aws s3 ls --endpoint-url http://localhost:8080

An error occurred (502) when calling the ListBuckets operation (reached max retries: 4): Bad Gateway

As we can observe aws cli has failed with request and 4 more retired requests. Same sequence of incoming invocations we can track on proxy instance: (GET HTTP incoming request and 4 retries. These are the requests made by aws cli over HTTP protocol.)

img_11.png

Let’s view them with more details:

img_11.png img_11.png img_11.png

Exploring request payload in details

1
2
3
4
5
6
7
8
9
10
11
12
2023-07-16 13:47:57 GET http://localhost:8080/
                        ← Connection killed: Request destination unknown. Unable to figure out where this request should be forwarded to.
                              Request                                                             Response                                                              Detail
Host:                   localhost:8080
Accept-Encoding:        identity
User-Agent:             aws-cli/1.27.92 md/Botocore#1.31.2 ua/2.0 os/macos#21.6.0 md/arch#x86_64 lang/python#3.10.9 md/pyimpl#CPython cfg/retry-mode#legacy botocore/1.31.2
X-Amz-Date:             20230716T104757Z
X-Amz-Content-SHA256:   e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Authorization:          AWS4-HMAC-SHA256 Credential=AKIA1111111111111111/20230716/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date,
                        Signature=7faebc25c8c5ea57e8a8565c7c145816fb9c9be5d3d5258a03613e4f75e36716
amz-sdk-invocation-id:  db19fb2e-221d-48b1-954e-9d31dbc097a4
amz-sdk-request:        attempt=1
  • aws-cli/1.27.92 md/Botocore#1.31.2 - as we can see aws-cli uses botocore 1.31.2 version and this is true, because aws-cli is written on Python and uses most popular botocore library to interact with AWS: https://github.com/aws/aws-cli
  • X-Amz-Date: 20230716T104757Z the date when request was made 16/07/2023 10:47:57
  • X-Amz-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  • Authorization: AWS4-HMAC-SHA256 Credential=AKIA1111111111111111/20230716/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=7faebc25c8c5ea57e8a8565c7c145816fb9c9be5d3d5258a03613e4f75e36716 is dynamic (for each request Signature will be regenerated. It has time-based generation protection in algorithm) it contains Credential=AKIA1111111111111111/20230716/us-east-1/s3/aws4_request - this is our fake ACCESS_KEY, extracted date, region, service name and endpoint (we will see later code snippets of aws cli)
  • amz-sdk-invocation-id: db19fb2e-221d-48b1-954e-9d31dbc097a4 - is the same for this and all retry requests
  • amz-sdk-request: attempt=1 the attempt number. On further retry requests it will be in other format attempt=4; max=5

According to latest aws-cli sources the following sequence is applied on generating Signature and signing the request before sending to AWS:

flowchart TD
    A[modify_request_before_signing] --> B[canonical_request]
    B --> C[string_to_sign]
    C --> D[inject_signature_to_request]
  • canonical request calculation:
1
2
3
4
5
6
7
8
9
'GET
/

host:localhost:8080
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date:20230716T165206Z

host;x-amz-content-sha256;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

x-amz-content-sha256 is prepared using sha256(request_body).hexdigest() function

  • string to sign calculation:
1
2
3
4
'AWS4-HMAC-SHA256
20230716T165206Z
20230716/us-east-1/s3/aws4_request
e9294b9f0d3a4917e178edd1774e837c64145f3dd66aa46b4116a2f966e09d5d'
1
2
3
4
5
6
def string_to_sign(self, request, canonical_request):
        sts = ['AWS4-HMAC-SHA256']
        sts.append(request.context['timestamp'])
        sts.append(self.credential_scope(request))
        sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
        return '\n'.join(sts)
1
2
3
4
5
6
7
def credential_scope(self, request):
        scope = []
        scope.append(request.context['timestamp'][0:8])
        scope.append(self._region_name)
        scope.append(self._service_name)
        scope.append('aws4_request')
        return '/'.join(scope)
  • signature calculation:

To generate signature sign function applied (using SHA256 implementation) multiple times to SECRET_KEY, DATE, REGION, SERVICE and string to sign

1
    _sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
1
'27b70dea39f1fddc4dd42d16c4cdd15fbce79dd82e72d33c2ea22d1e34072434'
1
2
3
4
5
6
7
8
9
def signature(self, string_to_sign, request):
        key = self.credentials.secret_key
        k_date = self._sign(
            (f"AWS4{key}").encode(), request.context["timestamp"][0:8]
        )
        k_region = self._sign(k_date, self._region_name)
        k_service = self._sign(k_region, self._service_name)
        k_signing = self._sign(k_service, 'aws4_request')
        return self._sign(k_signing, string_to_sign, hex=True)
  • the result request after _inject_signature_to_request applied:
1
2
3
4
User-Agent: aws-cli/1.27.160 Python/3.11.2 Darwin/21.6.0 botocore/1.29.154
X-Amz-Date: 20230716T165206Z
X-Amz-Content-SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Authorization: AWS4-HMAC-SHA256 Credential=AKIA1111111111111111/20230716/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=27b70dea39f1fddc4dd42d16c4cdd15fbce79dd82e72d33c2ea22d1e34072434

Key takeaways

  • ACCESS_KEY is sent in every HTTP request in text-plain form in the header
  • SECRET_ACCESS_KEY is not send over the network. However, it is included in multiple rounds of hashing as one of many variables when preparing the Signature of request
  • Special hash is used to sign the request to exclude the possibility of request tampering

To prevent tampering with a request while it’s in transit, some request elements are used to calculate a hash (digest) of the request, and the resulting hash value is included as part of the request. When an AWS service receives the request, it uses the same information to calculate a hash and matches it against the hash value in your request. If the values don’t match, AWS denies the request.

  • There is a datetime included during hash calculation, creating a window during which AWS is expecting the original request

In most cases, a request must reach AWS within five minutes of the time stamp in the request. Otherwise, AWS denies the request.

This post is licensed under CC BY 4.0 by the author.