From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/ike.lua | 550 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 nselib/ike.lua (limited to 'nselib/ike.lua') diff --git a/nselib/ike.lua b/nselib/ike.lua new file mode 100644 index 0000000..d3278cb --- /dev/null +++ b/nselib/ike.lua @@ -0,0 +1,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 ike.request(), +-- 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 ike.response()) +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 -- cgit v1.2.3