summaryrefslogtreecommitdiffstats
path: root/nselib/ike.lua
blob: d3278cb1edb7e1797528cddf9267ab3229e7b3e9 (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
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
---
--A very basic IKE library.
--
--The current functionality includes:
--
--  1. Generating a Main or Aggressive Mode IKE request packet with a variable amount of transforms and a vpn group.
--  2. Sending a packet
--  3. Receiving the response
--  4. Parsing the response for VIDs
--  5. Searching for the VIDs in 'ike-fingerprints.lua'
--  6. returning a parsed info table
--
--This library is meant for extension, which could include:
--
--  1. complete parsing of the response packet (might allow for better fingerprinting)
--  2. adding more options to the request packet
--     vendor field (might give better fingerprinting of services, e.g. Checkpoint)
--  3. backoff pattern analyses
--
--An a implementation resembling 'ike-scan' could be built.
--
--@author Jesper Kueckelhahn
--@license Same as Nmap--See https://nmap.org/book/man-legal.html

local _G = require "_G"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local rand = require "rand"
_ENV = stdnse.module("ike", stdnse.seeall)

local ENC_METHODS = {
  ["des"]     = 0x80010001,
  ["3des"]    = 0x80010005,
  ["cast"]    = 0x80010006,
  ["aes/128"] = { 0x80010007, 0x800E0080 },
  ["aes/192"] = { 0x80010007, 0x800E00C0 },
  ["aes/256"] = { 0x80010007, 0x800E0100 },
}

local AUTH_TYPES = {
  ["psk"]    = 0x80030001,
  ["rsa"]    = 0x80030003,
  ["ECDSA"]  = 0x80030008,
  ["Hybrid"] = 0x8003FADD,
  ["XAUTH"]  = 0x8003FDE9,
}

local HASH_ALGORITHM = {
  ["md5"]      = 0x80020001,
  ["sha1"]     = 0x80020002,
  ["sha2-256"] = 0x80020004,
  ["sha2-384"] = 0x80020005,
  ["sha2-512"] = 0x80020006,
}

local GROUP_DESCRIPTION = {
  ["768"]  = 0x80040001,
  ["1024"] = 0x80040002,
  ["1536"] = 0x80040005,
  ["2048"] = 0x8004000E,
}

local EXCHANGE_MODE = {
  ["Main"]       = 0x02,
  ["Aggressive"] = 0x04,
}

local PROTOCOL_IDS = {
  ["tcp"] = 0x06,
  ["udp"] = 0x11,
}

-- Response packet types
local EXCHANGE_TYPE = {
  [0x02] = "Main",
  [0x04] = "Aggressive",
  [0x05] = "Informational",
}

-- Payload names
local PAYLOADS = {
  [0x00] = "None",
  [0x01] = "SA",
  [0x03] = "Transform",
  [0x04] = "Key Exchange",
  [0x05] = "ID",
  [0x08] = "Hash",
  [0x0A] = "Nonce",
  [0x0D] = "VID",
}


-- Load the fingerprint file
-- (located in: nselib/data/ike-fingerprints.lua)
--
local function load_fingerprints()
  local file, filename_full, fingerprints

  -- Check if fingerprints are cached
  if(nmap.registry.ike_fingerprints ~= nil) then
    stdnse.debug1("ike: Loading cached fingerprints")
    return nmap.registry.ike_fingerprints
  end

  -- Try and find the file
  -- If it isn't in Nmap's directories, take it as a direct path
  filename_full = nmap.fetchfile('nselib/data/ike-fingerprints.lua')

  -- Load the file
  stdnse.debug1("ike: Loading fingerprints: %s", filename_full)
  local env = setmetatable({fingerprints = {}}, {__index = _G});
  file = loadfile(filename_full, "t", env)
  if( not(file) ) then
    stdnse.debug1("ike: Couldn't load the file: %s", filename_full)
    return false, "Couldn't load fingerprint file: " .. filename_full
  end
  file()
  fingerprints = env.fingerprints

  -- Check there are fingerprints to use
  if(#fingerprints == 0 ) then
    return false, "No fingerprints were loaded after processing ".. filename_full
  end

  return true, fingerprints
end


-- Extract Payloads
local function extract_payloads(packet)

  -- packet only contains HDR
  if #packet < 29 then return {} end

  local np = packet:byte(17) -- next payload
  local np_txt = PAYLOADS[np]
  local index = 29 -- starting point for search
  local ike_headers = {} -- ike headers

  -- loop over packet
  while np_txt and np_txt ~= "None" and index <= #packet do
    local payload_length, payload
    np, payload_length, index = string.unpack(">B x I2", packet, index)
    payload, index = string.unpack("c" .. (payload_length - 4), packet, index)
    payload = stdnse.tohex(payload)

    -- debug
    if np_txt == 'VID' then
      stdnse.debug2('IKE: Found IKE Header: %s - %s', np_txt, payload)
    else
      stdnse.debug2('IKE: Found IKE Header: %s', np_txt)
    end

    -- Store payload
    if ike_headers[np_txt] == nil then
      ike_headers[np_txt] = {payload}
    else
      table.insert(ike_headers[np_txt], payload)
    end

    np_txt = PAYLOADS[np]
  end
  return ike_headers

end




-- Search the fingerprint database for matches
-- This is a (currently) divided into two parts
--    1) version detection based on single fingerprints
--    2) version detection based on the order of all vendor ids
--
--  NOTE: the second step currently only has support for CISCO devices
--
-- Input is a table of collected vendor-ids, output is a table
-- with fields:
--  vendor, version, name, attributes (table), guess (table), os
local function lookup(vendor_ids)
  if vendor_ids == {} or vendor_ids == nil then return {} end

  -- concat all vids to one string
  local all_vids = ''
  for _,vid in pairs(vendor_ids) do all_vids = all_vids .. vid end

  -- the results
  local info = {
    vendor = nil,
    attribs = {},
  }
  local unmatched = {}

  local status, fingerprints
  status, fingerprints = load_fingerprints()

  if status then

    -- loop over the vendor_ids returned in ike request
    for _,vendor_id in pairs(vendor_ids) do

      -- loop over the fingerprints found in database
      for _,row in pairs(fingerprints) do

        if vendor_id:find(row.fingerprint) then

          -- if a match is found, check if it's a version detection or attribute
          if row.category == 'vendor' then
            local debug_string = ''
            if row.vendor  ~= nil then debug_string = debug_string .. row.vendor .. ' ' end
            if row.version ~= nil then debug_string = debug_string .. row.version       end
            stdnse.debug2("IKE: Fingerprint: %s matches %s", vendor_id,  debug_string)

            -- Only store the first match
            if info.vendor == nil then
              -- the fingerprint contains information about the VID
              info.vendor = row
            end

          elseif row.category == 'attribute' then
            info.attribs[ #info.attribs + 1] = row
            stdnse.debug2("IKE: Attribute: %s matches %s", vendor_id, row.text)
            break
          end
        else
          unmatched[#unmatched+1] = vendor_id
        end
      end
    end
  end
  if next(unmatched) then
    info.unknown_ids = unmatched
  end


  ---------------------------------------------------
  -- Search for the order of the vids
  -- Uses category 'vid_ordering'
  ---

  -- search in the 'vid_ordering' category
  local debug_string = ''
  for _,row in pairs(fingerprints) do

    if row.category == 'vid_ordering' and all_vids:find(row.fingerprint) then

      -- Use ordering information if there where no vendor matches from previous step
      if info.vendor == nil then
        info.vendor = row

        -- Debugging info
        debug_string = ''
        if info.vendor.vendor  ~= nil then debug_string = debug_string .. info.vendor.vendor  .. ' ' end
        if info.vendor.version ~= nil then debug_string = debug_string .. info.vendor.version .. ' ' end
        if info.vendor.ostype  ~= nil then debug_string = debug_string .. info.vendor.ostype         end
        stdnse.debug2('IKE: No vendor match, but ordering match found: %s', debug_string)

        return info

      -- Update OS based on ordering
      elseif info.vendor.vendor == row.vendor then
        info.vendor.ostype = row.ostype

        -- Debugging info
        debug_string = ''
        if info.vendor.vendor ~= nil then debug_string = debug_string .. info.vendor.vendor  .. ' to ' end
        if row.ostype ~= nil then debug_string = debug_string .. row.ostype end
        stdnse.debug2('IKE: Vendor and ordering match. OS updated: %s', debug_string)

        return info

      -- Only print debugging information if conflicting information is detected
      else
        -- Debugging info
        debug_string = ''
        if info.vendor.vendor ~= nil then debug_string = debug_string .. info.vendor.vendor  .. ' vs ' end
        if row.vendor ~= nil then debug_string = debug_string .. row.vendor end
        stdnse.debug2('IKE: Found an ordering match, but vendors do not match. %s', debug_string)

      end
    end
  end

  return info
end


---
-- Handle a response packet
--
-- A very limited response parser.
-- Currently only the VIDs are extracted.
-- This could be made more advanced to
-- allow for fingerprinting via the order
-- of the returned headers
-- @param packet A received IKE packet
-- @return A table of parsed response values
function response(packet)
  local resp = { ["mode"] = "", ["info"] = nil, ['vids']={}, ['success'] = false }

  if #packet > 19 then

    -- extract the return type
    local resp_type = EXCHANGE_TYPE[packet:byte(19)]
    local ike_headers = {}

    -- simple check that the type is something other than 'Informational'
    -- as this type does not include VIDs
    if resp_type ~= "Informational" then
      resp["mode"] = resp_type

      ike_headers = extract_payloads(packet)

      -- Extract the VIDs
      resp['vids'] = ike_headers['VID']

      -- search for fingerprints
      resp["info"] = lookup(resp['vids'])

      -- indicate that a packet 'useful' packet was returned
      resp['success'] = true
    end
  end

  return resp
end


--- Send a request and parse the response
--
-- Sends an IKE request such as generated by <code>ike.request()</code>,
-- binding to the same source port as the destination port.
-- @param host Destination host
-- @param port Destination port (table)
-- @return Parsed IKE response (output of <code>ike.response()</code>)
function send_request( host, port, packet )

  local socket = nmap.new_socket()

  -- lock resource (port 500/udp)
  local mutex = nmap.mutex("ike_port_500");
  mutex "lock";

  -- send the request packet
  socket:set_timeout(1000)
  socket:bind(nil, port.number)
  socket:connect(host, port, "udp")
  local s_status = socket:send(packet)

  -- receive answer
  if s_status then
    local r_status, data = socket:receive_bytes(1)

    if r_status then
      socket:close()

      -- release mutex
      mutex "done";
      return response(data)
    else
      socket:close()
    end
  else
    socket:close()
  end

  -- release mutex
  mutex "done";

  return {}
end

-- Create the aggressive part of a packet
--  Aggressive mode includes the user-id, so the
--  length of this has to be taken into account
--
local function generate_aggressive(port, protocol, id, diffie)
  -- get length of key data based on diffie
  local key_length
  if diffie == 1 then
    key_length = 96
  elseif diffie == 2 then
    key_length = 128
  elseif diffie == 5 then
    key_length = 192
  end

  return (
    -- Key Exchange
    string.pack(">Bx I2",
      0x0a, -- Next payload (Nonce)
      key_length + 4) -- Length
    .. rand.random_string(key_length) -- Random key data

    -- Nonce
    .. string.pack(">Bx I2",
      0x05, -- Next payload (Identification)
      20 + 4) -- Length
    ..rand.random_string(20) -- Nonce data

    -- Identification
    .. string.pack(">Bx I2 BBI2",
      0x00, -- Next Payload (None)
      #id + 4 + 4, -- Payload length
      0x03, -- ID Type (USER_FQDN)
      PROTOCOL_IDS[protocol], -- Protocol ID (UDP)
      port) -- Port (500)
    .. id
    )
end


-- Create the transform
-- AES encryption needs an extra value to define the key length
-- Currently only DES, 3DES and AES encryption is supported
--
local function generate_transform(auth, encryption, hash, group, number, total)
  local key_length, trans_length, aes_enc, sep, enc
  local next_payload, payload_number

  -- handle special case of aes
  if encryption:sub(1,3) == "aes" then
    trans_length = 0x0028
    enc = ENC_METHODS[encryption][1]
    key_length = ENC_METHODS[encryption][2]
  else
    trans_length = 0x0024
    enc = ENC_METHODS[encryption]
    key_length = nil
  end

  -- check if there are more transforms
  if number == total then
    next_payload = 0x00 -- none
  else
    next_payload = 0x03 -- transform
  end

  -- set the payload number

  local trans = string.pack(">Bx I2 BB xx I4I4I4I4",
  next_payload, -- Next payload
  trans_length, -- Transform length
  number, -- Transform number
  0x01, -- Transform ID (IKE)
  enc, -- Encryption algorithm
  HASH_ALGORITHM[hash], -- Hash algorithm
  AUTH_TYPES[auth], -- Authentication method
  GROUP_DESCRIPTION[group]  -- Group Description
  )

  if key_length ~= nil then
    trans = trans .. string.pack(">I4", key_length) -- only set for aes
  end

  trans = trans .. string.pack(">I4I8",
  0x800b0001, -- Life type (seconds)
  0x000c000400007080 -- Life duration (28800)
  )

  return trans
end


-- Generate multiple transforms
-- Input must be a table of complete transforms
--
local function generate_transforms(transform_table)
  local transforms = ''

  for i,t in pairs(transform_table) do
    transforms = transforms .. generate_transform(t.auth, t.encryption, t.hash, t.group, i, #transform_table)
  end

  return transforms
end


--- Create a request packet
--
-- Support for multiple transforms, which minimizes the
-- the amount of traffic/packets needed to be sent
-- @param port Associated port number
-- @param proto Associated protocol
-- @param mode "Aggressive" or "Main"
-- @param transforms Table of IKE transforms
-- @param diffie DH group number
-- @param id Identification data
-- @return IKE request datagram
function request(port, proto, mode, transforms, diffie, id)
  local payload_after_sa, str_aggressive, l, l_sa, l_pro

  local transform_string = generate_transforms(transforms)

  -- check for aggressive vs Main mode
  if mode == "Aggressive" then
    str_aggressive = generate_aggressive(port, proto, id, diffie)
    payload_after_sa = 0x04
  else
    str_aggressive = ""
    payload_after_sa = 0x00
  end


  -- calculate lengths
  l = 48 + transform_string:len() + str_aggressive:len()
  l_sa = 20 + transform_string:len()
  l_pro = 8 + transform_string:len()

  -- Build the packet
  local packet =
    rand.random_string(8) -- Initiator cookie
    .. ("\0"):rep(8) -- Responder cookie
  .. string.pack(">BBBBI4I4 BxI2I4I4 BxI2BBBB",
    0x01, -- Next payload (SA)
    0x10, -- Version
    EXCHANGE_MODE[mode], -- Exchange type
    0x00, -- Flags
    0x00000000, -- Message id
    l, -- packet length


    -- Security Association
    payload_after_sa, -- Next payload (Key exchange, if aggressive mode)
    l_sa, -- Length
    0x00000001, -- IPSEC
    0x00000001, -- Situation

    --## Proposal
    0x00, -- Next payload (None)
    l_pro, -- Payload length
    0x01, -- Proposal number
    0x01, -- Protocol ID (ISAKMP)
    0x00, -- SPI Size
    #transforms -- Proposal transforms
  )

  packet = packet .. transform_string -- transform

  if mode == 'Aggressive' then
    packet = packet .. str_aggressive
  end

  return packet
end


return _ENV