local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Checks if a target on a local Ethernet has its network card in promiscuous mode.

The techniques used are described at
http://www.securityfriday.com/promiscuous_detection_01.pdf.
]]

---
-- @output
-- Host script results:
-- |_ sniffer-detect: Likely in promiscuous mode (tests: "11111111")


author = "Marek Majkowski"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

categories = {"discovery", "intrusive"}

-- okay, we're interested only in hosts that are on our ethernet lan
hostrule = function(host)
  if nmap.address_family() ~= 'inet' then
    stdnse.debug1("is IPv4 compatible only.")
    return false
  end
  if host.directly_connected and
      host.mac_addr ~= nil and
      host.mac_addr_src ~= nil and
      host.interface ~= nil then
    local iface = nmap.get_interface_info(host.interface)
    if iface and iface.link == 'ethernet' then
      return true
    end
  end
  return false
end

local function check (layer2)
  return string.sub(layer2, 0, 12)
end


do_test = function(dnet, pcap, host, test)
  local status, length, layer2, layer3
  local i = 0

  -- ARP requests are send with timeouts: 10ms, 40ms, 90ms
  -- before each try, we wait at least 100ms
  -- in summary, this test takes at least 100ms and at most 440ms
  for i=1,3 do
    -- flush buffers :), wait quite long.
    repeat
      pcap:set_timeout(100)
      local test = host.mac_addr_src .. host.mac_addr
      status, length, layer2, layer3 = pcap:pcap_receive()
      while status and test ~= check(layer2) do
        status, length, layer2, layer3 = pcap:pcap_receive()
      end
    until status ~= true
    pcap:set_timeout(10 * i*i)

    dnet:ethernet_send(test)

    local test = host.mac_addr_src .. host.mac_addr
    status, length, layer2, layer3 = pcap:pcap_receive()
    while status and test ~= check(layer2) do
      status, length, layer2, layer3 = pcap:pcap_receive()
    end
    if status then
      -- the basic idea, was to inform user about time, when we got packet
      -- so that 1 would mean (0-10ms), 2=(10-40ms) and 3=(40ms-90ms)
      -- but when we're running this tests on macs, first test is always 2.
      -- which means that the first answer is dropped.
      -- for now, just return 1 if test was successful, it's easier
      -- return(i)
      return(1)
    end
  end
  return('_')
end

action = function(host)
  local dnet = nmap.new_dnet()
  local pcap = nmap.new_socket()
  local _
  local status
  local results = {
    ['1_____1_'] = false, -- MacOSX(Tiger.Panther)/Linux/ ?Win98/ WinXP sp2(no pcap)
    ['1_______'] = false, -- Old Apple/SunOS/3Com
    ['1___1_1_'] = false, -- MacOSX(Tiger)
    ['11111111'] = true,  -- BSD/Linux/OSX/     (or not promiscuous openwrt )
    ['1_1___1_'] = false, -- WinXP sp2 + pcap|| win98 sniff || win2k sniff (see below)
    ['111___1_'] = true,  -- WinXP sp2 promisc
    --['1111__1_'] = true,  -- ?Win98 promisc + ??win98 no promisc *not confirmed*
  }
  dnet:ethernet_open(host.interface)

  pcap:pcap_open(host.interface, 64, false, "arp")

  local test_static = host.mac_addr_src ..
    "\x08\x06\x00\x01\x08\x00\x06\x04\x00\x01" ..
    host.mac_addr_src ..
    host.bin_ip_src ..
    "\x00\x00\x00\x00\x00\x00" ..
    host.bin_ip
  local t = {
    "\xff\xff\xff\xff\xff\xff", -- B32 no meaning?
    "\xff\xff\xff\xff\xff\xfe", -- B31
    "\xff\xff\x00\x00\x00\x00", -- B16
    "\xff\x00\x00\x00\x00\x00", -- B8
    "\x01\x00\x00\x00\x00\x00", -- G
    "\x01\x00\x5e\x00\x00\x00", -- M0
    "\x01\x00\x5e\x00\x00\x01", -- M1 no meaning?
    "\x01\x00\x5e\x00\x00\x03", -- M3
  }
  local v
  local out = {}
  for _, v in ipairs(t) do
    out[#out+1] = do_test(dnet, pcap, host, v .. test_static)
  end
  out = table.concat(out)

  dnet:ethernet_close()
  pcap:pcap_close()

  if out == '1_1___1_' then
    return 'Windows with libpcap installed; may or may not be sniffing (tests: "' .. out .. '")'
  end
  if results[out] == false then
    -- probably not sniffing
    return
  end
  if results[out] == true then
    -- rather sniffer.
    return 'Likely in promiscuous mode (tests: "' .. out .. '")'
  end

  -- results[out] == nil
  return 'Unknown (tests: "' .. out .. '")'
end
