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

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner
  include Msf::Exploit::Capture

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'URGENT/11 Scanner, Based on Detection Tool by Armis',
        'Description' => %q{
          This module detects VxWorks and the IPnet IP stack, along with devices
          vulnerable to CVE-2019-12258.
        },
        'Author' => [
          'Ben Seri',   # Upstream tool
          'Brent Cook', # Metasploit module
          'wvu'         # Metasploit module
        ],
        'References' => [
          ['CVE', '2019-12258'],
          ['URL', 'https://armis.com/urgent11'],
          ['URL', 'https://github.com/ArmisSecurity/urgent11-detector']
        ],
        'DisclosureDate' => '2019-08-09', # NVD entry publication
        'License' => MSF_LICENSE,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => UNKNOWN_RELIABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options([
      OptString.new('RPORTS', required: true, default: "21 22 23 80 443", desc: 'Target ports for TCP detections')
    ])

    register_advanced_options([
      OptInt.new('RetransmissionRate', required: true, default: 3, desc: 'Send n TCP packets')
    ])

    deregister_options('PCAPFILE', 'FILTER')
  end

  #
  # Utility methods
  #

  def rports
    datastore['RPORTS'].split(/[\s,]/).collect { |i| (i.to_i.to_s == i) ? i.to_i : nil }.compact
  end

  def filter(ip)
    "src host #{ip} and dst host #{Rex::Socket.source_address(ip)}"
  end

  #
  # Scanner methods
  #

  def run_host(ip)
    # XXX: Configuring Ethernet and IP headers sends a UDP packet!
    @config = PacketFu::Utils.whoami?(target: ip)

    open_pcap
    capture.setfilter(filter(ip))

    port_open = false
    rports.each do |rport|
      port_open |= run_detections(ip, rport)
    end
    raise RuntimeError.new("No ports open on #{ip} from #{datastore['RPORTS']}") if !port_open
  rescue RuntimeError => e
    fail_with(Failure::BadConfig, e.message)
  ensure
    close_pcap
  end

  def detections
    %w[
      tcp_dos_detection
      tcp_malformed_options_detection
      icmp_code_detection
      icmp_timestamp_detection
    ]
  end

  def run_detections(ip, port)
    print_status("#{ip}:#{port} being checked")

    final_ipnet_score = 0
    final_vxworks_score = 0
    affected_vulnerabilities = []

    begin
      sock = Rex::Socket::Tcp.create(
        'PeerHost' => ip,
        'PeerPort' => port
      )
    rescue
      vprint_bad("Could not connect to #{ip}:#{port}, cannot verify vulnerability")
      return false
    end

    detections.each do |detection|
      @ipnet_score = 0
      @vxworks_score = 0
      @vulnerable_cves = []

      detection_name = detection.camelize

      begin
        send(detection, sock, ip, port)
      rescue StandardError => e
        vprint_error("#{detection_name} failed: #{e.message}")
        next
      end

      vprint_status(
        "\t#{detection_name.ljust(30)}" \
        "\tVxWorks: #{@vxworks_score}" \
        "\tIPnet: #{@ipnet_score}"
      )

      final_ipnet_score += @ipnet_score
      final_vxworks_score += @vxworks_score
      affected_vulnerabilities += @vulnerable_cves
    end

    sock.close

    if final_ipnet_score > 0
      vprint_good("#{ip}:#{port} detected as IPnet")
    elsif final_ipnet_score < 0
      vprint_error("#{ip}:#{port} detected as NOT IPnet")
    end

    if final_vxworks_score > 100
      vprint_good("#{ip}:#{port} detected as VxWorks")
    elsif final_vxworks_score < 0
      vprint_error("#{ip}:#{port} detected as NOT VxWorks")
    end

    affected_vulnerabilities.each do |vuln|
      msg = "#{ip}:#{port} affected by #{vuln}"
      print_good(msg)
      report_vuln(
        host: ip,
        name: name,
        refs: references,
        info: msg
      )
    end
    true
  end

  #
  # TCP detection methods
  #

  def tcp_malformed_options_detection(sock, ip, port)
    pkt = PacketFu::TCPPacket.new(config: @config)

    # IP destination address
    pkt.ip_daddr = ip

    # TCP SYN with malformed options
    pkt.tcp_dst = port
    pkt.tcp_flags.syn = 1
    pkt.tcp_opts = [2, 4, 1460].pack('CCn') + # MSS
                   [1].pack('C') +            # NOP
                   [3, 2].pack('CC') +        # WSCALE with invalid length
                   [3, 3, 0].pack('CCC')      # WSCALE with valid length
    pkt.recalc

    res = nil

    datastore['RetransmissionRate'].times do
      pkt.to_w
      res = inject_reply(:tcp)

      break unless res
    end

    unless res
      return @vxworks_score = 0,
             @ipnet_score = 50
    end

    if res.tcp_flags.rst == 1 &&
       res.tcp_dst == pkt.tcp_src && res.tcp_dst == pkt.tcp_src

      return @vxworks_score = 100,
             @ipnet_score = 100
    end

    return @vxworks_score = -100,
           @ipnet_score = -100
  end

  def tcp_dos_detection(sock, ip, port)
    pkt = PacketFu::TCPPacket.new(config: @config)

    # IP destination address
    pkt.ip_daddr = ip

    # TCP SYN with malformed (truncated) WS option
    pkt.tcp_src = sock.getlocalname.last
    pkt.tcp_dst = sock.peerport
    pkt.tcp_seq = rand(0xffffffff + 1)
    pkt.tcp_ack = rand(0xffffffff + 1)
    pkt.tcp_flags.syn = 1
    pkt.tcp_opts = [3, 2].pack('CC') + # WSCALE with invalid length
                   [1, 0].pack('CC') # NOP + EOL
    pkt.recalc

    res = nil

    datastore['RetransmissionRate'].times do
      pkt.to_w
      res = inject_reply(:tcp)

      break unless res
    end

    unless res
      return @vxworks_score = 0,
             @ipnet_score = 0
    end

    if res.tcp_flags.rst == 1 &&
       res.tcp_dst == pkt.tcp_src && res.tcp_dst == pkt.tcp_src

      return @vxworks_score = 100,
             @ipnet_score = 100,
             @vulnerable_cves = ['CVE-2019-12258']
    end

    return @vxworks_score = 0,
           @ipnet_score = 0
  end

  #
  # ICMP detection methods
  #

  def icmp_code_detection(sock, ip, _port = nil)
    pkt = PacketFu::ICMPPacket.new(config: @config)

    # IP destination address
    pkt.ip_daddr = ip

    # ICMP echo request with non-zero code
    pkt.icmp_type = 8
    pkt.icmp_code = rand(0x01..0xff)
    pkt.payload = capture_icmp_echo_pack
    pkt.recalc

    pkt.to_w
    res = inject_reply(:icmp)

    unless res
      return @ipnet_score = 0
    end

    # Echo reply with zeroed code
    if res.icmp_type == 0 && res.icmp_code == 0
      return @ipnet_score = 20
    end

    @ipnet_score = -20
  end

  def icmp_timestamp_detection(sock, ip, _port = nil)
    pkt = PacketFu::ICMPPacket.new(config: @config)

    # IP destination address
    pkt.ip_daddr = ip

    # Truncated ICMP timestamp request
    pkt.icmp_type = 13
    pkt.icmp_code = 0
    pkt.payload = "\x00" * 4
    pkt.recalc

    pkt.to_w
    res = inject_reply(:icmp)

    unless res
      return @ipnet_score = 0
    end

    # Timestamp reply
    if res.icmp_type == 14
      return @ipnet_score = 90
    end

    @ipnet_score = -30
  end

end
