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
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.)
Let’s view them with more details:
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 seeaws-cli
uses botocore 1.31.2 version and this is true, becauseaws-cli
is written on Python and uses most popular botocore library to interact with AWS: https://github.com/aws/aws-cliX-Amz-Date: 20230716T104757Z
the date when request was made 16/07/2023 10:47:57X-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 requestSignature
will be regenerated. It has time-based generation protection in algorithm) it containsCredential=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 requestsamz-sdk-request: attempt=1
the attempt number. On further retry requests it will be in other formatattempt=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.