CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/cloud/aws/enum_ssm.rb
Views: 11655
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'aws-sdk-ssm'
7
require 'aws-sdk-ec2'
8
9
class MetasploitModule < Msf::Auxiliary
10
include Rex::Proto::Http::WebSocket::AmazonSsm
11
include Msf::Auxiliary::Report
12
include Msf::Auxiliary::CommandShell
13
include Msf::Sessions::CreateSessionOptions
14
include Msf::Auxiliary::ReportSummary
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Amazon Web Services EC2 SSM enumeration',
21
'Description' => %q{
22
Provided AWS credentials, this module will call the authenticated
23
API of Amazon Web Services to list all SSM-enabled EC2 instances
24
accessible to the account. Once enumerated as SSM-enabled, the
25
instances can be controlled using out-of-band WebSocket sessions
26
provided by the AWS API (nominally, privileged out of the box).
27
This module provides not only the API enumeration identifying EC2
28
instances accessible via SSM with given credentials, but enables
29
session initiation for all identified targets (without requiring
30
target-level credentials) using the CreateSession datastore option.
31
The module also provides an EC2 ID filter and a limiting throttle
32
to prevent session stampedes or expensive messes.
33
},
34
'Author' => [
35
'RageLtMan <rageltman[at]sempervictus>'
36
],
37
'References' => [['URL', 'https://www.sempervictus.com/single-post/once-upon-a-cloudy-air-i-crossed-a-gap-which-wasn-t-there']],
38
'License' => MSF_LICENSE,
39
'DefaultOptions' => { 'CreateSession' => false },
40
'Notes' => {
41
'SideEffects' => [IOC_IN_LOGS],
42
'Reliability' => [],
43
'Stability' => [CRASH_SAFE]
44
}
45
)
46
)
47
48
register_options(
49
[
50
OptInt.new('LIMIT', [false, 'Only return the specified number of results from each region']),
51
OptString.new('FILTER_EC2_ID', [false, 'Look for specific EC2 instance ID']),
52
OptString.new('REGION', [true, 'AWS Region (e.g. "us-west-2")']),
53
OptString.new('ACCESS_KEY_ID', [true, 'AWS Access Key ID (eg. "AKIAXXXXXXXXXXXXXXXX")', '']),
54
OptString.new('SECRET_ACCESS_KEY', [true, 'AWS Secret Access Key (eg. "CA1+XXXXXXXXXXXXXXXXXXXXXX6aYDHHCBuLuV79")', ''])
55
]
56
)
57
end
58
59
def handle_aws_errors(error)
60
if error.class.module_parents.include?(Aws)
61
fail_with(Failure::UnexpectedReply, error.message)
62
else
63
raise error
64
end
65
end
66
67
def run
68
credentials = ::Aws::Credentials.new(datastore['ACCESS_KEY_ID'], datastore['SECRET_ACCESS_KEY'])
69
vprint_status "Checking #{datastore['REGION']}..."
70
client = ::Aws::SSM::Client.new(
71
region: datastore['REGION'],
72
credentials: credentials
73
)
74
inv_params = {
75
filters: [
76
{
77
key: 'AWS:InstanceInformation.InstanceStatus',
78
values: ['Terminated'],
79
type: 'NotEqual'
80
},
81
{
82
key: 'AWS:InstanceInformation.ResourceType',
83
values: ['EC2Instance'],
84
type: 'Equal'
85
}
86
]
87
}
88
89
if datastore['FILTER_EC2_ID']
90
inv_params[:filters] << {
91
key: 'AWS:InstanceInformation.InstanceId',
92
values: [datastore['FILTER_EC2_ID']],
93
type: 'Equal'
94
}
95
end
96
97
inv_params[:max_results] = datastore['LIMIT'] if datastore['LIMIT']
98
99
ssm_ec2 = client.get_inventory(inv_params).entities.map { |e| e.data['AWS:InstanceInformation'].content }.flatten
100
ssm_ec2.each do |ssm_host|
101
report_host(
102
host: ssm_host['IpAddress'],
103
os_flavor: ssm_host['PlatformName'],
104
os_name: ssm_host['PlatformType'],
105
os_sp: ssm_host['PlatformVersion'],
106
name: ssm_host['ComputerName'],
107
comments: "ec2-id: #{ssm_host['InstanceId']}"
108
)
109
report_note(
110
host: ssm_host['IpAddress'],
111
type: ssm_host['AgentType'],
112
data: ssm_host['AgentVersion']
113
)
114
vprint_good("Found AWS SSM host #{ssm_host['InstanceId']} (#{ssm_host['ComputerName']}) - #{ssm_host['IpAddress']}")
115
next unless datastore['CreateSession']
116
117
socket = get_ssm_socket(client, ssm_host['InstanceId'])
118
sess = Msf::Sessions::AwsSsmCommandShellBind.new(socket.lsock, { datastore: datastore, aws_ssm_host_info: ssm_host })
119
120
start_session(self, sess.info, datastore, false, socket.lsock, sess)
121
end
122
rescue Seahorse::Client::NetworkingError => e
123
print_error e.message
124
print_error "Confirm access to #{datastore['REGION']} with provided credentials"
125
rescue StandardError => e
126
handle_aws_errors(e)
127
end
128
129
def get_ssm_socket(client, ec2_id)
130
# Verify the connection params and availability of instance
131
inv_params = {
132
filters: [
133
{
134
key: 'AWS:InstanceInformation.InstanceId',
135
values: [ec2_id],
136
type: 'Equal'
137
}
138
]
139
}
140
inventory = client.get_inventory(inv_params)
141
# Extract peer info
142
if inventory.entities[0] && (inventory.entities[0].id == ec2_id)
143
peer_info = inventory.entities[0].data['AWS:InstanceInformation'].content[0]
144
else
145
raise 'SSM target not found'
146
end
147
session_init = client.start_session({
148
target: ec2_id,
149
document_name: 'SSM-SessionManagerRunShell'
150
# AWS-RunShellScript, AWS-RunPowerShellScript, etc
151
})
152
ssm_sock = connect_ssm_ws(session_init)
153
chan = ssm_sock.to_ssm_channel
154
chan.params.comm = Rex::Socket::Comm::Local unless chan.params.comm
155
chan.params.peerhost = peer_info['IpAddress']
156
chan.params.peerport = 0
157
chan.params.peerhostname = peer_info['ComputerName']
158
chan._start_ssm_keepalive
159
chan.update_term_size
160
return chan
161
end
162
end
163
164