From 26d344a0ef53559506c983d5b73566aaeed13482 Mon Sep 17 00:00:00 2001 From: Javier Godinez Date: Sun, 12 Mar 2017 22:57:58 -0700 Subject: [PATCH] Initial checkin of launch instances module --- .../admin/aws/aws_launch_instances.md | 166 ++++++++++++ .../admin/aws/aws_launch_instances.rb | 240 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 documentation/modules/auxiliary/admin/aws/aws_launch_instances.md create mode 100644 modules/auxiliary/admin/aws/aws_launch_instances.rb diff --git a/documentation/modules/auxiliary/admin/aws/aws_launch_instances.md b/documentation/modules/auxiliary/admin/aws/aws_launch_instances.md new file mode 100644 index 0000000000..5393b9e6cb --- /dev/null +++ b/documentation/modules/auxiliary/admin/aws/aws_launch_instances.md @@ -0,0 +1,166 @@ + +# Introduction + +The Launch Instances module (`aws_launch_instances`) can be used to launch a +Cloud host with a public IP address. Although hosts can be launched using the +Web console or the CLI, launching a host in the Cloud requires a fair +amount of configuration; this module does its best to abstract configuration +requirements away from the user by auto detecting the VPC, subnets, creating +security groups, etc. + + +# Background + +## AWS API Access Keys + +API access keys can be used to make calls against the AWS API, to say +retrieve deployment packages from S3. + +## VPC + +The VPC or Virtual Private Cloud, an isolated local area network. Network access +can be made available by assigning an Internet routable IP address to a host or +routing traffic to it through an ELB (Elastic Load Balancer). In either case +security-groups are used to open access to network ranges and specific TPC/UDP +ports. Security-groups provide much of the functionality of traditional firewalls +and can be configured by specifyig a protocol, a CIDR and a port. + +## How it Works + +The module performs several tasks to launch a host with a public IP address, +these are as follow: 1) select a VPC, 2) select a subnet, 3) create/select a +security group, 4) create/select a key-pair, and 5) launch a host. + +The module will attempt to launch the host in the first VPC it finds in the +given region (`Region` option). Most of the time there is only one VPC per +account per region, however one might find multiple VPCs within the same region. +In this case, one may use the `VPC_ID` advanced option to specify the VPC to +use. Selecting a subnet is a bit more complicated. To have traffic routed +between us and the Cloud host, a public subnet (a subnet that is routable to an +Internet gateway) must be selected and the Cloud host must be associated with +an Internet routable IP address. The module dynamically finds which subnet to +launch the host in. It will use the first subnet it finds having the +`Auto-assign Public IP` option set, if no such subnet exists, then it will +select the first subnet having an Internet gateway. To circumvent this process, +the `SUBNET_ID` advanced option can be set. + +When launching a Cloud host at least one security group is required. There are +several advanced options for creating/selecting a security group. The +`SEC_GROUP_ID` option works much in the same way the `VPC_ID` option does. +That is, the module will create a security group unless the `SEC_GROUP_ID` +options is set. If the `SEC_GROUP_ID` option is not set, the module will attempt +to create a security group using the values specified in the `SEC_GROUP_CIDR`, +`SEC_GROUP_NAME`, and `SEC_GROUP_PORT` options as configuration. + +The `KEY_NAME` and `SSH_PUB_KEY` options are used in conjunction to select or +create a key-pair (a named SSH public key). Key-pairs are used to authenticate +to the host once it is running. The `KEY_NAME` defaults to admin while +`SSH_PUB_KEY` is optional. If the `SSH_PUB_KEY` is left unset, then the module +will not attempt to create a key-pair and will simply attempt to launch the +instance using an existing key-pair denoted by `KEY_NAME`. To set the +`SSH_PUB_KEY` option, a public SSH key must be provided as can be generated by +`ssh-keygen -y -f `. Once a key-pair is created/selected, +the module launches the host via the AWS API specifying that it should +associate a public IP address. + +## Options + +The Launch Instances module is an auxiliary module that can be loaded using the +use command. To run the module, only the `AccessKeyId`, `SecretAccessKey`, and +`KEY_NAME` options are required. + +Basic Options: + +* `AMI_ID`: The Amazon Machine Image (AMI) ID (region dependent) +* `RHOST`: the AWS EC2 Endpoint (ec2.us-west-2.amazonaws.com), may change this to something closer to you +* `Region`: The default region (us-west-2), must match endpoint +* `AccessKeyId`: AWS API access key +* `SecretAccessKey`: AWS API secret access key +* `Token`: AWS API session token, optional +* `KEY_NAME`: The SSH key to be used for ec2-user +* `SSH_PUB_KEY`: The public SSH key to be used for ec2-user, e.g., "ssh-rsa ABCDE..." +* `USERDATA_FILE`: The script that will be executed on start + +Advanced Options: + +* `INSTANCE_TYPE`: The instance type +* `MaxCount`: Maximum number of instances to launch +* `MinCount`: Minumum number of instances to launch +* `ROLE_NAME`: The instance profile/role name +* `RPORT:` AWS EC2 Endpoint TCP Port +* `SEC_GROUP_ID`: the EC2 security group to use +* `SEC_GROUP_CIDR`: the EC2 security group network access CIDR, defaults to 0.0.0.0/0 +* `SEC_GROUP_NAME`: the EC2 security group name +* `SEC_GROUP_PORT`: the EC2 security group network access port, defaults to tcp:22 +* `SUBNET_ID`: The public subnet to use +* `UserAgent`: The User-Agent header to use for all requests +* `VPC_ID`: The EC2 VPC ID + +# Usage + +The Launch Instances module is an auxiliary module that can be loaded using the +use command. To run the module, only the `AccessKeyId`, `SecretAccessKey`, and +`KEY_NAME` options are required. + +``` +msf > use auxiliary/admin/aws/aws_launch_instances +msf auxiliary(aws_launch_instances) > show options + +Module options (auxiliary/admin/aws/aws_launch_instances): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + AMI_ID ami-1e299d7e yes The Amazon Machine Image (AMI) ID + AccessKeyId yes AWS access key + KEY_NAME admin yes The SSH key to be used for ec2-user + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOST ec2.us-west-2.amazonaws.com yes AWS region specific EC2 endpoint + Region us-west-2 yes The default region + SSH_PUB_KEY no The public SSH key to be used for ec2-user, e.g., "ssh-rsa ABCDE..." + SecretAccessKey yes AWS secret key + Token no AWS session token + USERDATA_FILE no The script that will be executed on start + +msf auxiliary(aws_launch_instances) > set SecretAccessKey asdfasd+asdfasdfasd... +SecretAccessKey => asdfasd+asdfasdfasd... +msf auxiliary(aws_launch_instances) > set AccessKeyId AKIAAKIAAKIAAKIAAKIAA +AccessKeyId => AKIAJH47PFECK3EVTBKA +msf auxiliary(aws_launch_instances) > set KEY_NAME ec2-user-key +KEY_NAME => ec2-user-key +msf auxiliary(aws_launch_instances) > set SSH_PUB_KEY ssh-rsa ABCDEDG123... +SSH_PUB_KEY => ssh-rsa ABCDEDG123... +msf auxiliary(aws_launch_instances) > run + +[*] Created ec2-user-key (ab:cd:ef:12:34:56:78:90:ab:ac:ad:ab:a1:23:45:67) +[*] Created security group: sg-12345678 +[*] Launching instance(s) in us-west-2, AMI: ami-1e299d7e, key pair name: ec2-user, security group: sg-12345678, subnet ID: subnet-abcdefgh +[*] Launched instance i-12345678 in us-west-2 account 123456789012 +[*] instance i-12345678 status: initializing +[*] instance i-12345678 status: initializing +... +[*] instance i-12345678 status: ok +[*] Instance i-12345678 has IP address 54.186.158.6 +[*] Auxiliary module execution completed +``` + +When the host has passed its primary system checks, the IP address will be +displayed. We can use this IP address to SSH to the host. Please note that +most users will want to set the `SEC_GROUP_CIDR` option to restrict access to +our new Cloud host. + +To SSH into the host, you must specify the SSH key, and ec2-user username, e.g., + +``` +$ ssh -i ec2-user-key ec2-user@54.186.158.6 +The authenticity of host '54.186.158.6 (54.186.158.6)' can't be established. +ECDSA key fingerprint is SHA256:ePj6WtCeK... +Are you sure you want to continue connecting (yes/no)? yes +Warning: Permanently added '54.186.158.6' (ECDSA) to the list of known hosts. + __| __|_ ) + _| ( / Amazon Linux AMI + ___|\___|___| +https://aws.amazon.com/amazon-linux-ami/2016.09-release-notes/ +5 package(s) needed for security, out of 9 available +Run "sudo yum update" to apply all updates. +[ec2-user@ip-172-31-8-176 ~]$ +``` \ No newline at end of file diff --git a/modules/auxiliary/admin/aws/aws_launch_instances.rb b/modules/auxiliary/admin/aws/aws_launch_instances.rb new file mode 100644 index 0000000000..c5f6641bc6 --- /dev/null +++ b/modules/auxiliary/admin/aws/aws_launch_instances.rb @@ -0,0 +1,240 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'metasploit/framework/aws/client' + +class MetasploitModule < Msf::Auxiliary + + include Metasploit::Framework::Aws::Client + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => "Launches Hosts in AWS", + 'Description' => %q{ + This module will attempt to launch an AWS instances (hosts) in EC2. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Javier Godinez ', + ], + 'References' => [ + [ 'URL', 'https://drive.google.com/open?id=0B2Ka7F_6TetSNFdfbkI1cnJHUTQ'], + [ 'URL', 'https://published-prd.lanyonevents.com/published/rsaus17/sessionsFiles/4721/IDY-W10-DevSecOps-on-the-Offense-Automating-Amazon-Web-Services-Account-Takeover.pdf' ] + ] + ) + ) + register_options( + [ + OptString.new('AccessKeyId', [true, 'AWS access key', '']), + OptString.new('SecretAccessKey', [true, 'AWS secret key', '']), + OptString.new('Token', [false, 'AWS session token', '']), + OptString.new('RHOST', [true, 'AWS region specific EC2 endpoint', 'ec2.us-west-2.amazonaws.com']), + OptString.new('Region', [true, 'The default region', 'us-west-2' ]), + OptString.new("AMI_ID", [true, 'The Amazon Machine Image (AMI) ID', 'ami-1e299d7e']), + OptString.new("KEY_NAME", [true, 'The SSH key to be used for ec2-user', 'admin']), + OptString.new("SSH_PUB_KEY", [false, 'The public SSH key to be used for ec2-user, e.g., "ssh-rsa ABCDE..."', '']), + OptString.new("USERDATA_FILE", [false, 'The script that will be executed on start', '']) + ] + ) + register_advanced_options( + [ + OptString.new('RPORT', [true, 'AWS EC2 Endpoint TCP Port', 443]), + OptBool.new('SSL', [true, 'AWS EC2 Endpoint SSL', true]), + OptString.new('INSTANCE_TYPE', [true, 'The instance type', 'm3.medium']), + OptString.new('ROLE_NAME', [false, 'The instance profile/role name', '']), + OptString.new('VPC_ID', [false, 'The EC2 VPC ID', '']), + OptString.new('SUBNET_ID', [false, 'The public subnet to use', '']), + OptString.new('SEC_GROUP_ID', [false, 'The EC2 security group to use', '']), + OptString.new('SEC_GROUP_CIDR', [true, 'EC2 security group network access CIDR', '0.0.0.0/0']), + OptString.new('SEC_GROUP_PORT', [true, 'EC2 security group network access PORT', 'tcp:22']), + OptString.new('SEC_GROUP_NAME', [false, 'Optional EC2 security group name', '']), + OptInt.new('MaxCount', [true, 'Maximum number of instances to launch', 1]), + OptInt.new('MinCount', [true, 'Minumum number of instances to launch', 1]) + ] + ) + deregister_options('VHOST') + end + + def run + if datastore['AccessKeyId'].blank? || datastore['SecretAccessKey'].blank? + print_error("Both AccessKeyId and SecretAccessKey are required") + return + end + # setup creds for making IAM API calls + creds = { + 'AccessKeyId' => datastore['AccessKeyId'], + 'SecretAccessKey' => datastore['SecretAccessKey'] + } + creds['Token'] = datastore['Token'] unless datastore['Token'].blank? + + create_keypair(creds) unless datastore['SSH_PUB_KEY'].blank? + vpc = datastore['VPC_ID'].blank? ? vpc(creds) : datastore['VPC_ID'] + sg = datastore['SEC_GROUP_ID'].blank? ? create_sg(creds, vpc) : datastore['SEC_GROUP_ID'] + subnet = datastore['SUBNET_ID'].blank? ? pub_subnet(creds, vpc) : datastore['SUBNET_ID'] + unless subnet + print_error("Could not find a public subnet, please provide one") + return + end + instance_id = launch_instance(creds, subnet, sg) + action = 'DescribeInstances' + doc = call_ec2(creds, 'Action' => action, 'InstanceId.1' => instance_id) + doc = print_results(doc, action) + begin + # need a better parser so we can avoid shit like this + ip = doc['reservationSet']['item']['instancesSet']['item']['networkInterfaceSet']['item']['privateIpAddressesSet']['item']['association']['publicIp'] + print_status("Instance #{instance_id} has IP adrress #{ip}") + rescue NoMethodError + print_error("Could not retrieve instance IP address") + end + end + + def opts(action, subnet, sg) + opts = { + 'Action' => action, + 'ImageId' => datastore['AMI_ID'], + 'KeyName' => datastore['KEY_NAME'], + 'InstanceType' => datastore['INSTANCE_TYPE'], + 'NetworkInterface.1.SubnetId' => subnet, + 'NetworkInterface.1.SecurityGroupId.1' => sg, + 'MinCount' => datastore['MinCount'].to_s, + 'MaxCount' => datastore['MaxCount'].to_s, + 'NetworkInterface.1.AssociatePublicIpAddress' => 'true', + 'NetworkInterface.1.DeviceIndex' => '0' + } + opts['IamInstanceProfile.Name'] = datastore['ROLE_NAME'] unless datastore['ROLE_NAME'].blank? + unless datastore['USERDATA_FILE'].blank? + if File.exist?(datastore['USERDATA_FILE']) + opts['UserData'] = Base64.encode64(open(datastore['USERDATA_FILE'], 'r').read).strip + else + print_error("Could not open userdata file: #{datastore['USERDATA_FILE']}") + end + end + opts + end + + def launch_instance(creds, subnet, sg) + action = 'RunInstances' + print_status("Launching instance(s) in #{datastore['Region']}, AMI: #{datastore['AMI_ID']}, key pair name: #{datastore['KEY_NAME']}, security group: #{sg}, subnet ID: #{subnet}") + doc = call_ec2(creds, opts(action, subnet, sg)) + doc = print_results(doc, action) + return if doc.nil? + # TODO: account for multiple instances + if doc['instancesSet']['item'].instance_of?(Array) + instance_id = doc['instancesSet']['item'].first['instanceId'] + else + instance_id = doc['instancesSet']['item']['instanceId'] + end + print_status("Launched instance #{instance_id} in #{datastore['Region']} account #{doc['ownerId']}") + action = 'DescribeInstanceStatus' + loop do + sleep(15) + doc = call_ec2(creds, 'Action' => action, 'InstanceId' => instance_id) + doc = print_results(doc, action) + if doc ['instanceStatusSet'].nil? + print_error("Error, could not get instance status, instance possibly terminated") + break + end + status = doc['instanceStatusSet']['item']['systemStatus']['status'] + print_status("instance #{instance_id} status: #{status}") + break if status == 'ok' || status != 'initializing' + end + instance_id + end + + def create_keypair(creds) + action = 'ImportKeyPair' + doc = call_ec2(creds, 'Action' => action, 'KeyName' => datastore['KEY_NAME'], 'PublicKeyMaterial' => Rex::Text.encode_base64(datastore['SSH_PUB_KEY'])) + if doc['Response'].nil? + doc = print_results(doc, action) + if doc['keyName'].nil? || doc['keyFingerprint'].nil? + print_error("Error creating key using privided key material (SSH_PUB_KEY)") + else + print_status("Created #{doc['keyName']} (#{doc['keyFingerprint']})") + end + else + if doc['Response']['Errors'] && doc['Response']['Errors']['Error'] + print_error(doc['Response']['Errors']['Error']['Message']) + else + print_error("Error creating key using privided key material (SSH_PUB_KEY)") + end + end + end + + def pub_subnet(creds, vpc_id) + # First look for subnets that are configured to provision a public IP when instances are launched + action = 'DescribeSubnets' + doc = call_ec2(creds, 'Action' => action) + doc = print_results(doc, action) + vpc_subnets = doc['subnetSet']['item'].select { |x| x['vpcId'] == vpc_id } + pub_subnets = vpc_subnets.select { |x| x['mapPublicIpOnLaunch'] == 'true' } + return pub_subnets.first['subnetId'] if pub_subnets.count > 0 + + # Second, try to retrieve public subnet id by looking through route tables to find subnets + # associated with an Internet gateway + action = 'DescribeRouteTables' + doc = call_ec2(creds, 'Action' => action) + doc = print_results(doc, action) + vpc_route_table = doc['routeTableSet']['item'].select { |x| x['vpcId'] == vpc_id } + vpc_route_table.each do |route_table| + next if route_table['associationSet'].nil? || route_table['routeSet'].nil? + entries = route_table['routeSet']['item'] + if entries.instance_of?(Hash) + if entries['gatewayId'].start_with?('igw-') + return route_table['associationSet']['item'].first['subnetId'] + end + else + route_table['routeSet']['item'].each do |route| + if route['gatewayId'] && route['gatewayId'].start_with?('igw-') + return route_table['associationSet']['item'].first['subnetId'] + end + end + end + end + nil + end + + def create_sg(creds, vpc_id) + name = Rex::Text.rand_text_alphanumeric(8) + action = 'CreateSecurityGroup' + doc = call_ec2(creds, 'Action' => action, 'GroupName' => name, 'VpcId' => vpc_id, 'GroupDescription' => name) + doc = print_results(doc, action) + print_error("Could not create SG") && return if doc['groupId'].nil? + sg = doc['groupId'] + proto, port = datastore['SEC_GROUP_PORT'].split(':') + cidr = URI.encode(datastore['SEC_GROUP_CIDR']) + action = 'AuthorizeSecurityGroupIngress' + doc = call_ec2(creds, 'Action' => action, + 'IpPermissions.1.IpRanges.1.CidrIp' => cidr, + 'IpPermissions.1.IpProtocol' => proto, + 'IpPermissions.1.FromPort' => port, + 'IpPermissions.1.ToPort' => port, + 'GroupId' => sg) + doc = print_results(doc, action) + if doc['return'] && doc['return'] == 'true' + print_status("Created security group: #{sg}") + else + print_error("Failed creating security group") + end + sg + end + + def vpc(creds) + action = 'DescribeVpcs' + doc = call_ec2(creds, 'Action' => action) + doc = print_results(doc, action) + if doc['vpcSet'].nil? || doc['vpcSet']['item'].nil? + print_error("Could not determine VPC ID for #{datastore['AccessKeyId']} in #{datastore['RHOST']}") + return nil + end + item = doc['vpcSet']['item'] + return item['vpcId'] if item.instance_of?(Hash) + return item.first['vpcId'] if item.instance_of?(Array) && !item.first['vpcId'].nil? + print_error("Could not determine VPC ID for #{datastore['AccessKeyId']} in #{datastore['RHOST']}") + nil + end +end