summaryrefslogtreecommitdiffstats
path: root/nselib/netbios.lua
blob: 47b4d4d98a576e96dba0b70640cd5e1563a88e27 (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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
---
-- Creates and parses NetBIOS traffic. The primary use for this is to send
-- NetBIOS name requests.
--
-- @author Ron Bowes <ron@skullsecurity.net>
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html

local dns = require "dns"
local math = require "math"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
_ENV = stdnse.module("netbios", stdnse.seeall)


types = {
  NB = 32,
  NBSTAT = 33,
}

--- Encode a NetBIOS name for transport.
--
-- Most packets that use the NetBIOS name require this encoding to happen
-- first. It takes a name containing any possible character, and converted it
-- to all uppercase characters (so it can, for example, pass case-sensitive
-- data in a case-insensitive way)
--
-- There are two levels of encoding performed:
-- * L1: Pad the string to 16 characters withs spaces (or NULLs if it's the
--     wildcard "*") and replace each byte with two bytes representing each
--     of its nibbles, plus 0x41.
-- * L2: Prepend the length to the string, and to each substring in the scope
--     (separated by periods).
--@param name The name that will be encoded (eg. "TEST1").
--@param scope [optional] The scope to encode it with. I've never seen scopes used
--       in the real world (eg, "insecure.org").
--@return The L2-encoded name and scope
--        (eg. "\x20FEEFFDFEDBCACACACACACACACACAAA\x08insecure\x03org")
function name_encode(name, scope)

  stdnse.debug3("Encoding name '%s'", name)
  -- Truncate or pad the string to 16 bytes
  if(#name >= 16) then
    name = string.sub(name, 1, 16)
  else
    local padding = " "
    if name == "*" then
      padding = "\0"
    end

    name = name .. string.rep(padding, 16 - #name)
  end

  -- Convert to uppercase
  name = string.upper(name)

  -- Do the L1 encoding
  local L1_encoded = {}
  for i=1, #name, 1 do
    local b = string.byte(name, i)
    L1_encoded[i*2-1] = string.char(((b & 0xF0) >> 4) + 0x41)
    L1_encoded[i*2]   = string.char((b & 0x0F) + 0x41)
  end

  -- Do the L2 encoding
  local L2_encoded = { string.char(32), table.concat(L1_encoded) }

  if scope ~= nil then
    -- Split the scope at its periods
    local piece
    for piece in string.gmatch(scope, "[^.]+") do
      L2_encoded[#L2_encoded+1] = string.char(#piece) .. piece
    end
  end

  L2_encoded = table.concat(L2_encoded)
  stdnse.debug3("=> '%s'", L2_encoded)
  return L2_encoded
end



--- Converts an encoded name to the string representation.
--
-- If the encoding is invalid, it will still attempt to decode the string as
-- best as possible.
--@param encoded_name The L2-encoded name
--@return the decoded name and the scope. The name will still be padded, and the
--         scope will never be nil (empty string is returned if no scope is present)
function name_decode(encoded_name)
  local name = ""
  local scope = ""

  local len = string.byte(encoded_name, 1)
  local i

  stdnse.debug3("Decoding name '%s'", encoded_name)

  name = name:gsub("(.)(.)", function (a, b)
      local ch = ((string.byte(a) - 0x41) << 4) | (string.byte(b) - 0x41)
      return string.char(ch)
    end)

  -- Decode the scope
  local pos = 34
  while #encoded_name > pos do
    local len = string.byte(encoded_name, pos)
    scope = scope .. string.sub(encoded_name, pos + 1, pos + len) .. "."
    pos = pos + 1 + len
  end

  -- If there was a scope, remove the trailing period
  if(#scope > 0) then
    scope = string.sub(scope, 1, #scope - 1)
  end

  stdnse.debug3("=> '%s'", name)

  return name, scope
end

--- Sends out a UDP probe on port 137 to get a human-readable list of names the
--  the system is using.
--@param host The IP or hostname to check.
--@param prefix [optional] The prefix to put on each line when it's returned.
--@return (status, result) If status is true, the result is a human-readable
--        list of names. Otherwise, result is an error message.
function get_names(host, prefix)

  local status, names, statistics = do_nbstat(host)

  if(prefix == nil) then
    prefix = ""
  end


  if(status) then
    local result = ""
    for i = 1, #names, 1 do
      result = result .. string.format("%s%s<%02x>\n", prefix, names[i]['name'], names[i]['prefix'])
    end

    return true, result
  else
    return false, names
  end
end

--- Sends out a UDP probe on port 137 to get the server's name (that is, the
--  entry in its NBSTAT table with a 0x20 suffix).
--@param host The IP or hostname of the server.
--@param names [optional] The names to use, from <code>do_nbstat</code>.
--@return (status, result) If status is true, the result is the NetBIOS name.
--        otherwise, result is an error message.
function get_server_name(host, names)

  local status
  local i

  if names == nil then
    status, names = do_nbstat(host)

    if(status == false) then
      return false, names
    end
  end

  for i = 1, #names, 1 do
    if names[i]['suffix'] == 0x20 then
      return true, names[i]['name']
    end
  end

  return true, nil
end

--- Sends out a UDP probe on port 137 to get the workstation's name (that is, the
--  unique entry in its NBSTAT table with a 0x00 suffix).
--@param host The IP or hostname of the server.
--@param names [optional] The names to use, from <code>do_nbstat</code>.
--@return (status, result) If status is true, the result is the NetBIOS name.
--        otherwise, result is an error message.
function get_workstation_name(host, names)

  local status
  local i

  if names == nil then
    status, names = do_nbstat(host)

    if(status == false) then
      return false, names
    end
  end

  for i = 1, #names, 1 do
    if names[i]['suffix'] == 0x00 and (names[i]['flags'] & 0x8000) == 0 then
      return true, names[i]['name']
    end
  end

  return true, nil
end
--- Sends out a UDP probe on port 137 to get the user's name
--
-- User name is the entry in its NBSTAT table with a 0x03 suffix, that isn't
-- the same as the server's name. If the username can't be determined, which is
-- frequently the case, nil is returned.
--@param host The IP or hostname of the server.
--@param names [optional] The names to use, from <code>do_nbstat</code>.
--@return (status, result) If status is true, the result is the NetBIOS name or nil.
--        otherwise, result is an error message.
function get_user_name(host, names)

  local status, server_name = get_server_name(host, names)

  if(status == false) then
    return false, server_name
  end

  if(names == nil) then
    status, names = do_nbstat(host)

    if(status == false) then
      return false, names
    end
  end

  for i = 1, #names, 1 do
    if names[i]['suffix'] == 0x03 and names[i]['name'] ~= server_name then
      return true, names[i]['name']
    end
  end

  return true, nil

end


--- This is the function that actually handles the UDP query to retrieve
-- the NBSTAT information.
--
-- We make use of the Nmap registry here, so if another script has already
-- performed a nbstat query, the result can be re-used.
--
-- The NetBIOS request's header looks like this:
--<code>
--  --------------------------------------------------
--  |  15 14 13 12 11 10 9  8  7  6  5  4  3  2  1  0 |
--  |                  NAME_TRN_ID                    |
--  | R |   OPCODE  |      NM_FLAGS      |   RCODE    | (FLAGS)
--  |                    QDCOUNT                      |
--  |                    ANCOUNT                      |
--  |                    NSCOUNT                      |
--  |                    ARCOUNT                      |
--  --------------------------------------------------
--</code>
--
-- In this case, the TRN_ID is a constant (0x1337, what else?), the flags
-- are 0, and we have one question. All fields are network byte order.
--
-- The body of the packet is a list of names to check for in the following
-- format:
-- * (ntstring) encoded name
-- * (2 bytes)  query type (0x0021 = NBSTAT)
-- * (2 bytes)  query class (0x0001 = IN)
--
-- The response header is the exact same, except it'll have some flags set
-- (0x8000 for sure, since it's a response), and ANCOUNT will be 1. The format
-- of the answer is:
--
-- * (ntstring) requested name
-- * (2 bytes)  query type
-- * (2 bytes)  query class
-- * (2 bytes)  time to live
-- * (2 bytes)  record length
-- * (1 byte)   number of names
-- * [for each name]
-- *  (16 bytes) padded name, with a 1-byte suffix
-- *  (2 bytes)  flags
-- * (variable) statistics (usually mac address)
--
--@param host The IP or hostname of the system.
--@return (status, names, statistics) If status is true, then the servers names are
--        returned as a table containing 'name', 'suffix', and 'flags'.
--        Otherwise, names is an error message and statistics is undefined.
function do_nbstat(host)

  local status, err
  local socket = nmap.new_socket()
  local encoded_name = name_encode("*")
  local statistics
  local reg
  if type(host) == "string" then --ip
    stdnse.debug3("Performing nbstat on host '%s'", host)
    nmap.registry.netbios = nmap.registry.netbios or {}
    nmap.registry.netbios[host] = nmap.registry.netbios[host] or {}
    reg = nmap.registry.netbios[host]
  else
    stdnse.debug3("Performing nbstat on host '%s'", host.ip)
    if host.registry.netbios == nil and
      nmap.registry.netbios ~= nil and
      nmap.registry.netbios[host.ip] ~= nil then
      host.registry.netbios = nmap.registry.netbios[host.ip]
    end
    host.registry.netbios = host.registry.netbios or {}
    reg = host.registry.netbios
  end

  -- Check if it's cached in the registry for this host
  if(reg["nbstat_names"] ~= nil) then
    stdnse.debug3(" |_ [using cached value]")
    return true, reg["nbstat_names"], reg["nbstat_statistics"]
  end

  -- Create the query header
  local query = string.pack(">I2I2I2I2I2I2",
  0x1337,  -- Transaction id
  0x0000,  -- Flags
  1,       -- Questions
  0,       -- Answers
  0,       -- Authority
  0        -- Extra
  ) .. string.pack(">zI2I2",
  encoded_name, -- Encoded name
  0x0021,       -- Query type (0x21 = NBSTAT)
  0x0001        -- Class = IN
  )
  status, err = socket:connect(host, 137, "udp")
  if(status == false) then
    return false, err
  end

  status, err = socket:send(query)
  if(status == false) then
    return false, err
  end

  socket:set_timeout(1000)

  local status, result = socket:receive_bytes(1)
  if(status == false) then
    return false, result
  end

  local close_status, err = socket:close()
  if(close_status == false) then
    return false, err
  end

  if(status) then
    local pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT, rr_name, rr_type, rr_class, rr_ttl
    local rrlength, name_count

    TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT, pos = string.unpack(">I2I2I2I2I2I2", result)

    -- Sanity check the result (has to have the same TRN_ID, 1 answer, and proper flags)
    if(TRN_ID ~= 0x1337) then
      return false, string.format("Invalid transaction ID returned: 0x%04x", TRN_ID)
    end
    if(ANCOUNT ~= 1) then
      return false, "Server returned an invalid number of answers"
    end
    if FLAGS & 0x8000 == 0 then
      return false, "Server's flags didn't indicate a response"
    end
    if FLAGS & 0x0007 ~= 0 then
      return false, string.format("Server returned a NetBIOS error: 0x%02x", FLAGS & 0x0007)
    end

    -- Start parsing the answer field
    rr_name, rr_type, rr_class, rr_ttl, pos = string.unpack(">zI2I2I4", result, pos)

    -- More sanity checks
    if(rr_name ~= encoded_name) then
      return false, "Server returned incorrect name"
    end
    if(rr_class ~= 0x0001) then
      return false, "Server returned incorrect class"
    end
    if(rr_type ~= 0x0021) then
      return false, "Server returned incorrect query type"
    end

    rrlength, name_count, pos = string.unpack(">I2B", result, pos)

    local names = {}
    for i = 1, name_count do
      local name, suffix, flags

      -- Instead of reading the 16-byte name and pulling off the suffix,
      -- we read the first 15 bytes and then the 1-byte suffix.
      name, suffix, flags, pos = string.unpack(">c15BI2", result, pos)
      name = string.gsub(name, "[ ]*$", "")

      names[i] = {}
      names[i]['name']   = name
      names[i]['suffix'] = suffix
      names[i]['flags']  = flags

      -- Decrement the length
      rrlength = rrlength - 18
    end

    if(rrlength > 0) then
      rrlength = rrlength - 1
    end
    statistics, pos = string.unpack(string.format(">c%d", rrlength), result, pos)

    -- Put it in the registry, in case anybody else needs it
    reg["nbstat_names"] = names
    reg["nbstat_statistics"] = statistics

    return true, names, statistics

  else
    return false, "Name query failed: " .. result
  end
end

function nbquery(host, nbname, options)
  -- override any options or set the default values
  local options = options or {}
  options.port = options.port or 137
  options.retPkt = options.retPkt or true
  options.dtype = options.dtype or types.NB
  options.host = host.ip
  options.flags = options.flags or ( options.multiple and 0x0110 )
  options.id = math.random(0xFFFF)

  -- encode and chop off the leading byte, as the dns library takes care of
  -- specifying the length
  local encoded_name = name_encode(nbname):sub(2)

  local status, response = dns.query( encoded_name, options )
  if ( not(status) ) then return false, "ERROR: nbquery failed" end

  local results = {}
  -- discard any additional responses
  if ( options.multiple and #response > 0 ) then
    for _, resp in ipairs(response) do
      assert( options.id == resp.output.id, "Received packet with invalid transaction ID" )
      if ( not(resp.output.answers) or #resp.output.answers < 1 ) then
        return false, "ERROR: Response contained no answers"
      end
      local dname = string.char(#resp.output.answers[1].dname) .. resp.output.answers[1].dname
      table.insert( results, { peer = resp.peer, name = name_decode(dname), data = resp.output.answers[1].data } )
    end
    return true, results
  else
    local dname = string.char(#response.answers[1].dname) .. response.answers[1].dname
    return true, { { peer = host.ip, name = name_decode(dname), data = response.answers[1].data } }
  end
end

---Convert the 16-bit flags field to a string.
--@param flags The 16-bit flags field
--@return A string representing the flags
function flags_to_string(flags)
  local result = {}

  if flags & 0x8000 ~= 0 then
    result[#result+1] = "<group>"
  else
    result[#result+1] = "<unique>"
  end

  if flags & 0x1000 ~= 0 then
    result[#result+1] = "<deregister>"
  end

  if flags & 0x0800 ~= 0 then
    result[#result+1] = "<conflict>"
  end

  if flags & 0x0400 ~= 0 then
    result[#result+1] = "<active>"
  end

  if flags & 0x0200 ~= 0 then
    result[#result+1] = "<permanent>"
  end

  return table.concat(result)
end


return _ENV;