summaryrefslogtreecommitdiffstats
path: root/scripts/llmnr-resolve.nse
blob: 25d2acd17d336c6ba637f614e35d88f3b39f8892 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
local nmap = require "nmap"
local stdnse = require "stdnse"
local table = require "table"
local packet = require "packet"
local ipOps = require "ipOps"
local target = require "target"
local math = require "math"
local string = require "string"

description = [[
Resolves a hostname by using the LLMNR (Link-Local Multicast Name Resolution) protocol.

The script works by sending a LLMNR Standard Query containing the hostname to
the 5355 UDP port on the 224.0.0.252 multicast address. It listens for any
LLMNR responses that are sent to the local machine with a 5355 UDP source port.
A hostname to resolve must be provided.

For more information, see:
* http://technet.microsoft.com/en-us/library/bb878128.aspx
]]

---
--@args llmnr-resolve.hostname Hostname to resolve.
--
--@args llmnr-resolve.timeout Max time to wait for a response. (default 3s)
--
--@usage
-- nmap --script llmnr-resolve --script-args 'llmnr-resolve.hostname=examplename' -e wlan0
--
--@output
-- Pre-scan script results:
-- | llmnr-resolve:
-- |   acer-PC : 192.168.1.4
-- |_  Use the newtargets script-arg to add the results as targets
--

prerule = function()
  if not nmap.is_privileged() then
    stdnse.verbose1("not running due to lack of privileges.")
    return false
  end
  return true
end

author = "Hani Benhabiles"

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

categories = {"discovery", "safe", "broadcast"}


--- Returns a raw llmnr query
-- @param hostname Hostname to query for.
-- @return query Raw llmnr query.
local llmnrQuery = function(hostname)
  return string.pack(">I2I2I2I2I2I2 s1x I2I2",
    math.random(0,65535), -- transaction ID
    0x0000, -- Flags: Standard Query
    0x0001, -- Questions = 1
    0x0000, -- Answer RRs = 0
    0x0000, -- Authority RRs = 0
    0x0000, -- Additional RRs = 0
    hostname, -- Hostname
    0x0001, -- Type: Host Address
    0x0001) -- Class: IN
end

--- Sends a llmnr query.
-- @param query Query to send.
local llmnrSend = function(query, mcast, mport)
  -- Multicast IP and UDP port
  local sock = nmap.new_socket()
  local status, err = sock:connect(mcast, mport, "udp")
  if not status then
    stdnse.debug1("%s", err)
    return
  end
  sock:send(query)
  sock:close()
end

-- Listens for llmnr responses
-- @param interface Network interface to listen on.
-- @param timeout Maximum time to listen.
-- @param result table to put responses into.
local llmnrListen = function(interface, timeout, result)
  local condvar = nmap.condvar(result)
  local start = nmap.clock_ms()
  local listener = nmap.new_socket()
  local status, l3data, _

  -- packets that are sent to our UDP port number 5355
  local filter = 'dst host ' .. interface.address .. ' and udp src port 5355'
  listener:set_timeout(100)
  listener:pcap_open(interface.device, 1024, true, filter)

  while (nmap.clock_ms() - start) < timeout do
    status, _, _, l3data = listener:pcap_receive()
    if status then
      local p = packet.Packet:new(l3data, #l3data)
      -- Skip IP and UDP headers
      local llmnr = string.sub(l3data, p.ip_hl*4 + 8 + 1)
      -- Flags
      local trans, flags, questions = string.unpack(">I2 I2 I2", llmnr)

      -- Make verifications
      -- Message == Response bit
      -- and 1 Question (hostname we requested) and
      if ((flags >> 15) == 1) and questions == 0x01 then
        stdnse.debug1("got response from %s", p.ip_src)
        -- Skip header's 12 bytes
        -- extract host length
        local qlen, index = string.unpack(">B", llmnr, 13)
        -- Skip hostname, null byte, type field and class field
        index = index + qlen + 1 + 2 + 2

        -- Now, answer record
        local response, alen = {}
        -- Extract hostname with the correct case sensitivity.
        response.hostname, index = string.unpack(">s1x", llmnr, index)

        -- skip type, class, ttl, dlen
        index = index + 2 + 2 + 4 + 2
        response.address, index = string.unpack(">c4", llmnr, index)
        response.address = ipOps.str_to_ip(response.address)
        table.insert(result, response)
      else
        stdnse.debug1("skipped llmnr response.")
      end
    end
  end
  condvar("signal")
end

-- Returns the network interface used to send packets to a target host.
--@param target host to which the interface is used.
--@return interface Network interface used for target host.
local getInterface = function(target)
  -- First, create dummy UDP connection to get interface
  local sock = nmap.new_socket()
  local status, err = sock:connect(target, "12345", "udp")
  if not status then
    stdnse.verbose1("%s", err)
    return
  end
  local status, address, _, _, _ = sock:get_info()
  if not status then
    stdnse.verbose1("%s", err)
    return
  end
  for _, interface in pairs(nmap.list_interfaces()) do
    if interface.address == address then
      return interface
    end
  end
end


action = function()
  local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
  timeout = (timeout or 3) * 1000
  local hostname = stdnse.get_script_args(SCRIPT_NAME .. ".hostname")
  local result, output = {}, {}
  local mcast = "224.0.0.252"
  local mport = 5355

  -- Check if a valid hostname was provided
  if not hostname or #hostname == 0 then
    stdnse.debug1("no hostname was provided.")
    return
  end

  -- Check if a valid interface was provided
  local interface = nmap.get_interface()
  if interface then
    interface = nmap.get_interface_info(interface)
  else
    interface = getInterface(mcast)
  end
  if not interface then
    return stdnse.format_output(false, ("Couldn't get interface for %s"):format(mcast))
  end

  -- Launch listener thread
  stdnse.new_thread(llmnrListen, interface, timeout, result)
  -- Craft raw query
  local query = llmnrQuery(hostname)
  -- Small sleep so the listener doesn't miss the response
  stdnse.sleep(0.5)
  -- Send query
  llmnrSend(query, mcast, mport)
  -- Wait for listener thread to finish
  local condvar = nmap.condvar(result)
  condvar("wait")

  -- Check responses
  if #result > 0 then
    for _, response in pairs(result) do
      table.insert(output, response.hostname.. " : " .. response.address)
      if target.ALLOW_NEW_TARGETS then
        target.add(response.address)
      end
    end
    if ( not(target.ALLOW_NEW_TARGETS) ) then
      table.insert(output,"Use the newtargets script-arg to add the results as targets")
    end
    return stdnse.format_output(true, output)
  end
end