632 lines
20 KiB
Ruby
Executable File
632 lines
20 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
#
|
|
# This script is intended to manage a single AWS EC2 instance for use during
|
|
# pentests. The script offers following functionalities:
|
|
#
|
|
# start - Starts an EC2 instance. If it does not exist, it is to be created
|
|
# stop - Stops the EC2 instance. It does not terminate it.
|
|
# restart - Restarts the EC2 instance
|
|
# terminate - Terminates the EC2 instance.
|
|
# address - Gets an IPv4 address of the EC2 instance. If verbose options is set, will return more FQDN also.
|
|
# status - Checks what is a status of picked EC2 instance.
|
|
# ssh - Opens a ssh connection with specified instance. If it is not running, it is to be created and started.
|
|
# notify - Sends gnome notification using "notify-send" with running instance uptime
|
|
#
|
|
# The basic use case for this script is to have yourself launched an EC2 instance for quick
|
|
# verification of Web Application vulnerabilities like out-of-bound communication: blind-XXE for instance.
|
|
# Everytime you will be in position of needing a machine with public IPv4 address - take this script for a spin,
|
|
# and it will provide you with your own EC2 instance.
|
|
#
|
|
# Requirements:
|
|
# - gem "aws-sdk-ec2"
|
|
#
|
|
# Author: Mariusz Banach, '19, <mb@binary-offensive.com>
|
|
#
|
|
|
|
require 'aws-sdk-ec2'
|
|
require 'base64'
|
|
require 'optparse'
|
|
|
|
VERSION = '0.1'
|
|
|
|
$aws_config_path = File.join((ENV['OS'] == 'Windows_NT') ? ENV['UserProfile'] : File.expand_path('~'), '.aws')
|
|
|
|
$config = {
|
|
|
|
#
|
|
# Notably interesting configuration to fill up
|
|
#
|
|
|
|
:image_id => 'ami-07360d1b1c9e13198',
|
|
:key_name => 'ec2-pentest-key',
|
|
:sg_name => 'ec2-pentest-usage',
|
|
:instance_name => 'pentestec2',
|
|
:instance_type => 't2.micro',
|
|
:ssh_user => 'ec2-user',
|
|
:ssh_identity_file => File.join($aws_config_path, 'ec2-pentest-key.pem'),
|
|
|
|
# ----
|
|
:verbose => false,
|
|
:quiet => false,
|
|
:debug => false,
|
|
:aws_path => "",
|
|
:aws_profile => 'default',
|
|
:region => 'us-east-1',
|
|
}
|
|
|
|
$supported_funcs = {
|
|
'start' => 'Starts an EC2 instance. If it does not exist, it is to be created',
|
|
'stop' => 'Stops the EC2 instance. It does not terminate it.',
|
|
'restart' => 'Restarts the EC2 instance',
|
|
'terminate' => 'Terminates the EC2 instance.',
|
|
'address' => 'Gets an IPv4 address of the EC2 instance. If verbose options is set, will return more FQDN also.',
|
|
'status' => 'Checks what is a status of picked EC2 instance.',
|
|
'ssh' => 'Opens a ssh connection with specified instance. If it is not running, it is to be created and started.',
|
|
'notify' => 'Sends gnome notification using "notify-send" with running instance uptime.'
|
|
}
|
|
|
|
class Logger
|
|
def Logger._out(x)
|
|
if not $config[:quiet] and ($config[:verbose] or $config[:debug])
|
|
STDOUT.write(x + "\n")
|
|
end
|
|
end
|
|
|
|
def Logger.dbg(x)
|
|
if $config[:debug]
|
|
Logger._out("[dbg] #{x}")
|
|
end
|
|
end
|
|
|
|
def Logger.info(x)
|
|
Logger._out("[.] #{x}")
|
|
end
|
|
|
|
def Logger.fatal(x)
|
|
unless $config[:quiet]
|
|
STDOUT.write(x + "\n")
|
|
end
|
|
exit 1
|
|
end
|
|
|
|
def Logger.fail(x)
|
|
Logger._out("[-] #{x}")
|
|
end
|
|
|
|
def Logger.ok(x)
|
|
Logger._out("[+] #{x}")
|
|
end
|
|
end
|
|
|
|
|
|
class AwsEc2Manager
|
|
attr_reader :instance_name
|
|
|
|
def initialize(func, instance_name, config)
|
|
@instance_name = instance_name
|
|
@checked = false
|
|
@func = func
|
|
@config = config
|
|
@instance_id = ""
|
|
@instance_id_file = File.join($aws_config_path, instance_name+'_id')
|
|
|
|
try_to_load_instance_id
|
|
|
|
@aws_access_key_id = ""
|
|
@aws_secret_access_key = ""
|
|
|
|
|
|
if @config[:aws_profile] != 'default' or not @config[:aws_path].to_s.empty?
|
|
path = @config[:aws_path].to_s.empty? ? $aws_config_path : @config[:aws_path]
|
|
Logger.dbg("Initializing AWS config with creds from path: #{path} - profile: #{@config[:aws_profile]}")
|
|
shared_creds = Aws::SharedCredentials.new(
|
|
path: path,
|
|
profile_name: @config[:aws_profile]
|
|
)
|
|
Aws.config.update({
|
|
credentials: shared_creds
|
|
})
|
|
end
|
|
|
|
Aws.config.update({region: @config[:region]})
|
|
|
|
@ec2_client = Aws::EC2::Client.new(region: @config[:region])
|
|
@ec2 = Aws::EC2::Resource.new(client: @ec2_client)
|
|
end
|
|
|
|
def try_to_load_instance_id
|
|
if File.file? @instance_id_file
|
|
File.open(@instance_id_file) do |f|
|
|
@instance_id = f.gets.strip
|
|
Logger.dbg("Using instance ID: #{@instance_id}")
|
|
end
|
|
else
|
|
Logger.fail("No instance id file: #{@instance_id_file}")
|
|
end
|
|
end
|
|
|
|
def get_security_group_id
|
|
group_id = ""
|
|
@ec2_client.describe_security_groups do |security_group|
|
|
if security_group.group_name == @config[:sg_name]
|
|
group_id = security_group.group_id
|
|
break
|
|
end
|
|
end
|
|
|
|
group_id
|
|
end
|
|
|
|
def try_to_find_instance_id
|
|
Logger.dbg("Trying to find that specific instance online...")
|
|
insts = @ec2.instances({
|
|
filters: [
|
|
{
|
|
name: 'image-id',
|
|
values: [@config[:image_id]]
|
|
},
|
|
{
|
|
name: 'instance-type',
|
|
values: [@config[:instance_type]]
|
|
},
|
|
{
|
|
name: 'tag:name',
|
|
values: [@config[:instance_name]]
|
|
},
|
|
{
|
|
name: 'key-name',
|
|
values: [@config[:key_name]]
|
|
},
|
|
]
|
|
})
|
|
|
|
insts.each do |instance|
|
|
Logger.dbg("Checking instance with ID: #{instance.id}")
|
|
if instance.state.code == 48
|
|
Logger.fail("Instance is terminated. Leaving id file empty.")
|
|
File.open(@instance_id_file, 'w') do |f|
|
|
f.puts("")
|
|
end
|
|
@instance_id = ""
|
|
break
|
|
end
|
|
|
|
Logger.dbg("Found proper instance: #{instance.id} / #{instance.image_id}. Clobberring id file...")
|
|
@instance_id = instance.id
|
|
|
|
File.open(@instance_id_file, 'w') do |f|
|
|
f.puts(instance.id)
|
|
end
|
|
break
|
|
end
|
|
|
|
if not insts.any?
|
|
Logger.fail("Did not found any instance matching our criterias.")
|
|
end
|
|
|
|
@instance_id
|
|
end
|
|
|
|
def get_instance_id
|
|
if @checked and not @instance_id.to_s.empty?
|
|
Logger.dbg("Returning cached checked instance id.")
|
|
|
|
elsif not @instance_id.to_s.empty?
|
|
Logger.dbg("Checking if instance with ID = #{@instance_id} still exists.")
|
|
|
|
i = @ec2.instance(@instance_id)
|
|
if i.exists?
|
|
Logger.dbg("Instance still exists.")
|
|
@checked = true
|
|
else
|
|
Logger.dbg("Instance does not exist, clearing cached instance id in file.")
|
|
File.open(@instance_id_file, 'w') do |f|
|
|
f.puts("")
|
|
end
|
|
|
|
try_to_find_instance_id
|
|
end
|
|
end
|
|
|
|
Logger.info("Working on instance: #{@instance_id}")
|
|
@instance_id
|
|
end
|
|
|
|
def create(wait: false)
|
|
# group_id = get_security_group_id
|
|
# unless group_id
|
|
# Logger.fatal("Could not obtain EC2 Security Group ID by name: #{@config[:sg_name]}")
|
|
# end
|
|
|
|
Logger.dbg(%Q(Creating an instance:
|
|
AMI Image ID: #{@config[:image_id]}
|
|
EC2 Key Name: #{@config[:key_name]}
|
|
Security Group Name: #{@config[:sg_name]}
|
|
))
|
|
# Security Group ID: #{group_id}
|
|
|
|
instance = @ec2.create_instances({
|
|
image_id: @config[:image_id],
|
|
min_count: 1,
|
|
max_count: 1,
|
|
key_name: @config[:key_name],
|
|
security_groups: [@config[:sg_name]],
|
|
instance_type: @config[:instance_type],
|
|
tag_specifications: [
|
|
{
|
|
resource_type: 'instance',
|
|
tags: [
|
|
key: 'name',
|
|
value: @config[:instance_name],
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
if instance.any?
|
|
Logger.ok("Instance created. Waiting for it to get into running state...")
|
|
else
|
|
Logger.fail("Could not spin up an instance! Something went wrong but will proceed anyway...")
|
|
end
|
|
|
|
# Wait for the instance to be created, running, and passed status checks
|
|
inst = instance.first
|
|
if wait
|
|
puts "Waiting for instance to get it up & running. This might take a couple of minutes."
|
|
@ec2.client.wait_until(:instance_status_ok, {instance_ids: [inst.id]})
|
|
else
|
|
Logger.ok("Instance up & initializing.")
|
|
end
|
|
|
|
File.open(@instance_id_file, 'w') do |f|
|
|
f.puts(inst.id)
|
|
end
|
|
|
|
if @config[:quiet]
|
|
puts "created"
|
|
elsif @config[:verbose] or @config[:debug]
|
|
puts "Created instance: inst.id"
|
|
end
|
|
end
|
|
|
|
def start(wait: false)
|
|
state = status(raw: true)
|
|
|
|
Logger.info("Instance is in state: #{state}")
|
|
|
|
if state == 'notcreated'
|
|
create(wait: wait)
|
|
else
|
|
if get_instance_id.to_s.empty?
|
|
Logger.fatal("No instance that could be started.")
|
|
end
|
|
|
|
i = @ec2.instance(@instance_id)
|
|
if i.exists?
|
|
case i.state.code
|
|
when 0 # pending
|
|
puts "#{i.id} is pending, so it will be running in a bit"
|
|
when 16 # started
|
|
puts "#{i.id} is already started"
|
|
when 48 # terminated
|
|
puts "#{i.id} is terminated, gotta create another one."
|
|
create(wait: wait)
|
|
else
|
|
puts "Started instance. Please wait couple of minutes before doing SSH."
|
|
begin
|
|
i.start
|
|
return true
|
|
rescue Aws::EC2::Errors::IncorrectInstanceState => e
|
|
if e.include? "is not in a state from which it can be started."
|
|
Logger.fatal("EC2 instance is in a state from which it can't be started right now. Try later.")
|
|
else
|
|
Logger.fatal("Could not start EC2 instance: #{e}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
def stop
|
|
if get_instance_id.to_s.empty?
|
|
Logger.fatal("No instance that could be stopped.")
|
|
end
|
|
|
|
i = @ec2.instance(@instance_id)
|
|
if i.exists?
|
|
case i.state.code
|
|
when 48 # terminated
|
|
puts "#{i.id} is already stopped."
|
|
when 64 # stopping
|
|
puts "#{i.id} is stopping, so it become stopped in a while."
|
|
when 80 # stopped
|
|
puts "#{i.id} is already stopped."
|
|
else
|
|
puts "Stopped an instance."
|
|
i.stop
|
|
end
|
|
end
|
|
end
|
|
|
|
def restart
|
|
if get_instance_id.to_s.empty?
|
|
Logger.fatal("No instance that could be restarted.")
|
|
end
|
|
|
|
i = @ec2.instance(@instance_id)
|
|
if i.exists?
|
|
case i.state.code
|
|
when 48 # terminated
|
|
start
|
|
else
|
|
puts "Issued instance reboot signal."
|
|
i.reboot
|
|
end
|
|
end
|
|
end
|
|
|
|
def terminate
|
|
if get_instance_id.to_s.empty?
|
|
Logger.fatal("No instance that could be terminated.")
|
|
end
|
|
|
|
i = @ec2.instance(@instance_id)
|
|
if i.exists?
|
|
case i.state.code
|
|
when 48 # terminated
|
|
puts "#{i.id} is already terminated."
|
|
else
|
|
puts "Terminated instance. Cleared instance id file."
|
|
i.terminate
|
|
File.open(@instance_id_file, 'w') do |f|
|
|
f.puts("")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def address(raw: false)
|
|
if get_instance_id.to_s.empty?
|
|
if @config[:quiet]
|
|
exit 1
|
|
end
|
|
Logger.fatal("No instance id to operate on. Instance will need to be created first.")
|
|
end
|
|
|
|
addr = ""
|
|
i = @ec2.instance(@instance_id)
|
|
if i.exists?
|
|
Logger.dbg("Instance found.")
|
|
|
|
addr = i.public_ip_address
|
|
|
|
if not raw
|
|
if @config[:verbose]
|
|
puts "Public IPv4:\t#{i.public_ip_address}"
|
|
puts "Public FQDN:\t#{i.public_dns_name}"
|
|
puts "Private IPv4:\t#{i.private_ip_address}"
|
|
puts "Private FQDN:\t#{i.private_dns_name}"
|
|
else
|
|
puts i.public_ip_address
|
|
end
|
|
end
|
|
end
|
|
|
|
addr
|
|
end
|
|
|
|
def status(raw: false, quitOnFailure: true)
|
|
get_instance_id
|
|
|
|
if @instance_id.to_s.empty?
|
|
Logger.fail("No instance id stored locally. Will try to look up online.")
|
|
if not try_to_find_instance_id
|
|
puts("No instance created.")
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
if @instance_id.to_s.empty? and quitOnFailure
|
|
state = "notcreated"
|
|
if not raw
|
|
puts "State: notcreated"
|
|
end
|
|
return state
|
|
end
|
|
|
|
state = ""
|
|
inst = @ec2.instance(@instance_id)
|
|
if inst.exists?
|
|
state = inst.state.name
|
|
if not raw
|
|
if @config[:verbose] or @config[:debug]
|
|
puts "State:\t\tInstance is #{inst.state.name} (#{inst.state.code})"
|
|
|
|
if inst.state.code == 16
|
|
dif = ((Time.now - inst.launch_time) / 60).ceil
|
|
t = inst.launch_time
|
|
stime = t.strftime("%Y-%m-%d %I:%M%P UTC")
|
|
puts "Launched time:\t#{stime}; #{dif.to_s} minutes ago."
|
|
end
|
|
else
|
|
puts inst.state.name
|
|
end
|
|
end
|
|
else
|
|
state = "notcreated"
|
|
if not raw
|
|
puts "State: notcreated"
|
|
end
|
|
end
|
|
|
|
state
|
|
end
|
|
|
|
def ssh
|
|
state = status(raw: true)
|
|
|
|
if state == 'stopped'
|
|
puts "Instance is stopped. Creating it first."
|
|
if not start(wait: true)
|
|
Logger.fatal("Could not create EC2 instance.")
|
|
end
|
|
|
|
state = 'running'
|
|
end
|
|
|
|
if state == 'running'
|
|
addr = address(raw: true)
|
|
|
|
cmd = "ssh -i #{@config[:ssh_identity_file]} -o ConnectTimeout=10 -oStrictHostKeyChecking=no #{@config[:ssh_user]}@#{addr}"
|
|
Logger.dbg("Running command: #{cmd}")
|
|
|
|
puts "Attempting to ssh #{@config[:ssh_user]}@#{addr} ...\n\n"
|
|
exec(cmd)
|
|
end
|
|
|
|
raise "Unsupported EC2 machine state: #{state}"
|
|
end
|
|
|
|
def notify
|
|
get_instance_id
|
|
|
|
if @instance_id.to_s.empty?
|
|
if not try_to_find_instance_id
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
inst = @ec2.instance(@instance_id)
|
|
if inst.exists?
|
|
Logger.dbg('Instance exists.')
|
|
if inst.state.code == 16
|
|
Logger.dbg('Instance is running.')
|
|
minutes = ((Time.now - inst.launch_time) / 60).ceil
|
|
|
|
title = "EC2 Instance #{@config[:instance_name]} is running."
|
|
body = ""
|
|
if minutes < 60
|
|
body = "Your instance has been running #{minutes} minutes by now. Consider stopping it."
|
|
else
|
|
hours = (minutes / 60).floor
|
|
restm = minutes % 60
|
|
body = "Your instance's been running #{hours}h and #{restm}mins by now. Consider stopping it."
|
|
end
|
|
|
|
cmd = "notify-send '#{title}' '#{body}'"
|
|
|
|
Logger.dbg("Executing notification command: #{cmd}")
|
|
exec(cmd)
|
|
puts cmd
|
|
else
|
|
Logger.dbg("Instance is not running.")
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
def parse_options
|
|
options = {}
|
|
parser = OptionParser.new do |opts|
|
|
funcs = ""
|
|
$supported_funcs.each do |k, v|
|
|
funcs += " - #{k}\t\t\t#{v}\n"
|
|
end
|
|
|
|
opts.banner = %Q(
|
|
Usage: aws-manager.rb [options] <func> <name>
|
|
|
|
Available 'func' values:
|
|
#{funcs}
|
|
Options:
|
|
)
|
|
opts.on('-h', '--help', 'Display this screen' ) do
|
|
puts opts
|
|
exit
|
|
end
|
|
|
|
opts.on('-q', '--quiet', 'Surpress informative output.') do |q|
|
|
$config[:quiet] = q
|
|
end
|
|
|
|
opts.on('-v', '--verbose', 'Turn on verbose logging.') do |v|
|
|
$config[:verbose] = v
|
|
end
|
|
|
|
opts.on('--debug', 'Turn on debug logging.') do |d|
|
|
$config[:debug] = d
|
|
end
|
|
|
|
opts.on('-dPATH', '--aws-path=PATH', "Path to shared AWS credentials file. Default value that will be used: $AWS_PATH/credentials ") do |p|
|
|
$config[:aws_path] = p
|
|
end
|
|
|
|
opts.on('-pNAME', '--profile=NAME', 'AWS credentials profile to use. Should no option is given, "default" is used.') do |n|
|
|
$config[:aws_profile] = n
|
|
end
|
|
|
|
opts.on('-pREGION', '--region=REGION', 'AWS regoin to use. Default one: "us-east-1".') do |n|
|
|
$config[:region] = n
|
|
end
|
|
|
|
opts.on('-iID', '--image-id=ID', "AWS image ID to create an EC2 from. Default: '#{$config[:image_id]}") do |i|
|
|
$config[:image_id] = i
|
|
end
|
|
|
|
opts.on('-kKEY', '--key-name=KEY', "AWS EC2 Key Name to use. Default: '#{$config[:key_name]}") do |k|
|
|
$config[:key_name] = k
|
|
end
|
|
|
|
opts.on('-sNAME', '--security-group-name=NAME', "AWS EC2 Security Group name to use. Default: '#{$config[:sg_name]}") do |s|
|
|
$config[:sg_name] = s
|
|
end
|
|
|
|
opts.on('-tTYPE', '--instance-type=TYPE', "Instance type to spin. Default: '#{$config[:instance_type]}") do |s|
|
|
$config[:instance_type] = s
|
|
end
|
|
|
|
opts.on('-uUSER', '--user=USER', "SSH user to log into when doing 'ssh'. Default: '#{$config[:ssh_user]}") do |s|
|
|
$config[:ssh_user] = s
|
|
end
|
|
|
|
end
|
|
|
|
args = parser.parse!
|
|
|
|
func = args.shift.downcase
|
|
raise "Need to specify <func> parameter to invoke." unless func
|
|
|
|
unless $supported_funcs.keys.include? func
|
|
Logger.fatal("Unsupported function specified. You need to pick on of these: #{$supported_funcs.keys.join(', ')}")
|
|
end
|
|
|
|
instance_name = args.shift.downcase
|
|
$config[:instance_name] = instance_name
|
|
|
|
raise "Need to specify <name> parameter to invoke." unless instance_name
|
|
|
|
raise "EC2 Security Group name must be specified." unless $config[:sg_name]
|
|
raise "EC2 Key pair name must be specified." unless $config[:key_name]
|
|
raise "EC2 image id (AMI) must be specified." unless $config[:image_id]
|
|
raise "EC2 instance type must be specified." unless $config[:instance_type]
|
|
raise "EC2 instance name must be specified." unless $config[:instance_name]
|
|
raise "EC2 instance name must be specified." unless $config[:ssh_user]
|
|
|
|
return func, instance_name
|
|
end
|
|
|
|
def main
|
|
func, instance_name = parse_options
|
|
Logger.dbg("Using AWS configuration path: #{$aws_config_path}")
|
|
Logger.dbg("Action to take: #{func} #{instance_name}")
|
|
|
|
manager = AwsEc2Manager.new(func, instance_name, $config)
|
|
manager.send func
|
|
end
|
|
|
|
main
|