From 29e7e274472d137477dff51115bccd00ca84426a Mon Sep 17 00:00:00 2001 From: dmiller Date: Sat, 3 Dec 2016 06:08:11 +0000 Subject: [PATCH] New script fingerprint-strings --- CHANGELOG | 3 + nselib/lpeg-utility.lua | 15 ++++ scripts/fingerprint-strings.nse | 120 ++++++++++++++++++++++++++++++++ scripts/script.db | 1 + 4 files changed, 139 insertions(+) create mode 100644 scripts/fingerprint-strings.nse diff --git a/CHANGELOG b/CHANGELOG index d6b4aa4f3..a9877afd4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Script fingerprint-strings will print the ASCII strings it finds in the + service fingerprints that Nmap shows for unidentified services. [Daniel Miller] + o [GH#505] Updated Russian translation of Zenmap by Alexander Kozlov. o [NSE][GH#588] Fix a crash in smb.lua when using smb-ls due to a diff --git a/nselib/lpeg-utility.lua b/nselib/lpeg-utility.lua index 7a4b5f33f..88acfd74b 100644 --- a/nselib/lpeg-utility.lua +++ b/nselib/lpeg-utility.lua @@ -12,6 +12,7 @@ local stdnse = require "stdnse" local pairs = pairs local string = require "string" local tonumber = tonumber +local rawset = rawset _ENV = {} @@ -142,12 +143,26 @@ do } --- Turn the service fingerprint reply to a probe into a binary blob + --@param fp the port.version.service_fp provided by the NSE API. + --@param probe the probe name to match, e.g. GetRequest, TLSSessionReq, etc. + --@return the raw probe response received to that probe, or nil if there was no response. function get_response (fp, probe) fp = string.gsub(fp, "\nSF:", "") local i, e = string.find(fp, string.format("%s,%%x+,", probe)) if i == nil then return nil end return unescape:match(getquote:match(fp, e+1)) end + + local svfp_parser = lpeg.P ({ + anywhere("%r(") * lpeg.Cf(lpeg.Ct("") * (lpeg.V "probematch" * lpeg.P(")%r(")^-1)^1, rawset), + probematch = lpeg.Cg(lpeg.C((lpeg.P(1) - ",")^1) * "," * (lpeg.R("09") + lpeg.R("AF"))^1 * "," * lpeg.Cs(getquote/function(q) return unescape:match(q) end)), + }) + --- Get the service fingerprint reply to a probe into a binary blob + --@param fp the port.version.service_fp provided by the NSE API. + function parse_fp (fp) + fp = string.gsub(fp, "\nSF:", "") + return svfp_parser:match(fp) + end end return _ENV diff --git a/scripts/fingerprint-strings.nse b/scripts/fingerprint-strings.nse new file mode 100644 index 000000000..50e11e6ae --- /dev/null +++ b/scripts/fingerprint-strings.nse @@ -0,0 +1,120 @@ +local stdnse = require "stdnse" +local nmap = require "nmap" +local lpeg = require "lpeg" +local U = require "lpeg-utility" +local table = require "table" + +description = [[ +Prints the readable strings from service fingerprints of unknown services. +]] + +--- +--@output +--| fingerprint-strings: +--| DNSStatusRequest, GenericLines, LANDesk-RC, TLSSessionReq: +--| bobo +--| bobobo +--| GetRequest, HTTPOptions, LPDString, NULL, RTSPRequest, giop, oracle-tns: +--| bobobo +--| Help, LDAPSearchReq, TerminalServer: +--| bobobo +--| bobobo +--| Kerberos, NotesRPC, SIPOptions: +--| bobo +--| LDAPBindReq: +--| bobobo +--| bobo +--| bobobo +--| SSLSessionReq, SSLv23SessionReq: +--| bobo +--| bobobo +--| bobo +--| afp: +--| bobo +--|_ bobo +-- +--@args fingerprint-strings.n The number of printable ASCII characters required to make up a "string" (Default: 4) + +author = "Daniel Miller" +categories = {"version"} + +portrule = function (host, port) + -- Run for any port that has a service fingerprint indicating an unknown service + return port.version and port.version.service_fp +end + +-- Create a table if necessary and append to it +local function safe_append (t, v) + if t then + t[#t+1] = v + else + t = {v} + end + return t +end + +-- Extract strings of length n or greater. +local function strings (blob, n) + local pat = lpeg.P { + (lpeg.V "plain" + lpeg.V "skip")^1, + -- Collect long-enough string of printable and space characters + plain = (lpeg.R "\x21\x7e" + lpeg.V "space")^n, + -- Collapse white space + space = (lpeg.S " \t"^1)/" ", + -- Skip anything else + skip = ((lpeg.R "\x21\x7e"^-(n-1) * (lpeg.R "\0 " + lpeg.R "\x7f\xff")^1)^1)/"\n ", + } + return lpeg.match(lpeg.Cs(pat), blob) +end + +action = function(host, port) + -- Get the table of probe responses + local responses = U.parse_fp(port.version.service_fp) + -- extract the probe names + local probes = stdnse.keys(responses) + -- If there were no probes (WEIRD!) we're done. + if #probes <= 0 then + return nil + end + + local min = stdnse.get_script_args(SCRIPT_NAME .. ".n") or 4 + + -- Ensure probes show up in the same order every time + table.sort(probes) + local invert = {} + for i=1, #probes do + -- Extract the strings from this probe + local plain = strings(responses[probes[i]], min) + if plain then + stdnse.debug1("%s:>>>%s<<<", probes[i], plain) + -- rearrange some whitespace to look nice + plain = plain:gsub("^[\n ]*", "\n "):gsub("[\n ]+$", "") + -- Gather all the probes that had this same set of strings. + if plain ~= "" then + invert[plain] = safe_append(invert[plain], probes[i]) + end + end + end + + -- If none of the probes had sufficiently long strings, then we're done. + if not next(invert) then + return nil + end + + -- Now reverse the representation so that strings are listed under probes + local labels = {} + local lookup = {} + for plain, plist in pairs(invert) do + local label = table.concat(plist, ", ") + labels[#labels+1] = label + lookup[label] = plain + end + -- Always keep sorted order! + table.sort(labels) + local out = stdnse.output_table() + for i=1, #labels do + out[labels[i]] = lookup[labels[i]] + end + -- XML output will not be very useful because this is intended for users eyes only. + return out +end diff --git a/scripts/script.db b/scripts/script.db index d8941a1f1..7bede6a82 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -111,6 +111,7 @@ Entry { filename = "epmd-info.nse", categories = { "default", "discovery", "safe Entry { filename = "eppc-enum-processes.nse", categories = { "discovery", "safe", } } Entry { filename = "fcrdns.nse", categories = { "discovery", "safe", } } Entry { filename = "finger.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "fingerprint-strings.nse", categories = { "version", } } Entry { filename = "firewalk.nse", categories = { "discovery", "safe", } } Entry { filename = "firewall-bypass.nse", categories = { "intrusive", "vuln", } } Entry { filename = "flume-master-info.nse", categories = { "default", "discovery", "safe", } }