diff --git a/clouds/aws/README.md b/clouds/aws/README.md index a3eeba8..0432ef2 100644 --- a/clouds/aws/README.md +++ b/clouds/aws/README.md @@ -96,3 +96,5 @@ Afterwards, one should see following logs in CloudWatch traces for planted Lambd - **`exfiltrateLambdaTasksDirectory.py`** - Script that creates an in-memory ZIP file from the entire directory `$LAMBDA_TASK_ROOT` (typically `/var/task`) and sends it out in a form of HTTP(S) POST request, within an `exfil` parameter. To be used for exfiltrating AWS Lambda's entire source code. - **`identifyS3Bucket.rb`** - This script attempts to identify passed name whether it resolves to a valid AWS S3 Bucket via different means. This script may come handy when revealing S3 buckets hidden behind HTTP proxies. + +- **`pentest-ec2-instance`** - A set of utilities for quick starting, ssh-ing and stopping of a single temporary EC2 instance intended to be used for Web out-of-band tests (SSRF, reverse-shells, dns/http/other daemons). diff --git a/clouds/aws/identifyS3Bucket.rb b/clouds/aws/identifyS3Bucket.rb old mode 100644 new mode 100755 diff --git a/clouds/aws/pentest-ec2-manager/Gemfile b/clouds/aws/pentest-ec2-manager/Gemfile new file mode 100644 index 0000000..f188a4a --- /dev/null +++ b/clouds/aws/pentest-ec2-manager/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true +source "https://rubygems.org" +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +gem "aws-sdk-ec2" diff --git a/clouds/aws/pentest-ec2-manager/README.md b/clouds/aws/pentest-ec2-manager/README.md new file mode 100644 index 0000000..013912c --- /dev/null +++ b/clouds/aws/pentest-ec2-manager/README.md @@ -0,0 +1,97 @@ +# pentest-ec2-manager + +A set of utilities for quick starting, ssh-ing and stopping of temporary EC2 instances intended to be used for Web out-of-band tests (SSRF, reverse-shells, dns/http/other daemons). +Those scripts are useful for managing *single* EC2 instance, which is picked based on specific characteristics: `key-name`, `image-id`, `security-group-name`, `instance-type`. + +Most common use case: +- _Performing SSRF tests._ - When you want to quickly assert there is out-of-band request going over to attacker-controlled machine + +**CAUTION NOTE**: Files in this repository are preconfigured/hardcoded with some intial specific configurations. Those configurations are placed on top of each script file, one can change them easily. + + +### Installation + +Installation is pretty straightforward assuming you have AWS account already and AWS Access Key (AKIA...) and AWS Secret Key with you. + +If you have AWS account, installation can be started using `init.sh` script. This script assumes you can use `sudo` to pull in prerequisities. +``` +bash $ ./init.sh + +---------------------------------------------- + +:: AWS EC2 single-instance management utilities installation script. + +This script is going to: + - Update your repos & install packages such as: ssh, cron, jq, ruby, rubygems, awscli, gem bundler, gem 'aws-sdk-ec2' + - Configure your AWS credentials + - Create AWS security groups, EC2 key pairs + - Integrate EC2 management aliases into the end of your .bashrc + - Add a cron job that will notify you every two hours if your EC2 machine is up and running + +---------------------------------------------- + +Would you like to proceed? [Y/n] +``` + +After hitting `Y` it shall configure AWS CLI for you, pull `aws-sdk-ec2` for ruby and then create Security Groups, Key Pairs and append things to your bashrc. + + +### Usage + +After script installation is over, your bash will get preloaded (in `~/.bashrc`) with following aliases: + +* `startpentestec2` - Starts EC2 Instance if it exists, otherwise creates it +* `stoppentestec2` - Stops EC2 Instance +* `terminatepentestec2` - Terminates EC2 Instance (which means also deletes that instance's Elastic Block Store / hdd) +* `sshpentestec2` - Attemtps to SSH into managed EC2 instance. +* `getpentestec2` - Obtains IPv4 address of managed EC2 instance. +* `checkpentestec2` - Prints out EC2 Instance status (running, stopped, notcreated, pending, etc) + +If you do not want to use bash aliases, or prefer having it your own way, you can call the `aws-manager.rb` script directly: + +``` +bash $ ruby aws-manager.rb --help + +Usage: aws-manager.rb [options] + +Available 'func' values: + - 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. + +Options: + -h, --help Display this screen + -q, --quiet Surpress informative output. + -v, --verbose Turn on verbose logging. + --debug Turn on debug logging. + -d, --aws-path=PATH Path to shared AWS credentials file. Default value that will be used: $AWS_PATH/credentials + --profile=NAME AWS credentials profile to use. Should no option is given, "default" is used. + -p, --region=REGION AWS regoin to use. Default one: "us-east-1". + -i, --image-id=ID AWS image ID to create an EC2 from. Default: 'ami-07360d1b1c9e13198 + -k, --key-name=KEY AWS EC2 Key Name to use. Default: 'ec2-pentest-key + -s, --security-group-name=NAME AWS EC2 Security Group name to use. Default: 'ec2-pentest-usage + -t, --instance-type=TYPE Instance type to spin. Default: 't2.micro + -u, --user=USER SSH user to log into when doing 'ssh'. Default: 'ec2-user + +``` + + +Typical usage boils down to issuing `func` operations. Eventually one would like to observe what happens under the hood using one of those flags: +* `-v` +* `--debug` + +On the other hand, it is possible to surpress script's output nearly entirely using: +* `-q` +flag. + + +### TODO + +* Test, bug fixes +* Support different Regions. Currently scripts are fixed on using one region. +* Support more than one instance. Very far aim to reach. diff --git a/clouds/aws/pentest-ec2-manager/aws-manager.rb b/clouds/aws/pentest-ec2-manager/aws-manager.rb new file mode 100755 index 0000000..88decc8 --- /dev/null +++ b/clouds/aws/pentest-ec2-manager/aws-manager.rb @@ -0,0 +1,631 @@ +#!/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 B., '19, +# + +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] + +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 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 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 diff --git a/clouds/aws/pentest-ec2-manager/ec2-utils.sh b/clouds/aws/pentest-ec2-manager/ec2-utils.sh new file mode 100755 index 0000000..9d0fe3a --- /dev/null +++ b/clouds/aws/pentest-ec2-manager/ec2-utils.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +EC2_NAME=pentestec2 + +# +# Will set the following aliases: +# - ssh alias for quick ssh-connection +# - get alias for quick vm's ip resolution +# - start alias for starting up particular vm +# - stop alias for stopping particular vm +# - is alias for checking whether the vm is running. +# +# For instance, when EC2_NAME=pentestec2 - the following aliases will be defined: +# sshpentestec2, getpentestec2, ispentestec2, and so on... +# +function setup_aliases() { + name=${EC2_NAME,,} + if [ -z $name ]; then + echo "[!] You must set the EC2_NAME variable within that script first!" + return 1 + fi + + alias start$name="startec2" + alias stop$name="stopec2" + alias terminate$name="terminateec2" + alias ssh$name="sshec2" + alias get$name="getec2" + alias check$name="checkec2" +} + +function startec2() { + ruby ./aws-manager.rb "$@" start $EC2_NAME +} + +function stopec2() { + ruby ./aws-manager.rb "$@" stop $EC2_NAME +} + +function terminateec2() { + ruby ./aws-manager.rb "$@" terminate $EC2_NAME +} + +function sshec2() { + ruby ./aws-manager.rb "$@" ssh $EC2_NAME +} + +function getec2() { + ruby ./aws-manager.rb "$@" address $EC2_NAME +} + +function checkec2() { + ruby ./aws-manager.rb "$@" status $EC2_NAME +} + +setup_aliases diff --git a/clouds/aws/pentest-ec2-manager/init.sh b/clouds/aws/pentest-ec2-manager/init.sh new file mode 100755 index 0000000..983c5c2 --- /dev/null +++ b/clouds/aws/pentest-ec2-manager/init.sh @@ -0,0 +1,281 @@ +#!/bin/bash + +AWS_REGION=us-east-1 +EC2_NAME=pentestec2 +EC2_KEY_NAME=ec2-pentest-key +EC2_SECURITY_GROUP_NAME=ec2-pentest-usage +MANAGEMENT_SCRIPT=ec2-utils.sh + +install_prerequisities() { + if [[ ! -z "$(which ruby)" ]] && [[ ! -z "$(which gem)" ]] && [[ ! -z "$(which bundle)" ]]; then + echo "[.] Prerequisities already installed." + return + fi + + sudo=sudo + if [ $EUID -eq 0 ] || [[ ! -z "$(which sudo)" ]]; then + sudo= + fi + + if [[ ! -z "$(which dnf)" ]]; then + $sudo dnf update + $sudo dnf install -y ssh cron ruby rubygems awscli jq + elif [[ ! -z "$(which apt-get)" ]]; then + $sudo apt-get update + $sudo apt-get install -y ssh cron ruby rubygems awscli jq + elif [[ ! -z "$(which yum)" ]]; then + $sudo yum update + $sudo yum install -y ssh cron ruby rubygems awscli jq + else + echo "[!] Please install following packages: ssh, cron, ruby, rubygems, awscli, jq yourself first." + exit 1 + fi + + gem install bundler + bundle install + + aw=$(which aws) + if [ -z "$aw" ]; then + echo "[!] Something went wrong. There is no 'aws' after installing 'awscli'." + exit 1 + fi +} + +configure_credentials() { + if [ -e ~/.aws/credentials ]; then + con=$(cat ~/.aws/credentials) + if echo $con | grep -q AKIA; then + echo "[.] AWS CLI already configured." + return + fi + fi + + if [ ! -d "~/.aws" ]; then + mkdir -p ~/.aws + fi + + access_key= + secret_key= + read -p 'AWS Access Key (AKIA...): ' access_key + read -s -p "AWS Secret Access Key: " secret_key + + if [ "$secret_key" == "" ] || [ "$access_key" == "" ]; then + echo "[!] You must specify both access key and secret key." + exit 1 + fi + + touch ~/.aws/config + touch ~/.aws/credentials + + cat < ~/.aws/config +[default] +region = $AWS_REGION +output = json +EOT + + cat < ~/.aws/credentials +[default] +aws_access_key_id = $access_key +aws_secret_access_key = $secret_key +EOT +} + +create_security_group() { + groupId=$(aws ec2 create-security-group --group-name $EC2_SECURITY_GROUP_NAME --description "Security Group with allow any any for inbound and outbound." 2>&1 ) + + if echo $groupId | grep -q "InvalidGroup.Duplicate"; then + echo "[.] Security Group already exists." + return + elif echo $groupId | grep -q "An error occurred" ; then + echo "[!] Failed: $groupId" + exit 1 + fi + + groupId=$(echo $groupId | jq -r .GroupId) + + echo "[>] Security Group ID: $groupId" + aws ec2 authorize-security-group-ingress --group-id $groupId --ip-permissions IpProtocol=tcp,FromPort=0,ToPort=65535,IpRanges='[{CidrIp=0.0.0.0/0,Description="allow any any"}]' + aws ec2 authorize-security-group-ingress --group-id $groupId --ip-permissions IpProtocol=udp,FromPort=0,ToPort=65535,IpRanges='[{CidrIp=0.0.0.0/0,Description="allow any any"}]' + aws ec2 authorize-security-group-egress --group-id $groupId --ip-permissions IpProtocol=tcp,FromPort=0,ToPort=65535,IpRanges='[{CidrIp=0.0.0.0/0,Description="allow any any"}]' + aws ec2 authorize-security-group-egress --group-id $groupId --ip-permissions IpProtocol=udp,FromPort=0,ToPort=65535,IpRanges='[{CidrIp=0.0.0.0/0,Description="allow any any"}]' + + echo "[>] Created inbound/outbound allow any any rules" +} + +delete_key_pair_and_instances() { + echo "[.] Deleting key pair..." + out=$(aws ec2 delete-key-pair --key-name $EC2_KEY_NAME 2>&1 ) + if echo $out | grep -q "An error occurred" ; then + echo "[!] Deleting key pair failed: $out" + exit 1 + fi + + echo "[.] Terminating related EC2 instances..." + out=$(aws ec2 terminate-instances --instance-ids $(aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --filters "Name=key-name,Values=$EC2_KEY_NAME" --output text) 2>&1 ) + if echo $out | grep -q "An error occurred" ; then + echo "[!] Terminating instances failed: $out" + exit 1 + fi +} + +create_ec2_key_pair() { + keymaterial= + out=$(aws ec2 describe-key-pairs --key-name $EC2_KEY_NAME 2>&1 ) + if echo $out | jq -r .KeyPairs[0].KeyName | grep -q $EC2_KEY_NAME ; then + echo "[.] Key pair already created in your AWS account." + + filefinger= + if [ -f "~/.aws/$EC2_KEY_NAME.pem" ] ; then + filefinger=$(openssl pkcs8 -in ~/.aws/$EC2_KEY_NAME.pem -inform PEM -outform DER -topk8 -nocrypt | openssl sha1 -c | cut -d= -f2 | tr -d ' ') + fi + awsfinger=$(echo $out | jq -r .KeyPairs[0].KeyFingerprint) + + if [ ! -e ~/.aws/$EC2_KEY_NAME.pem ]; then + echo + echo "[!] ERROR: It looks like you don't have ~/.aws/$EC2_KEY_NAME.pem file with your EC2 Key-Pair" + echo "[!] This means you do have the key pair on your AWS account but not physically in a file." + echo "[!] In such case you will be unable to SSH into previously created EC2 instances without it, so " + echo "[!] you are left with creating a new key pair and then terminating existing EC2 instances." + echo + read -p "Do you want to remove your $EC2_NAME EC2 instance and recreate key pairs? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] + then + delete_key_pair_and_instances + else + echo "[-] Unable to continue. You must sort this out yourself." + return + fi + elif [ "$filefinger" != "" ] && [ "$awsfinger" != "" ] && [ "$filefinger" != "$awsfinger" ]; then + echo + echo "[!] ERROR: It looks like your EC2 Key Pair located at ~/.aws/$EC2_KEY_NAME.pem" + echo "[!] has differrent fingerprint than the Key Pair on your AWS account." + echo "[!] In such case you will be unable to SSH into previously created EC2 instances using it, so " + echo "[!] you are left with creating a new key pair and then terminating existing EC2 instances." + echo + echo "Your locally stored PEM fingerprint: $filefinger" + echo "Your on AWS stored PEM fingerprint: $awsfinger" + echo + read -p "Do you want to remove your $EC2_NAME EC2 instance and recreate key pairs? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] + then + delete_key_pair_and_instances + else + echo "[-] Unable to continue. You must sort this out yourself." + return + fi + else + echo "[+] EC2 Key pair already created. PEM stored at: ~/.aws/$EC2_KEY_NAME.pem" + return + fi + fi + + keymaterial=$(aws ec2 create-key-pair --key-name $EC2_KEY_NAME 2>&1 ) + if echo $keymaterial | grep -q "An error occurred" ; then + echo "[!] Creating key pair failed: $keymaterial" + exit 1 + fi + + touch ~/.aws/$EC2_KEY_NAME.pem + echo $keymaterial | jq -r .KeyMaterial > ~/.aws/$EC2_KEY_NAME.pem + chmod 400 ~/.aws/$EC2_KEY_NAME.pem + + echo "[+] EC2 Key pair created. PEM stored at: ~/.aws/$EC2_KEY_NAME.pem" +} + +integrate_with_bashrc() { + if cat ~/.bashrc | grep -q $MANAGEMENT_SCRIPT ; then + echo "[.] Script is already integrated." + return + elif cat ~/.bashrc | grep -q "Adding aliases for EC2 single-instance management" ; then + echo "[.] Script is already integrated." + return + fi + + sed -i -e "s@ruby ./aws-manager.rb@ruby $PWD/aws-manager.rb@" ./$MANAGEMENT_SCRIPT + + source $PWD/$MANAGEMENT_SCRIPT + cat <> ~/.bashrc + +# Adding aliases for EC2 single-instance management. +source $PWD/$MANAGEMENT_SCRIPT + +EOT +} + +add_cron_job() { + crontab -l > /tmp/.cr 2> /dev/null + cp /tmp/.cr /tmp/.cr.bak + + if cat /tmp/.cr | grep -q "aws-manager.rb notify "; then + echo "[.] Crontab job already scheduled." + return + fi + + cmd="$(which ruby) $PWD/aws-manager.rb notify $EC2_NAME" + job=" * */2 * * * $USER $cmd" + + echo -e "[*] Adding following line to crontab:\n\n\t\t$job\n" + + echo $job >> /tmp/.cr + crontab /tmp/.cr + if [ $? -ne 0 ]; then + echo "[!] Updating crontab failed. Restoring original one (you can find it at: /tmp/.cr.bak) " + crontab /tmp/.cr.bak + fi + rm /tmp/.cr + #rm /tmp/.cr.bak +} + +echo +echo "----------------------------------------------" +echo +echo ":: AWS EC2 single-instance management utilities installation script." +echo +echo "This script is going to:" +echo -e "\t- Update your repos & install packages such as: ssh, cron, jq, ruby, rubygems, awscli, gem bundler, gem 'aws-sdk-ec2'" +echo -e "\t- Configure your AWS credentials" +echo -e "\t- Create AWS security groups, EC2 key pairs" +echo -e "\t- Integrate EC2 management aliases into the end of your .bashrc" +echo -e "\t- Add a cron job that will notify you every two hours if your EC2 machine is up and running" +echo +echo "----------------------------------------------" +echo +read -p "Would you like to proceed? [Y/n] " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]] +then + echo -e "\n[.] Step 1: Installing prerequisities first." + install_prerequisities + + echo -e "\n[.] Step 2: Configuring credentials." + configure_credentials + + echo -e "\n\n[.] Step 3: Creating Security Group." + create_security_group + + echo -e "\n[.] Step 4: Creating EC2 Key Pairs." + create_ec2_key_pair + + echo -e "\n[.] Step 5: Integrating scripts with your .bashrc" + integrate_with_bashrc + + echo -e "\n[.] Step 6: Adding notification cron job" + add_cron_job + + echo -e "\n[+] Ready to roll." + echo + echo "----------------------------------------------" + echo + echo -e "You can now try out our shiny new bash aliases:" + echo -e "\t- check$EC2_NAME - To check an instance status" + echo -e "\t- get$EC2_NAME - To get the instance IPv4 address." + echo -e "\t- start$EC2_NAME - To start the instance." + echo -e "\t- stop$EC2_NAME - To stop the instance." + echo -e "\t- terminate$EC2_NAME - To terminate the instance." + echo -e "\t- ssh$EC2_NAME - To ssh into that instance" + echo + echo "Go ahead, check them out. Start with creating EC2 first using 'start$EC2_NAME'" + bash +fi