--- -- Functions for vulnerability management. -- -- The vulnerabilities library may be used by scripts to report and -- store vulnerabilities in a common format. -- -- Reported vulnerabilities information must be stored in tables. -- Each vulnerability must have its own state: -- NOT_VULN: The program was confirmed to be not vulnerable. -- LIKELY_VULN: The program is likely to be vulnerable, -- this can be the case when we do a simple version comparison. This -- state should cover possible false positive situations. -- VULN: The program was confirmed to be vulnerable. -- EXPLOIT: The program was confirmed to be vulnerable and -- was exploited successfully. The VULN state will be -- set automatically. -- DoS: The program was confirmed to be vulnerable to Denial -- of Service attack. The VULN state will be set -- automatically. -- -- To match different vulnerability states, like the VULN -- and EXPLOIT states or the VULN and -- DoS states, one can use the bitwise operations. -- -- -- Vulnerability table: -- -------------------- -- -- local vuln_table = { -- title = "BSD ftpd Single Byte Buffer Overflow", -- mandatory field -- state = vulns.STATE.EXPLOIT, -- mandatory field -- -- Of course we must confirm the exploitation, otherwise just mark -- -- it vulns.STATE.VULN if the vulnerability was confirmed. -- -- states: 'NOT_VULN', 'LIKELY_VULN', 'VULN', 'DoS' and 'EXPLOIT' -- -- -- -- The following fields are all optional -- -- IDS = { -- Table of IDs -- -- ID Type ID (must be a string) -- CVE = 'CVE-2001-0053', -- BID = '2124', -- }, -- -- risk_factor = "High", -- 'High', 'Medium' or 'Low' -- scores = { -- A map of the different scores -- CVSS = "10.0", -- CVSSv2 = "...", -- }, -- -- description = [[ -- One-byte buffer overflow in BSD-based ftpd allows remote attackers -- to gain root privileges.]], -- -- dates = { -- disclosure = { year = 2000, month = 12, day = 18}, -- }, -- -- check_results = { -- A string or a list of strings -- -- This field can store the results of the vulnerability check. -- -- Did the server return anything ? some specialists can -- -- investigate this and decide if the program is vulnerable. -- }, -- -- exploit_results = { -- A string or a list of strings -- -- This field can store the results of the exploitation. -- }, -- -- extra_info = { -- A string or a list of strings -- -- This field can be used to store and shown any useful -- -- information about the vulnerability, server, etc. -- }, -- -- references = { -- List of references -- 'http://www.openbsd.org/advisories/ftpd_replydirname.txt', -- -- -- If some popular IDs like 'CVE' and 'OSVBD' are provided -- -- then their links will be automatically constructed. -- }, -- } -- -- -- -- The following examples illustrates how to use the library. -- -- Examples for portrule and hostrule scripts: -- -- -- portrule and hostrule scripts must use the vulns.Report class -- -- to report vulnerabilities -- local vuln_table = { -- title = "BSD ftpd Single Byte Buffer Overflow", -- mandatory field -- references = { -- List of references -- 'http://www.openbsd.org/advisories/ftpd_replydirname.txt', -- }, -- ... -- } -- ... -- vuln_table.state = vulns.STATE.VULN -- local report = vulns.Report:new(SCRIPT_NAME, host, port) -- return report:make_output(vuln_table, ...) -- -- -- -- local vuln_table = { -- title = "BSD ftpd Single Byte Buffer Overflow", -- mandatory field -- references = { -- List of references -- 'http://www.openbsd.org/advisories/ftpd_replydirname.txt', -- }, -- ... -- } -- ... -- vuln_table.state = vulns.STATE.VULN -- local report = vulns.Report:new(SCRIPT_NAME, host, port) -- report:add(vuln_table, ...) -- return report:make_output() -- -- -- -- Examples for prerule and postrule scripts: -- -- local FID -- my script FILTER ID -- -- prerule = function() -- FID = vulns.save_reports() -- if FID then -- return true -- end -- return false -- end -- -- postrule = function() -- if nmap.registry[SCRIPT_NAME] then -- FID = nmap.registry[SCRIPT_NAME].FID -- if vulns.get_ids(FID) then -- return true -- end -- end -- return false -- end -- -- prerule_action = function() -- nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {} -- nmap.registry[SCRIPT_NAME].FID = FID -- return nil -- end -- -- postrule_action = function() -- return vulns.make_output(FID) -- show all the vulnerabilities -- end -- -- local tactions = { -- prerule = prerule_action, -- postrule = postrule_action, -- } -- -- action = function(...) return tactions[SCRIPT_TYPE](...) end -- -- -- -- Library debug messages: -- -- * Level 2: show the NOT VULNERABLE entries. -- * Level 3: show all the vulnerabilities that are saved into the registry. -- * Level 5: show all the other debug messages (useful for debugging). -- -- Note: Vulnerability tables are always re-constructed before they are -- saved in the registry. We do this to avoid using vulnerability tables -- that are referenced by other objects to let the Lua garbage-collector -- collect these last objects. -- -- @args vulns.showall If set, the library will show and report all the -- registered vulnerabilities which includes the -- NOT VULNERABLE ones. By default the library will only -- report the VULNERABLE entries: VULNERABLE, -- LIKELY VULNERABLE, VULNERABLE (DoS) -- and VULNERABLE (Exploitable). -- This argument affects the following functions: -- vulns.Report.make_output(): the default output function for -- portule/hostrule scripts. -- vulns.make_output(): the default output function for postrule scripts. -- vulns.format_vuln() and vulns.format_vuln_table() functions. -- @args vulns.short If set, vulnerabilities will be output in short format, a -- single line consisting of the host's target name or IP, the state, and -- either the CVE ID or the title of the vulnerability. Does not affect XML output. -- -- @author Djalal Harouni -- @author Henri Doreau -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html local ipOps = require "ipOps" local nmap = require "nmap" local stdnse = require "stdnse" local string = require "string" local stringaux = require "stringaux" local table = require "table" local type = type local next = next local pairs = pairs local ipairs = ipairs local select = select local tostring = tostring local insert = table.insert local concat = table.concat local sort = table.sort local setmetatable = setmetatable local string_format = string.format local string_upper = string.upper local debug = stdnse.debug local compare_ip = ipOps.compare_ip _ENV = stdnse.module("vulns", stdnse.seeall) -- This is the vulnerability database -- (it will reference a table in the registry: nmap.registry.VULNS -- see the save_reports() function). local VULNS -- Vulnerability Database (registry) internal data representation -- -- -- VULNS = nmap.registry.VULNS -- VULNS = { -- -- -- Vulnerability entries -- ENTRIES = { -- -- HOSTS = { -- -- Table of hosts -- [host_a_ip] = { -- -- list of vulnerabilities that affect the host A -- { -- vuln_1 -- title = 'Program X vulnerability', -- state = vulns.State.VULN, -- IDS = {CVE = 'CVE-XXXX-XXXX', OSVDB = 'XXXXX'}, -- -- -- the following fields are all optional -- risk_factor = 'High', -- description = 'vulnerability description ...', -- -- references = VULNS.SHARED.REFERENCES[x], -- }, -- -- { -- vuln_2 -- ... -- }, -- ... -- }, -- -- [host_b_ip] = { -- ... -- }, -- }, -- -- NETWORKS = { -- -- list of vulnerabilities that lacks the 'host' table -- { -- vuln_1 -- ... -- }, -- { -- ... -- }, -- }, -- }, -- -- -- Store shared data between vulnerabilities here (type of data: tables) -- SHARED = { -- -- List of references, members will be referenced by the previous -- -- vulnerability entries. -- REFERENCES = { -- { -- ["http://..."] = true, -- ["http://..."] = true, -- ... -- }, -- { -- ... -- }, -- }, -- }, -- -- -- These are tables that are associated with the different filters. -- -- This will help the vulnerabilities lookup mechanism. -- -- -- -- Just caches to reference all the vulnerabilities information: -- -- tables, maps etc. Only memory addresses are stored here. -- FILTER_IDS = { -- -- [fid_1] = { -- FILTER ID as it returned by vulns.save_reports() -- 'CVE' = { -- 'CVE-XXXX-XXXX' = { -- ENTRIES = { -- HOSTS = { -- -- References to hosts and their vulnerabilities -- -- -- The same IP address with multiple targetnames. -- [host_a_ip] = { -- [host_a_ip_targetname_x] = -- VULNS.ENTRIES.HOSTS[host_a_ip][vuln_x], -- [host_a_ip_targetname_y] = -- VULNS.ENTRIES.HOSTS[host_a_ip][vuln_y], -- } -- [host_x_ip] = { -- [host_x_targetname_x or host_x_ip] = -- VULNS.ENTRIES.HOSTS[host_x][vuln_x], -- } -- [host_y_ip] = { -- [host_y_targetname_y or host_y_ip] = -- VULNS.ENTRIES.HOSTS[host_y][vuln_z], -- } -- ... -- }, -- NETWORKS = { -- VULNS.ENTRIES.NETWORKS[vuln_x], -- ... -- } -- }, -- }, -- -- 'CVE-YYYY-YYYY' = { -- -- }, -- }, -- -- 'OSVDB' = { -- 'XXXXX' = { -- -- entries = { -- ... -- }, -- }, -- 'YYYYY' = { -- entries = { -- ... -- }, -- }, -- }, -- -- 'YOUR_FAVORITE_ID' = { -- 'XXXXX' = { -- ... -- }, -- }, -- -- -- Entries without the vulnerability ID are stored here. -- 'NMAP_ID' = { -- 'XXXXX' = { -- ... -- }, -- }, -- }, -- -- [fid_2] = { -- ... -- }, -- -- [fid_3] = { -- ... -- }, -- }, -- -- -- List of the filters callbacks -- FILTERS_FUNCS = { -- [fid_1] = callback_filter_1, -- [fid_2] = callback_filter_2, -- ... -- } -- -- } -- end of VULNS -- This value is used to reference vulnerability entries -- that lacks vulnerability IDs. local NMAP_ID_NUM = 0 -- SHOW_ALL: if set the format and make_output() functions will -- show the vulnerability entries with a state == NOT_VULN local SHOW_ALL = stdnse.get_script_args('vulns.showall') or stdnse.get_script_args('vuln.showall') or stdnse.get_script_args('vulns.show-all') or stdnse.get_script_args('vuln.show-all') local SHORT_OUTPUT = stdnse.get_script_args('vulns.short') -- The different states of the vulnerability STATE = { LIKELY_VULN = 0x01, NOT_VULN = 0x02, VULN = 0x04, DoS = 0x08, EXPLOIT = 0x10, UNKNOWN = 0x20, } -- The vulnerability messages. STATE_MSG = { [STATE.LIKELY_VULN] = 'LIKELY VULNERABLE', [STATE.NOT_VULN] = 'NOT VULNERABLE', [STATE.VULN] = 'VULNERABLE', [STATE.DoS] = 'VULNERABLE (DoS)', [STATE.EXPLOIT] = 'VULNERABLE (Exploitable)', [STATE.DoS | STATE.VULN] = 'VULNERABLE (DoS)', [STATE.EXPLOIT | STATE.VULN] = 'VULNERABLE (Exploitable)', [STATE.UNKNOWN] = 'UNKNOWN (unable to test)', } -- Scripts must provide the correct risk factor string. local RISK_FACTORS = { ['HIGH'] = true, ['MEDIUM'] = true, ['LOW'] = true, } -- Use this function to copy a variable into another one. -- If the src is an empty table then return nil. -- Note: this is a special function for this library. local function tcopy(src) if src and type(src) == "table" then if next(src) then local dst = {} for k,v in pairs(src) do if type(v) == "table" then dst[k] = tcopy(v) else dst[k] = v end end return dst else return nil end end return src end -- Use this function to push data from src list to dst list. local function tadd(dst, src) if dst and type(dst) == "table" and src and type(src) == "table" then for _, v in ipairs(src) do dst[#dst + 1] = v end end end -- A list of popular vulnerability IDs with their callbacks to -- construct and return the correct links. local POPULAR_IDS_LINKS = { CVE = function(id) local link = 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=' return string_format("%s%s", link, id) end, OSVDB = function(id) local link = 'http://osvdb.org/' return string_format("%s%s", link, id) end, BID = function(id) local link = 'https://www.securityfocus.com/bid/' return string_format("%s%s", link, id) end, } --- Registers and associates a callback function with the popular ID -- vulnerability type to construct and return popular links -- automatically. -- -- The callback function takes a vulnerability ID as a parameter -- and must return a link. The library automatically supports three -- different popular IDs: -- CVE: cve.mitre.org -- OSVDB: osvdb.org -- BID: www.securityfocus.com/bid -- -- @usage -- function get_example_link(id) -- return string.format("%s%s", -- "http://example.com/example?name=", id) -- end -- vulns.register_popular_id('EXM-ID', get_example_link) -- -- @param id_type String representing the vulnerability ID type. -- 'CVE', 'OSVDB' ... -- @param callback A function to construct and return links. -- @return True on success or false if it can not register the callback. register_popular_id = function(id_type, callback) if id_type and callback and type(id_type) == "string" and type(callback) == "function" then POPULAR_IDS_LINKS[string_upper(id_type)] = callback return true end return false end --- Calls the function associated with the popular ID vulnerability -- type to construct and to return the appropriate reference link. -- -- The library automatically supports three different popular IDs: -- CVE: cve.mitre.org -- OSVDB: osvdb.org -- BID: www.securityfocus.com/bid -- -- @usage -- local link = vulns.get_popular_link('CVE', 'CVE-2001-0053') -- -- @param id_type String representing the vulnerability ID type. -- 'CVE', 'OSVDB' ... -- @param id String representing the vulnerability ID. -- @return URI The URI on success or nil if the library does not support -- the specified id_type, and in this case you can register -- new ID types by calling vulns.register_popular_id(). get_popular_link = function(id_type, id) local id_vuln_type = string_upper(id_type) if POPULAR_IDS_LINKS[id_vuln_type] then return POPULAR_IDS_LINKS[id_vuln_type](id) end end --- Validate the vulnerability information -- -- @param vuln_table The vulnerability information table. -- @return True on success or false if some mandatory information is -- missing. local validate_vuln = function(vuln_table) local ret = false if type(vuln_table) == "table" and vuln_table.title and type(vuln_table.title) == "string" and vuln_table.state and STATE_MSG[vuln_table.state] then if vuln_table.risk_factor then if type(vuln_table.risk_factor) == "string" and vuln_table.risk_factor:len() > 0 then if RISK_FACTORS[string_upper(vuln_table.risk_factor)] then ret = true end end else ret = true end end return ret end --- Normalize the vulnerability information. -- -- This function will modify the internal fields of the vulnerability. -- -- @param vuln_table The vulnerability information table. local normalize_vuln_info = function(vuln_table) if not vuln_table.IDS then vuln_table.IDS = vuln_table.ids or {} end if not next(vuln_table.IDS) then -- Use the internal NMAP_ID if vulnerability IDs are missing. NMAP_ID_NUM = NMAP_ID_NUM + 1 -- Push IDs as strings instead of numbers to avoid -- dealing with array holes. vuln_table.IDS.NMAP_ID = string_format("NMAP-%d", NMAP_ID_NUM) else for id_type, id in pairs(vuln_table.IDS) do -- Push IDs as strings instead of numbers to avoid -- dealing with array holes. if type(id) == "number" then vuln_table.IDS[id_type] = tostring(id) end end end -- If the vulnerability state is 'DoS' or 'EXPLOIT' then set -- the 'VULN' state. if vuln_table.state == STATE.DoS or vuln_table.state == STATE.EXPLOIT then vuln_table.state = vuln_table.state | STATE.VULN end -- Convert the following string fields to tables. if vuln_table.description and type(vuln_table.description) == "string" then vuln_table.description = {vuln_table.description} end if vuln_table.check_results and type(vuln_table.check_results) == "string" then vuln_table.check_results = {vuln_table.check_results} end if vuln_table.exploit_results and type(vuln_table.exploit_results) == "string" then vuln_table.exploit_results = {vuln_table.exploit_results} end if vuln_table.extra_info and type(vuln_table.extra_info) == "string" then vuln_table.extra_info = {vuln_table.extra_info} end if vuln_table.references and type(vuln_table.references) == "string" then vuln_table.references = {vuln_table.references} end end -- Default filter to use if the script did not provide one. local default_filter = function(vuln_table) return true end --- Register the callback filters. -- -- This function just inserts the callback filters in the filters_db. -- -- @param filters_db The filters database (a table in the registry). -- @param filter_callback The callback function. -- @return FID The filter ID associated with the callback function. local register_filter = function(filters_db, filter_callback) if filter_callback and type(filter_callback) == "function" then filters_db[#filters_db + 1] = filter_callback else filters_db[#filters_db + 1] = default_filter end return #filters_db end --- Call filter functions. -- -- The callback filters will take a vulnerability table and inspect -- it. The vulnerability will be stored in the registry if one of these -- filters return true. -- -- @param filters_db The filters database (a table in the registry). -- @param vuln_table The vulnerability information table. -- @return List The list of filters that have returned True. If all the -- Filters functions returned false then nil will be returned. local filter_vulns = function(filters_db, vuln_table) local FIDS = {} for fid, callback in ipairs(filters_db) do if callback(vuln_table) == true then FIDS[#FIDS + 1] = fid end end return next(FIDS) and FIDS or nil end --- Add IDs to the ID table -- -- IDs can be 'CVE', 'OSVDB', 'BID' ... -- @usage -- l_add_id_type(fid_table, 'CVE') -- -- @param fid_table The filter ID table. -- @param id_type String representing the vulnerability ID type. local l_add_id_type = function(fid_table, id_type) fid_table[string_upper(id_type)] = fid_table[id_type] or {} end --- Get simple "targetname:port_number" keys local l_get_host_port_key = function(vuln_table) local target = "" if vuln_table.host and next(vuln_table.host) then target = stdnse.get_hostname(vuln_table.host) if vuln_table.port and next(vuln_table.port) then target = target..string_format(":%d", vuln_table.port.number) end end return target end --- Update the FILTER ID table references. -- -- When a new vulnerability table is stored in the registry in the -- nmap.registry.VULNS.ENTRIES database, we will also update -- the nmap.registry.VULNS.FILTERS_IDS[fid_table] to -- reference the new saved vulnerability. -- -- @usage -- l_update_id(fid_table, 'CVE', 'CVE-2001-0053', vuln_table) -- -- @param fid_table The filter ID table. -- @param id_type String representing the vulnerability ID type. -- 'CVE', 'OSVDB' ... -- @param id String representing the vulnerability ID. -- @param vuln_table The vulnerability table reference that was stored -- in the registry nmap.registry.VULNS.ENTRIES. -- @return Table A reference to the vulnerability table that was just -- saved in the FILTER ID table. local l_update_id = function(fid_table, id_type, id, vuln_table) local id_type = string_upper(id_type) -- Add the ID vulnerability type if it is missing l_add_id_type(fid_table, id_type) -- Make sure that we are referencing the correct tables fid_table[id_type][id] = fid_table[id_type][id] or {} fid_table[id_type][id]['ENTRIES'] = fid_table[id_type][id]['ENTRIES'] or {} local push_table = fid_table[id_type][id]['ENTRIES'] if vuln_table.host and next(vuln_table.host) then local target_key = l_get_host_port_key(vuln_table) local host_info = string_format(" (host:%s %s)", vuln_table.host.ip, target_key) debug(5, "vulns.lua: Updating VULNS.FILTERS_IDS{} with '%s' ID:%s:%s %s", vuln_table.title, id_type, id, host_info) push_table.HOSTS = push_table.HOSTS or {} push_table.HOSTS[vuln_table.host.ip] = push_table.HOSTS[vuln_table.host.ip] or {} push_table.HOSTS[vuln_table.host.ip][target_key] = vuln_table return push_table.HOSTS[vuln_table.host.ip][target_key] else debug(5, "vulns.lua: Updating VULNS.FILTERS_IDS{} with '%s' ID:%s:%s", vuln_table.title, id_type, id) push_table.NETWORKS = push_table.NETWORKS or {} push_table.NETWORKS[#push_table.NETWORKS + 1] = vuln_table return push_table.NETWORKS[#push_table.NETWORKS] end end --- Lookup for vulnerability ID in the vulnerability database -- associated with the FILTER ID, and return -- a table of vulnerabilities identified by the provided ID. -- -- @usage -- local ids_table = l_lookup_id(fid_table, 'CVE', 'CVE-2001-0053') -- -- @param fid_table The filter ID table. -- @param id_type String representing the vulnerability ID type. -- 'CVE', 'OSVDB' ... -- @param id String representing the vulnerability ID. -- @return Table A table of vulnerabilities if there are entries -- identified by the id parameter, otherwise nil. local l_lookup_id = function(fid_table, id_type, id) local id_type = string_upper(id_type) if fid_table[id_type] then return fid_table[id_type][id] end end --- Save the references in the references_db -- -- @param references_db The references_db which is a table in the registry -- @param new_refs A list of references to save. -- @return table The table of references in the references_db. local l_push_references = function(references_db, new_refs) if new_refs and next(new_refs) then local refs = {} for _, l in ipairs(new_refs) do refs[l] = true end insert(references_db, refs) return references_db[#references_db] end end --- Re-construct the vulnerability table and save it in the vulnerability -- database (vulndb: registry). -- -- @param vulndb The vulnerability database which is a table in the -- registry. -- @param new_vuln The vulnerability information table. -- @return vuln_table The vulnerability table in the vulndb. local l_push_vuln = function(vulndb, new_vuln) -- Reconstruct the vulnerability table to avoid referencing -- any old external data. -- e.g: we can have other objects that reference the 'new_vuln' -- object, so we reconstruct the 'vuln' object to not reference -- the 'new_vuln' and to let the GC collect the 'new_vuln' and -- any other external object referencing it. local new_vuln = new_vuln local vuln = { title = new_vuln.title, state = new_vuln.state, _FIDS_MATCH = tcopy(new_vuln._FIDS_MATCH), IDS = {}, } if new_vuln.IDS and next(new_vuln.IDS) then for id_type, id in pairs(new_vuln.IDS) do vuln.IDS[string_upper(id_type)] = id end end -- Save these fields only when the state is not 'NOT VULNERABLE' if (new_vuln.state & STATE.NOT_VULN) == 0 then if new_vuln.risk_factor then vuln.risk_factor = new_vuln.risk_factor vuln.scores = tcopy(new_vuln.scores) end vuln.description = tcopy(new_vuln.description) vuln.dates = tcopy(new_vuln.dates) -- Store the following information for the post-processing scripts --vuln.check_results = tcopy(new_vuln.check_results) --if vuln.check_results then -- insert(vuln.check_results, 1, -- string_format("Script %s checks:", new_vuln.script_name)) --end --if (vuln.state & STATE.EXPLOIT) ~= 0 then -- vuln.exploit_results = tcopy(new_vuln.exploit_results) -- if vuln.exploit_results then -- insert(vuln.exploit_results, 1, -- string_format("Script %s exploits:", new_vuln.script_name)) -- end --end --vuln.extra_info = tcopy(new_vuln.extra_info) --if vuln.extra_info then -- insert(vuln.extra_info, 1, -- string_format("Script %s info:", new_vuln.script_name)) --end end vuln.references = l_push_references(vulndb.SHARED.REFERENCES, new_vuln.references) if new_vuln.script_name then vuln.scripts = {} insert(vuln.scripts, new_vuln.script_name) end local ref_vuln if new_vuln.host and next(new_vuln.host) then vuln.host = tcopy(new_vuln.host) vuln.port = tcopy(new_vuln.port) vulndb.ENTRIES.HOSTS[vuln.host.ip] = vulndb.ENTRIES.HOSTS[vuln.host.ip] or {} insert(vulndb.ENTRIES.HOSTS[vuln.host.ip], vuln) ref_vuln = vulndb.ENTRIES.HOSTS[vuln.host.ip][#vulndb.ENTRIES.HOSTS[vuln.host.ip]] else insert(vulndb.ENTRIES.NETWORKS, vuln) ref_vuln = vulndb.ENTRIES.NETWORKS[#vulndb.ENTRIES.NETWORKS] end -- Return a reference to the vulnerability table in the registry return ref_vuln end --- Update the references that are stored in the references_db -- -- @param references_db The references_db which is a table in the registry -- @param old_refs A table of the previously saved references. -- @param new_refs A list of references to save. -- @return table The table of updated references in the references_db. local l_update_references = function(references_db, old_refs, new_refs) if old_refs and next(old_refs) and new_refs and next(new_refs) then for _, l in ipairs(new_refs) do old_refs[l] = true end end return next(old_refs) and old_refs or nil end --- Update the vulnerability information table that was stored in the -- vulnerability database (vulndb: registry). -- -- @param vulndb The vulnerability database which is a table in the registry. -- @param old_vuln The old vulnerability table stored in the vulndb. -- @param new_vuln The new vulnerability information table. -- @return vuln_table The updated vulnerability table in the vulndb. local l_update_vuln = function(vulndb, old_vuln, new_vuln) local old_vuln, new_vuln = old_vuln, new_vuln -- Update vulnerability state if old_vuln.state < new_vuln.state then old_vuln.state = new_vuln.state end -- Update the FILTERS IDS MATCH for fid_table in pairs(new_vuln._FIDS_MATCH) do old_vuln[fid_table] = true end -- Add new IDs to the old vulnerability entry if new_vuln.IDS and next(new_vuln.IDS) then for id_type, id in pairs(new_vuln.IDS) do local id_vuln_type = string_upper(id_type) if not old_vuln.IDS[id_vuln_type] then old_vuln.IDS[id_vuln_type] = id end end end -- Remove these fields if the state is NOT VULNERABLE -- Note: At this level the old_vuln.state was already updated. if (old_vuln.state & STATE.NOT_VULN) ~= 0 then old_vuln.risk_factor = nil old_vuln.scores = nil old_vuln.description = nil old_vuln.dates = nil --old_vuln.check_results = nil --old_vuln.exploit_results = nil --old_vuln.extra_info = nil else if new_vuln.risk_factor then old_vuln.risk_factor = new_vuln.risk_factor if not old_vuln.scores and new_vuln.scores then old_vuln.scores = tcopy(new_vuln.scores) end end if not old_vuln.description and new_vuln.description then old_vuln.description = tcopy(new_vuln.description) end if not old_vuln.dates and new_vuln.dates then old_vuln.dates = tcopy(old_vuln.dates) end -- Store the following information for the post-processing scripts --if new_vuln.check_results then -- old_vuln.check_results = old_vuln.check_results or {} -- insert(old_vuln.check_results, -- string_format("Script %s checks:", new_vuln.script_name)) -- tadd(old_vuln.check_results, new_vuln.check_results) --end --if new_vuln.exploit_results and --(old_vuln.state & STATE.EXPLOIT) ~= 0 then -- old_vuln.exploit_results = old_vuln.exploit_results or {} -- insert(old_vuln.exploit_results, -- string_format("Script %s exploits:", new_vuln.script_name)) -- tadd(old_vuln.exploit_results, new_vuln.exploit_results) --end --if new_vuln.extra_info then -- old_vuln.extra_info = old_vuln.extra_info or {} -- insert(old_vuln.extra_info, -- string_format("Script %s info:", new_vuln.script_name)) -- tadd(old_vuln.extra_info, new_vuln.extra_info) --end end -- Update the 'port' table if necessary if not old_vuln.port and new_vuln.port then old_vuln.port = tcopy(new_vuln.port) end -- Add the script name to the list of scripts that tested this -- vulnerability. if new_vuln.script_name then old_vuln.scripts = old_vuln.scripts or {} insert(old_vuln.scripts, new_vuln.script_name) end -- Update the references links if new_vuln.references and next(new_vuln.references) then old_vuln.references = l_update_references(vulndb.SHARED.REFERENCES, old_vuln.references, new_vuln.references) end return old_vuln end --- Adds the vulnerability table to the vulndb (registry). -- -- @param vulndb The vulnerability database which is a table in the -- registry. -- @param vuln_table The vulnerability information table. -- @return True if the vulnerability information table was saved, -- otherwise False. local l_add = function(vulndb, vuln_table) local vuln_table = vuln_table -- Get the Filters IDs list local FIDS = filter_vulns(vulndb.FILTERS_FUNCS, vuln_table) -- All the Filters denied the vulnerability entry if not FIDS then return false else -- Store the Filters IDS that will reference this vulnerability -- This is a special field vuln_table._FIDS_MATCH = {} for _, fid in ipairs(FIDS) do vuln_table._FIDS_MATCH[vulndb.FILTERS_IDS[fid]] = true end end -- If we are here then the vulnerability entry has passed -- some filters. The list of passed filters is stored in the -- FIDS variable -- Store the new vulnerability IDS in this list: -- 1) If the vulnerability is new then store all the IDS. -- 2) If the vulnerability was already pushed, then we can have a -- situation when the current vulnerability table (which is the -- same vulnerability that was already pushed) have some new -- IDS entries, and in this case we will also save these new IDS, -- and make them reference the old vulnerability entry. local NEW_IDS = {} -- If the vulnerability was already saved in the registry, then -- store its references here. local old_entries = {} -- Count how many vuln_table.IDS entries should be and should reference -- the vulnerability table in the registry -- (in all the FILTERS_IDS tables). local ids_count = 0 -- Count how many vuln_table.IDS entries are referencing an old -- vulnerability entry that was already saved in the registry. local ids_found = 0 local host_info, target_key = "", "" if vuln_table.host and next(vuln_table.host) then target_key = l_get_host_port_key(vuln_table) host_info = string_format(" (host:%s %s)", vuln_table.host.ip, target_key) end -- Search the Filters IDS for the vulnerability for _, fid in ipairs(FIDS) do for id_type, id in pairs(vuln_table.IDS) do -- Count how many IDs should be referencing the vulnerability -- entry in all the FILTERS_IDS tables. ids_count = ids_count + 1 -- If the IDs are referencing an old vulnerability entry -- that was saved previously in the registry then make this -- variable false. local id_not_found = true debug(5, "vulns.lua: Searching VULNS.FILTERS_IDS[%d] for '%s' ID:%s:%s", fid, vuln_table.title, id_type, id) local db = l_lookup_id(vulndb.FILTERS_IDS[fid], id_type, id) if db and db.ENTRIES and db.ENTRIES.HOSTS then if vuln_table.host and next(vuln_table.host) then local old_vuln_list = db.ENTRIES.HOSTS[vuln_table.host.ip] if old_vuln_list then -- Host IP is already affected by this vulnerability. -- Check the couple "targetname:port" now local tmp_vuln = old_vuln_list[target_key] if tmp_vuln then debug(5, "vulns.lua: VULNS.FILTERS_IDS[%d] '%s' ID:%s:%s%s: FOUND", fid, vuln_table.title, id_type, id, host_info) if old_entries[#old_entries] ~= tmp_vuln then old_entries[#old_entries + 1] = tmp_vuln end ids_found = ids_found + 1 -- The ID couple is correctly referencing a vulnerability -- entry in the vulnerability database (registry). id_not_found = false end end end end -- If the ID couple (id_type, id) was not found then save it -- in order to make it later reference the saved vulnerability -- entry (vulnerability table in the registry). if id_not_found then debug(5, "vulns.lua: VULNS.FILTERS_IDS[%d] '%s' ID:%s:%s%s: NOT FOUND", fid, vuln_table.title, id_type, id, host_info) NEW_IDS[id_type] = {['id'] = id, ['fid'] = fid} end end end -- This will reference the vulnerability table that was saved -- in the registry. local vuln_ref -- Old entry, update the vulnerability information if ids_found > 0 then if #old_entries > 1 then debug(3, "vulns.lua: Warning at vuln '%s': ".. "please check the vulnerability IDs field.", vuln_table.title) for _, old_vuln in ipairs(old_entries) do debug(3, "vulns: Warning at vuln '%s': ".. "please check the vulnerability IDs field.", old_vuln.title) end end debug(3, "vulns.lua: Updating vulnerability entry: '%s'%s", vuln_table.title, host_info) debug(3, "vulns.lua: Vulnerability '%s' referenced by %d IDs from %d (%s)", vuln_table.title, ids_found, ids_count, ids_found < ids_count and "Bad" or "Good") -- Update the vulnerability entry with the first one found. -- Note: Script writers must provide correct IDs or things can -- go bad. vuln_ref = l_update_vuln(vulndb, old_entries[1], vuln_table) else -- New vulnerability entry debug(3, "vulns.lua: Adding new vulnerability entry: '%s'%s", vuln_table.title, host_info) -- Push the new vulnerability into the registry vuln_ref = l_push_vuln(vulndb, vuln_table) end -- Update the FILTERS IDS tables to reference the vulnerability entry -- This vulnerability entry is now saved in the registry. if ids_found < ids_count then for _, fid in ipairs(FIDS) do for id_type, new_entry in pairs(NEW_IDS) do if new_entry['fid'] == fid then -- Add the ID couple (id_type, id) to the -- VULNS.FILTERS_IDS[fid] table that lacks them debug(5, "vulns.lua: Updating VULNS.FILTERS_IDS[%d]", new_entry.fid) l_update_id(vulndb.FILTERS_IDS[new_entry['fid']], id_type, new_entry.id, vuln_ref) end end end end return true end --- Check and normalize the selection filter fields. -- -- @param Filter The selection filter table. -- @return Table The new selection filter that should be used. local l_normalize_selection_filter = function(filter) if filter and type(filter) == "table" and next(filter) then local ret = {} if filter.state and STATE_MSG[filter.state] then ret.state = filter.state end if filter.risk_factor and type(filter.risk_factor) == "string" and RISK_FACTORS[string_upper(filter.risk_factor)] then ret.risk_factor = string_upper(filter.risk_factor) end if filter.hosts_filter and type(filter.hosts_filter) == "function" then ret.hosts_filter = filter.hosts_filter end if filter.ports_filter and type(filter.ports_filter) == "function" then ret.ports_filter = filter.ports_filter end if filter.id_type and type(filter.id_type) == "string" then ret.id_type = string_upper(filter.id_type) ret.id = filter.id end return ret end end --- Checks the vulnerability table against the provided selection filter -- -- @param vuln_table The vulnerability information table. -- @param Filter The filter table. -- @return True if the vulnerability table passes the selection filter, -- otherwise False. local l_filter_vuln = function(vuln_table, filter) if filter and next(filter) then if filter.state and (vuln_table.state & filter.state) == 0 then return false end if filter.risk_factor then if not vuln_table.risk_factor or string_upper(vuln_table.risk_factor) ~= string_upper(filter.risk_factor) then return false end end if filter.hosts_filter then if not vuln_table.host or not next(vuln_table.host) or not filter.hosts_filter(vuln_table.host) then return false end end if filter.ports_filter then if not vuln_table.port or not next(vuln_table.port) or not filter.ports_filter(vuln_table.port) then return false end end if filter.id_type then if not vuln_table.IDS or not next(vuln_table.IDS) or not vuln_table.IDS[filter.id_type] then return false elseif filter.id then return (vuln_table.IDS[filter.id_type] == filter.id) end end end return true end --- Find vulnerabilities by ID local l_find_by_id = function(fid_table, vuln_id_type, id) local out = {} local db = l_lookup_id(fid_table, vuln_id_type, id) if db then debug(5, "vulns.lua: Lookup VULNS.FILTERS_IDS{} for ID:%s:%s: FOUND", vuln_id_type, id) if db.ENTRIES and db.ENTRIES.HOSTS and next(db.ENTRIES.HOSTS) then for _, vuln_list in pairs(db.ENTRIES.HOSTS) do for _, vuln_table in pairs(vuln_list) do debug(5, "vulns.lua: Vulnerability '%s' (host:%s): FOUND", vuln_table.title, vuln_table.host.ip) out[#out + 1] = vuln_table end end end if db.ENTRIES.NETWORKS and next(db.ENTRIES.NETWORKS) then for _, vuln_table in ipairs(db.ENTRIES.NETWOKRS) do debug(5, "vulns.lua: Vulnerability '%s': FOUND", vuln_table.title) out[#out + 1] = vuln_table end end end return next(out) and out or nil end --- Find vulnerabilities. local l_find_vulns = function(fid_table, entries, filter) local out, check_vuln = {} if filter then check_vuln = function(vuln_table, fid_table, filter) -- Check if this vulnerability entry is referenced by the fid_table return vuln_table._FIDS_MATCH[fid_table] and l_filter_vuln(vuln_table, filter) end else check_vuln = function(vuln_table, fid_table) return vuln_table._FIDS_MATCH[fid_table] end end for host_ip, vulns_list in pairs(entries.HOSTS) do for _, vuln_table in ipairs(vulns_list) do if check_vuln(vuln_table, fid_table, filter) then debug(5, "vulns.lua: Vulnerability '%s' (host: %s): FOUND", vuln_table.title, vuln_table.host.ip) out[#out + 1] = vuln_table end end end for _, vuln_table in ipairs(entries.NETWORKS) do if check_vuln(vuln_table, fid_table, filter) then debug(5, "vulns.lua: Vulnerability '%s': FOUND", vuln_table.title) out[#out + 1] = vuln_table end end return next(out) and out or nil end --- Format and push vulnerabilities into an output table. local l_push_vuln_output = function(output, vlist, showall) local out, vuln_list = output, vlist for idx, vuln_table in ipairs(vuln_list) do local vuln_out = format_vuln_table(vuln_table, showall) if vuln_out then insert(out, concat(vuln_out, "\n")) if #vuln_list > 1 and idx ~= #vuln_list then insert(out, "") end end end end --- Report vulnerabilities. local l_make_output = function(fid_table, entries, filter) local hosts, networks = {}, {vulns = {}, not_vulns = {}} local save_not_vulns = function(vulns, vuln_table) end if SHOW_ALL then save_not_vulns = function(vulns, vuln_table) vulns[#vulns + 1] = vuln_table end end local check_vuln if filter then check_vuln = function(vuln_table, fid_table, filter) -- Check if this vulnerability entry is referenced by the fid_table return vuln_table._FIDS_MATCH[fid_table] and l_filter_vuln(vuln_table, filter) end else check_vuln = function(vuln_table, fid_table) return vuln_table._FIDS_MATCH[fid_table] end end for ip, vulns_list in pairs(entries.HOSTS) do local host_entries = { ip = ip, vulns = {}, not_vulns = {}, } for _, vuln_table in ipairs(vulns_list) do if check_vuln(vuln_table, fid_table, filter) then debug(5, "vulns.lua: Vulnerability '%s' (host: %s): FOUND", vuln_table.title, vuln_table.host.ip) if (vuln_table.state & STATE.NOT_VULN) == 0 then host_entries.vulns[#host_entries.vulns + 1] = vuln_table else save_not_vulns(host_entries.not_vulns, vuln_table) end end end host_entries.state = next(host_entries.vulns) and STATE.VULN or STATE.NOT_VULN insert(hosts, host_entries) end for _, vuln_table in ipairs(entries.NETWORKS) do if check_vuln(vuln_table, fid_table, filter) then debug(5, "vulns.lua: Vulnerability '%s': FOUND", vuln_table.title) if (vuln_table.state & STATE.NOT_VULN) == 0 then networks.vulns[#networks.vulns + 1] = vuln_table else save_not_vulns(networks.not_vulns, vuln_table) end end end local output = {} local function sort_hosts(a, b) return compare_ip(a.ip, "le", b.ip) end local function sort_ports(a, b) if a.port and b.port then return a.port.number < b.port.number end return false end if next(hosts) then debug(3, "vulns.lua: sorting vulnerability entries for %d host", #hosts) sort(hosts, sort_hosts) for hidx, host in ipairs(hosts) do insert(output, string_format("Vulnerability report for %s: %s", host.ip, STATE_MSG[host.state])) if next(host.vulns) then sort(host.vulns, sort_ports) l_push_vuln_output(output, host.vulns) end if next(host.not_vulns) and SHOW_ALL then sort(host.vulns, sort_ports) if #host.vulns > 0 then insert(output, "") end l_push_vuln_output(output, host.not_vulns, SHOW_ALL) end if #hosts > 1 and hidx ~= #hosts then insert(output, "") end end end if next(networks.vulns) then if next(hosts) then insert(output, "") end insert(output, "VULNERABLE Entries:") l_push_vuln_output(output, networks.vulns) end if next(networks.not_vulns) and SHOW_ALL then if #networks.vulns or next(hosts) then insert(output, "") end insert(output, "NOT VULNERABLE Entries:") l_push_vuln_output(output, networks.not_vulns, SHOW_ALL) end return next(output) and output or nil end --- Add vulnerabilities IDs wrapper local registry_add_ids = function(fid, ...) local t = {...} for _, v in ipairs(t) do local id_type = v l_add_id_type(VULNS.FILTERS_IDS[fid], id_type) end end --- Get vulnerabilities IDs wrapper local registry_get_ids = function(fid) return VULNS.FILTERS_IDS[fid] end --- Lookup for a vulnerability wrapper local registry_lookup_id = function(fid, vuln_id_type, id) if l_lookup_id(VULNS.FILTERS_IDS[fid], vuln_id_type, id) then return true end return false end --- Find vulnerabilities by ID wrapper local registry_find_by_id = function(fid, vuln_id_type, id) if registry_lookup_id(fid, vuln_id_type, id) then debug(5, "vulns.lua: Lookup VULNS.FILTERS_IDS[%d] for vulnerabilities", fid) return l_find_by_id(VULNS.FILTERS_IDS[fid], vuln_id_type, id) end end --- Find vulnerabilities wrapper local registry_find_vulns = function(fid, selection_filter) local fid_table = VULNS.FILTERS_IDS[fid] if fid_table and next(fid_table) then -- Normalize the 'selection_filter' fields local filter = l_normalize_selection_filter(selection_filter) debug(5, "vulns.lua: Lookup VULNS.FILTERS_IDS[%d] for vulnerabilities", fid) return l_find_vulns(VULNS.FILTERS_IDS[fid], VULNS.ENTRIES, filter) end end --- Report vulnerabilities wrapper local registry_make_output = function(fid, selection_filter) local fid_table = VULNS.FILTERS_IDS[fid] if fid_table and next(fid_table) then local filter = l_normalize_selection_filter(selection_filter) debug(5, "vulns.lua: Lookup VULNS.FILTERS_IDS[%d] for vulnerabilities", fid) local output = l_make_output(VULNS.FILTERS_IDS[fid], VULNS.ENTRIES, filter) return stdnse.format_output(true, output) end end --- Save vulnerabilities wrapper local registry_add_vulns = function(script_name, ...) local vulns = {...} if not script_name or not next(vulns) then -- just ignore the entry return false end local count = 0 for _, vuln_table in ipairs(vulns) do if validate_vuln(vuln_table) then normalize_vuln_info(vuln_table) vuln_table.script_name = script_name debug(3, "vulns.lua: *** New Vuln '%s' %sreported by '%s' script ***", vuln_table.title, vuln_table.host and string_format(" host:%s ", vuln_table.host.ip) or "", vuln_table.script_name) if l_add(VULNS, vuln_table) then count = count + 1 end end end return count > 0 and true or false, count end --- Add vulnerability IDs type to the vulnerability database associated -- with the FILTER ID. -- -- This function will create a table for each specified vulnerability ID -- into the vulnerability database to store the associated vulnerability -- entries. -- -- This function takes a FILTER ID as it is returned by -- the vulns.save_reports() function and a variable number -- of vulnerability IDs type as parameters. -- -- Scripts must call vulns.save_reports() function first to -- setup the vulnerability database. -- -- @usage -- vulns.add_ids(fid, 'CVE', 'OSVDB') -- -- @param FILTER ID as it is returned by vulns.save_reports() -- @param IDs A variable number of strings that represent the -- vulnerability IDs type. add_ids = function(fid, ...) -- Define this function in save_reports() end --- Gets the vulnerability database associated with the -- FILTER ID. -- -- This function can be used to check if there are vulnerability entries -- that were saved in the vulnerability database. -- The format of the vulnerability database associated with the -- FILTER ID is specified as Lua comments in this library. -- -- Scripts must call vulns.save_reports() function first to -- setup the vulnerability database. -- -- @usage -- local vulndb = vulns.get_ids(fid) -- if vulndb then -- -- process vulnerability entries -- end -- -- @param FILTER ID as it is returned by vulns.save_reports() -- @return vulndb The internal vulnerability database associated with the -- FILTER ID if there are vulnerability entries that were -- saved, otherwise nil. get_ids = function(fid) -- Define this function in save_reports() end --- Lookup for a vulnerability entry in the vulnerability database -- associated with the FILTER ID. -- -- This function can be used to see if there are any references to the -- specified vulnerability in the database, it will return -- True if so which means that one of the scripts has -- attempted to check this vulnerability. -- -- Scripts must call vulns.save_reports() function first to -- setup the vulnerability database. -- -- @usage -- local status = vulns.lookup(fid, 'CVE', 'CVE-XXXX-XXXX') -- -- @param FILTER ID as it is returned by vulns.save_reports() -- @param vuln_id_type A string representing the vulnerability ID type. -- @param id The vulnerability ID. -- @return True if there are references to this entry in the vulnerability -- database, otherwise False. lookup_id = function(fid, vuln_id_type, id) -- Define this function in save_reports() end --- Adds vulnerability tables into the vulnerability database -- (registry). -- -- This function takes a variable number of vulnerability tables and -- stores them in the vulnerability database if they satisfy the callback -- filters that were registered by the vulns.save_reports() -- function. -- -- Scripts must call vulns.save_reports() function first to -- setup the vulnerability database. -- -- @usage -- local vuln_table = { -- title = "Vulnerability X", -- state = vulns.STATE.VULN, -- ..., -- -- take a look at the vulnerability table example at the beginning. -- } -- local status, ret = vulns.add(SCRIPT_NAME, vuln_table) -- @param script_name The script name. The SCRIPT_NAME -- environment variable will do the job. -- @param vulnerabilities A variable number of vulnerability tables. -- @return True if the vulnerability tables were added, otherwise False. -- @return Number of added vulnerabilities on success. add = function(script_name, ...) -- Define this function in save_reports() end --- Search and return vulnerabilities in a list. -- -- This function will return a list of the vulnerabilities that were -- stored in the vulnerability database associated with the -- FILTER ID that satisfy the selection filter. -- It will take a FILTER ID as it is returned by the -- vulns.save_reports function and a -- selection_filter table as parameters. -- -- Scripts must call vulns.save_reports() function first to -- setup the vulnerability database. -- -- This function is not affected by the vulns.showall script -- argument. The selection_filter is an optional table -- parameter of optional fields which can be used to select which -- vulnerabilities to return, if it is not set then all vulnerability -- entries will be returned. -- -- @usage -- -- All the following fields are optional. -- local selection_filter = { -- state = vulns.STATE.VULN, -- number -- risk_factor = "High", -- string -- hosts_filter = function(vuln_table.host) -- -- Function that returns a boolean -- -- True if it passes the filter, otherwise false. -- end, -- -- vuln_table.host = {ip, targetname, bin_ip} -- ports_filter = function(vuln_table.port) -- -- Function that returns a boolean -- -- True if it passes the filter, otherwise false. -- end, -- -- vuln_table.port = {number, protocol, service -- -- version} -- id_type = 'CVE', -- Vulnerability type ID (string) -- id = 'CVE-XXXX-XXXX', -- CVE id (string) -- } -- local list = vulns.find(fid, selection_filter) -- -- @param FILTER ID as it is returned by vulns.save_reports() -- @param selection An optional table to select which vulnerabilities to -- list. The fields of the selection filter table are: -- state: The vulnerability state. -- risk_factor: The vulnerability risk_factor field, can -- be one of these values: "High", -- "Medium" or "Low". -- hosts_filter: A function to filter the host table of -- the vulnerability table. This function must return -- a boolean, true if it passes the filter otherwise -- false. The host table: -- host = {ip, targetname, bin_ip} -- ports_filter: A function to filter the port table of -- the vulnerability table. This function must return -- a boolean, true if it passes the filter, otherwise -- false. The port table: -- port = {number, protocol, service, version} -- id_type: The vulnerability ID type, (e.g: 'CVE', 'OSVDB' ...) -- id: The vulnerability ID. -- All these fields are optional. -- @return List of vulnerability tables on success, or nil on failures. find = function(fid, selection_filter) -- Define this function in save_reports() end --- Search vulnerability entries by ID and return the results in a list. -- -- This function will return a list of the same vulnerability that affects -- different hosts, each host will have its own vulnerability table. -- -- Scripts must call vulns.save_reports() function first to -- setup the vulnerability database. -- -- @usage -- local list = vulns.find_by_id(fid, 'CVE', 'CVE-XXXX-XXXX') -- -- @param FILTER ID as it is returned by vulns.save_reports() -- @param vuln_id_type A string representing the vulnerability ID type. -- @param id The vulnerability ID. -- @return List of vulnerability tables on success, or nil on failures. find_by_id = function(fid, vuln_id_type, id) -- Define this function in save_reports() end --- Report vulnerabilities. -- -- Format and report all the vulnerabilities that were stored in the -- vulnerability database associated with the FILTER ID for -- user display. -- -- This function takes a FILTER ID as it is returned by the -- vulns.save_reports() function and a -- selection_filter as parameters. -- -- Scripts must call vulns.save_reports() function first to -- activate this function, then they can use it as a tail call to report -- all vulnerabilities that were saved into the registry. Results will be -- sorted by IP addresses and Port numbers. -- -- To show the NOT VULNERABLE entries users must specify -- the vulns.showall script argument. -- -- The selection_filter is an optional table parameter of -- optional fields which can be used to select which vulnerabilities to -- report, if it is not set then all vulnerabilities entries will be -- returned. -- -- @usage -- -- All the following fields are optional. -- local selection_filter = { -- state = vulns.STATE.VULN, -- number -- risk_factor = "High", -- string -- hosts_filter = function(vuln_table.host) -- -- Function that returns a boolean -- -- True if it passes the filter, otherwise false. -- end, -- -- vuln_table.host = {ip, targetname, bin_ip} -- ports_filter = function(vuln_table.port) -- -- Function that returns a boolean -- -- True if it passes the filter, otherwise false. -- end, -- -- vuln_table.port = {number, protocol, service -- -- version} -- id_type = 'CVE', -- Vulnerability type ID (string) -- id = 'CVE-XXXX-XXXX', -- CVE id (string) -- } -- return vulns.make_output(fid, selection_filter) -- -- @param FILTER ID as it is returned by vulns.save_reports() -- @param selection An optional table to select which vulnerabilities to -- report. The fields of the selection filter table are: -- state: The vulnerability state. -- risk_factor: The vulnerability risk_factor field, can -- be one of these values: "High", -- "Medium" or "Low". -- hosts_filter: A function to filter the host table of -- the vulnerability table. This function must return -- a boolean, true if it passes the filter otherwise -- false. The host table: -- host = {ip, targetname, bin_ip} -- ports_filter: A function to filter the port table of -- the vulnerability table. This function must return -- a boolean, true if it passes the filter, otherwise -- false. The port table: -- port = {number, protocol, service, version} -- id_type: The vulnerability ID type, (e.g: 'CVE', 'OSVDB' ...) -- id: The vulnerability ID. -- All these fields are optional. -- @return multiline string on success, or nil on failures. make_output = function(fid, selection_filter) -- Define this function in save_reports() end --- Normalize and format some special vulnerability fields -- -- @param vuln_field The vulnerability field -- @return List The contents of the vuln_field stored in a list. local format_vuln_special_fields = function(vuln_field) local out = {} if vuln_field then if type(vuln_field) == "table" then for _, line in ipairs(vuln_field) do if type(line) == "string" then tadd(out, stringaux.strsplit("\r?\n", line)) else insert(out, line) end end elseif type(vuln_field) == "string" then out = stringaux.strsplit("\r?\n", vuln_field) end end return next(out) and out or nil end --- Inspect and format the vulnerability information. -- -- The result of this function must be checked, it will return a table -- on success, or nil on failures. -- -- @param Table The vulnerability information table. -- @param showall A string if set then show all the vulnerability -- entries including the NOT VULNERABLE ones. -- @return Table The formatted vulnerability information stored in a -- table on success. If one of the mandatory vulnerability fields is -- missing or if the 'showall' parameter is not set and -- the vulnerability state isNOT VULNERABLE then it will -- print a debug message about the vulnerability and return nil. local format_vuln_base = function(vuln_table, showall) if not vuln_table.title or not type(vuln_table.title) == "string" or not vuln_table.state or not STATE_MSG[vuln_table.state] then return nil end if not showall and (vuln_table.state & STATE.NOT_VULN) ~= 0 then debug(2, "vulns.lua: vulnerability '%s'%s: %s.", vuln_table.title, vuln_table.host and string_format(" (host:%s%s)", vuln_table.host.ip, vuln_table.host.targetname and " "..vuln_table.host.targetname or "") or "", STATE_MSG[vuln_table.state]) return nil end local output_table = stdnse.output_table() local out = {} if SHORT_OUTPUT then -- Don't waste time/space inserting anything setmetatable(out, { __newindex = function () return nil end }) end output_table.title = vuln_table.title insert(out, vuln_table.title) output_table.state = STATE_MSG[vuln_table.state] insert(out, string_format(" State: %s", STATE_MSG[vuln_table.state])) if vuln_table.IDS and next(vuln_table.IDS) then local ids_t = {} for id_type, id in pairs(vuln_table.IDS) do -- ignore internal NMAP IDs if id_type ~= 'NMAP_ID' then table.insert(ids_t, string_format("%s:%s", id_type, id)) end end if next(ids_t) then insert(out, string_format(" IDs: %s", table.concat(ids_t, " "))) output_table.ids = ids_t end end -- Show this information only if the program is vulnerable if (vuln_table.state & STATE.NOT_VULN) == 0 then if vuln_table.risk_factor then local risk_str = "" if vuln_table.scores and next(vuln_table.scores) then output_table.scores = vuln_table.scores for score_type, score in pairs(vuln_table.scores) do risk_str = risk_str .. string_format(" %s: %s", score_type, score) end end insert(out, string_format(" Risk factor: %s%s", vuln_table.risk_factor, risk_str)) end if vuln_table.description then local desc = format_vuln_special_fields(vuln_table.description) if desc then for _, line in ipairs(desc) do insert(out, string_format(" %s", line)) end output_table.description = vuln_table.description end end if vuln_table.dates and next(vuln_table.dates) then output_table.dates = vuln_table.dates if vuln_table.dates.disclosure and next(vuln_table.dates.disclosure) then output_table.disclosure = string_format("%s-%s-%s", vuln_table.dates.disclosure.year, vuln_table.dates.disclosure.month, vuln_table.dates.disclosure.day) insert(out, string_format(" Disclosure date: %s-%s-%s", vuln_table.dates.disclosure.year, vuln_table.dates.disclosure.month, vuln_table.dates.disclosure.day)) end end if vuln_table.check_results then output_table.check_results = vuln_table.check_results local check = format_vuln_special_fields(vuln_table.check_results) if check then insert(out, " Check results:") for _, line in ipairs(check) do insert(out, string_format(" %s", line)) end end end if vuln_table.exploit_results then output_table.exploit_results = vuln_table.exploit_results local exploit = format_vuln_special_fields(vuln_table.exploit_results) if exploit then insert(out, " Exploit results:") for _, v in ipairs(vuln_table.exploit_results) do insert(out, string_format(" %s", v)) end end end if vuln_table.extra_info then output_table.extra_info = vuln_table.extra_info local extra = format_vuln_special_fields(vuln_table.extra_info) if extra then insert(out, " Extra information:") for _, v in ipairs(vuln_table.extra_info) do insert(out, string_format(" %s", v)) end end end end if vuln_table.IDS or vuln_table.references then local ref_set = {} -- Show popular references if vuln_table.IDS and next(vuln_table.IDS) then for id_type, id in pairs(vuln_table.IDS) do local id_type = string_upper(id_type) local link = get_popular_link(id_type, id) if link then ref_set[link] = true end end end -- Show other references if vuln_table.references and next(vuln_table.references) then for k, v in pairs(vuln_table.references) do local str = type(k) == "string" and k or v ref_set[str] = true end end if next(ref_set) then insert(out, " References:") local ref_str = {} for link in pairs(ref_set) do insert(out, string_format(" %s", link)) table.insert(ref_str, link) end output_table.refs = ref_str end end if SHORT_OUTPUT then out = {("%s %s %s"):format( vuln_table.host.targetname or vuln_table.host.ip, STATE_MSG[vuln_table.state], vuln_table.IDS.CVE or vuln_table.title )} end return out, output_table end --- Format the vulnerability information and return it in a table. -- -- This function can return nil if the vulnerability mandatory fields -- are missing or if the script argument vulns.showall and -- the 'showall' string parameter were not set and the state -- of the vulnerability is NOT VULNERABLE. -- -- Script writers must check the returned result. -- -- If the vulnerability table contains the host and -- port tables, then the following fields will be shown: -- vuln_table.host.targetname, -- vuln_table.host.ip, vuln_table.port.number and -- vuln_table.port.service -- -- @usage -- local vuln_output = vulns.format_vuln_table(vuln_table) -- if vuln_output then -- -- process the vuln_output table -- end -- -- @param vuln_table The vulnerability information table. -- @param showall A string if set then show all the vulnerabilities -- including the NOT VULNERABLE ones. This optional -- parameter can be used to overwrite the vulns.showall -- script argument value. -- @return Multiline string on success. If one of the mandatory -- vulnerability fields is missing or if the script argument -- vulns.showall and the 'showall' string -- parameter were not specified and the vulnerability state is -- NOT VULNERABLE then it will print a debug message -- about the vulnerability and return nil. format_vuln_table = function(vuln_table, showall) local out = format_vuln_base(vuln_table, showall) if out then -- Show the 'host' and 'port' tables information. if vuln_table.host and type(vuln_table.host) == "table" and vuln_table.host.ip then local run_info = "Target: " if vuln_table.host.targetname then run_info = run_info..vuln_table.host.targetname end run_info = run_info..string_format(" (%s)", vuln_table.host.ip) if vuln_table.port and type(vuln_table.port == "table") and vuln_table.port.number then run_info = run_info..string_format(" Port: %s%s", vuln_table.port.number, vuln_table.port.service and "/"..vuln_table.port.service or "") end insert(out, 1, run_info) end -- Show the list of scripts that reported this vulnerability if vuln_table.scripts and next(vuln_table.scripts) then local script_list = string_format(" Reported by scripts: %s", concat(vuln_table.scripts, " ")) insert(out, script_list) end return out end end --- Format the vulnerability information and return it as a string. -- -- This function can return nil if the vulnerability mandatory fields -- are missing or if the script argument vulns.showall and -- the 'showall' string parameter were not set and the -- state of the vulnerability is NOT VULNERABLE. -- -- Script writers must check the returned result. -- -- If the vulnerability table contains the host and -- port tables, then the following fields will be shown: -- vuln_table.host.targetname, -- vuln_table.host.ip, vuln_table.port.number and -- vuln_table.port.service -- -- @usage -- local vuln_str = vulns.format_vuln(vuln_table, 'showall') -- if vuln_str then -- return vuln_str -- end -- -- @param vuln_table The vulnerability information table. -- @param showall A string if set then show all the vulnerabilities -- including the NOT VULNERABLE ones. This optional -- parameter can be used to overwrite the vulns.showall -- script argument value. -- @return Multiline string on success. If one of the mandatory -- vulnerability fields is missing or if the script argument -- vulns.showall and the 'showall' string -- parameter were not specified and the vulnerability state is -- NOT VULNERABLE then it will print a debug message -- about the vulnerability and return nil. format_vuln = function(vuln_table, showall) local out = format_vuln_table(vuln_table, showall or SHOW_ALL) if out then return concat(out, "\n") end end --- Initializes the vulnerability database and instructs the library -- to save all the vulnerability tables reported by scripts into this -- database (registry). -- -- Usually this function should be called during a prerule -- function so it can instructs the library to save vulnerability -- entries that will be reported by the vulns.Report class -- or by the vulns.add() function. -- -- This function can take an optional callback filter parameter that can -- help the library to decide if it should store the vulnerability table -- in the registry or not. The callback function must return a boolean -- value. If this parameter is not set then all vulnerability tables -- will be saved. -- This function will return a uniq FILTER ID for the scripts -- to be used by the other library functions to reference the appropriate -- vulnerability entries that were saved previously. -- -- @usage -- FID = vulns.save_reports() -- save all vulnerability reports. -- -- -- Save only vulnerabilities with the VULNERABLE state. -- local function save_only_vuln(vuln_table) -- if (vuln_table.state & vulns.STATE.VULN) ~= 0 then -- return true -- end -- return false -- end -- FID = vulns.save_reports(save_only_vuln) -- -- @param filter_callback The callback function to filter vulnerabilities. -- The function will receive a vulnerability table as a parameter in -- order to inspect it, and must return a boolean value. True if the -- the vulnerability table should be saved in the registry, otherwise -- false. This parameter is optional. -- @return Filter ID A uniq ID to be used by the other library functions -- to reference and identify the appropriate vulnerabilities. save_reports = function(filter_callback) if not VULNS then nmap.registry.VULNS = nmap.registry.VULNS or {} VULNS = nmap.registry.VULNS VULNS.ENTRIES = VULNS.ENTRIES or {} VULNS.ENTRIES.HOSTS = VULNS.ENTRIES.HOSTS or {} VULNS.ENTRIES.NETWORKS = VULNS.ENTRIES.NETWORKS or {} VULNS.SHARED = VULNS.SHARED or {} VULNS.SHARED.REFERENCES = VULNS.SHARED.REFERENCES or {} VULNS.FILTERS_FUNCS = VULNS.FILTERS_FUNCS or {} VULNS.FILTERS_IDS = VULNS.FILTERS_IDS or {} -- Enable functions add_ids = registry_add_ids get_ids = registry_get_ids lookup_id = registry_lookup_id add = registry_add_vulns find_by_id = registry_find_by_id find = registry_find_vulns make_output = registry_make_output end local fid = register_filter(VULNS.FILTERS_FUNCS, filter_callback) VULNS.FILTERS_IDS[fid] = {} debug(3, "vulns.lua: New Filter table: VULNS.FILTERS_IDS[%d]", fid) return fid end --- The Report class -- -- Hostrule and Portrule scripts should use this class to store and -- report vulnerabilities. Report = { --- Creates a new Report object -- -- @return report object new = function(self, script_name, host, port) local o = {} setmetatable(o, self) self.__index = self o.entries = {vulns = {}, not_vulns = {}} o.script_name = script_name if host then o.host = {} o.host.ip = host.ip o.host.targetname = host.targetname o.host.bin_ip = host.bin_ip if port then o.port = {} o.port.number = port.number o.port.protocol = port.protocol o.port.service = port.service -- Copy table o.port.version = tcopy(port.version) end end -- TODO: CPE support return o end, --- Registers and associates a callback function with the popular ID -- vulnerability type to construct and return popular links -- automatically. -- -- The callback function takes a vulnerability ID as a parameter -- and must return a link. The library automatically supports three -- different popular IDs: -- CVE: cve.mitre.org -- OSVDB: osvdb.org -- BID: www.securityfocus.com/bid -- -- @usage -- function get_example_link(id) -- return string.format("%s%s", -- "http://example.com/example?name=", id) -- end -- report:add_popular_id('EXM-ID', get_example_link) -- -- @param id_type String representing the vulnerability ID type. -- 'CVE', 'OSVDB' ... -- @param callback A function to construct and return links. -- @return True on success or false if it can not register the callback. add_popular_id = function(self, id_type, callback) return register_popular_id(id_type, callback) end, --- Adds vulnerability tables to the report. -- -- Takes a variable number of vulnerability tables and stores them -- in the internal db of the report so they can be reported later. -- -- @usage -- local vuln_table = { -- title = "Vulnerability X", -- state = vulns.STATE.VULN, -- ..., -- -- take a look at the vulnerability table example at the beginning. -- } -- local status, ret = report:add_vulns(vuln_table) -- @param vulnerabilities A variable number of vulnerability tables. -- @return True if the vulnerability tables were added, otherwise -- False. -- @return Number of added vulnerabilities on success. add_vulns = function(self, ...) local count = 0 for i = 1, select("#", ...) do local vuln_table = select(i, ...) if validate_vuln(vuln_table) then normalize_vuln_info(vuln_table) vuln_table.script_name = self.script_name vuln_table.host = self.host vuln_table.port = self.port if (vuln_table.state & STATE.NOT_VULN) ~= 0 then insert(self.entries.not_vulns, vuln_table) else insert(self.entries.vulns, vuln_table) end add(vuln_table.script_name, vuln_table) count = count + 1 end end return count > 0 and true or false, count end, --- Report vulnerabilities. -- -- Takes a variable number of vulnerability tables and stores them -- in the internal db of the report, then format all the -- vulnerabilities that are in this db for user display. Scripts should -- use this function as a tail call. -- -- To show the NOT VULNERABLE entries users must specify -- the vulns.showall script argument. -- -- @usage -- local vuln_table = { -- title = "Vulnerability X", -- state = vulns.STATE.VULN, -- ..., -- -- take a look at the vulnerability table example at the beginning. -- } -- return report:make_output(vuln_table) -- -- @param vulnerabilities A variable number of vulnerability tables. -- @return multiline string on success, or nil on failures. make_output = function(self, ...) self:add_vulns(...) local vuln_count = #self.entries.vulns local not_vuln_count = #self.entries.not_vulns local output = {} local output_table = stdnse.output_table() local out_t = stdnse.output_table() local output_t2 = stdnse.output_table() -- VULNERABLE: LIKELY_VULN, VULN, DoS, EXPLOIT if vuln_count > 0 then output_table.state = "VULNERABLE" if not SHORT_OUTPUT then insert(output, "VULNERABLE:") end for i, vuln_table in ipairs(self.entries.vulns) do local vuln_out, out_t = format_vuln_base(vuln_table) if type(out_t) == "table" then local ID = vuln_table.IDS.CVE or vuln_table.IDS[next(vuln_table.IDS)] output_t2[ID] = out_t end if vuln_out then output_table.report = concat(vuln_out, "\n") insert(output, concat(vuln_out, "\n")) if vuln_count > 1 and i ~= vuln_count then insert(output, "") -- separate several entries end end end end -- NOT VULNERABLE: NOT_VULN if not_vuln_count > 0 then if SHOW_ALL then if vuln_count > 0 then insert(output, "") end output_table.state = "NOT VULNERABLE" if not SHORT_OUTPUT then insert(output, "NOT VULNERABLE:") end end for i, vuln_table in ipairs(self.entries.not_vulns) do local vuln_out, out_t = format_vuln_base(vuln_table, SHOW_ALL) if type(out_t) == "table" then local ID = vuln_table.IDS.CVE or vuln_table.IDS[next(vuln_table.IDS)] output_t2[ID] = out_t end if vuln_out then output_table.report = concat(vuln_out, "\n") insert(output, concat(vuln_out, "\n")) if not_vuln_count > 1 and i ~= not_vuln_count then insert(output, "") -- separate several entries end end end end if #output==0 and #output_t2==0 then return nil end return output_t2, stdnse.format_output(true, output) end, } return _ENV;