1local stdnse = require "stdnse"
2local nmap = require "nmap"
3local lpeg = require "lpeg"
4local U = require "lpeg-utility"
5local table = require "table"
6local tableaux = require "tableaux"
7
8description = [[
9Prints the readable strings from service fingerprints of unknown services.
10
11Nmap's service and application version detection engine sends named probes to
12target services and tries to identify them based on the response. When there is
13no match, Nmap produces a service fingerprint for submission. Sometimes,
14inspecting this fingerprint can give clues as to the identity of the service.
15However, the fingerprint is encoded and wrapped to ensure it doesn't lose data,
16which can make it hard to read.
17
18This script simply unwraps the fingerprint and prints the readable ASCII strings
19it finds below the name of the probe it responded to. The probe names are taken
20from the nmap-service-probes file, not from the response.
21]]
22
23---
24--@usage
25-- nmap -sV --script fingerprint-strings <target>
26--
27--@output
28--| fingerprint-strings:
29--|   DNSStatusRequest, GenericLines, LANDesk-RC, TLSSessionReq:
30--|     bobo
31--|     bobobo
32--|   GetRequest, HTTPOptions, LPDString, NULL, RTSPRequest, giop, oracle-tns:
33--|     bobobo
34--|   Help, LDAPSearchReq, TerminalServer:
35--|     bobobo
36--|     bobobo
37--|   Kerberos, NotesRPC, SIPOptions:
38--|     bobo
39--|   LDAPBindReq:
40--|     bobobo
41--|     bobo
42--|     bobobo
43--|   SSLSessionReq, SSLv23SessionReq:
44--|     bobo
45--|     bobobo
46--|     bobo
47--|   afp:
48--|     bobo
49--|_    bobo
50--
51--@args fingerprint-strings.n The number of printable ASCII characters required to make up a "string" (Default: 4)
52
53author = "Daniel Miller"
54categories = {"version"}
55
56portrule = function (host, port)
57  -- Run for any port that has a service fingerprint indicating an unknown service
58  -- OK to run at any version intensity (e.g. not checking nmap.version_intensity)
59  -- because no traffic is sent and lower intensity is more likely to not match.
60  return port.version and port.version.service_fp
61end
62
63-- Create a table if necessary and append to it
64local function safe_append (t, v)
65  if t then
66    t[#t+1] = v
67  else
68    t = {v}
69  end
70  return t
71end
72
73-- Extract strings of length n or greater.
74local function strings (blob, n)
75  local pat = lpeg.P {
76    (lpeg.V "plain" + lpeg.V "skip")^1,
77    -- Collect long-enough string of printable and space characters
78    plain = (lpeg.R "\x21\x7e" + lpeg.V "space")^n,
79    -- Collapse white space
80    space = (lpeg.S " \t"^1)/" ",
81    -- Skip anything else
82    skip = ((lpeg.R "\x21\x7e"^-(n-1) * (lpeg.R "\0 " + lpeg.R "\x7f\xff")^1)^1)/"\n    ",
83  }
84  return lpeg.match(lpeg.Cs(pat), blob)
85end
86
87action = function(host, port)
88  -- Get the table of probe responses
89  local responses = U.parse_fp(port.version.service_fp)
90  -- extract the probe names
91  local probes = tableaux.keys(responses)
92  -- If there were no probes (WEIRD!) we're done.
93  if #probes <= 0 then
94    return nil
95  end
96
97  local min = stdnse.get_script_args(SCRIPT_NAME .. ".n") or 4
98
99  -- Ensure probes show up in the same order every time
100  table.sort(probes)
101  local invert = {}
102  for i=1, #probes do
103    -- Extract the strings from this probe
104    local plain = strings(responses[probes[i]], min)
105    if plain then
106      -- rearrange some whitespace to look nice
107      plain = plain:gsub("^[\n ]*", "\n    "):gsub("[\n ]+$", "")
108      -- Gather all the probes that had this same set of strings.
109      if plain ~= "" then
110        invert[plain] = safe_append(invert[plain], probes[i])
111      end
112    end
113  end
114
115  -- If none of the probes had sufficiently long strings, then we're done.
116  if not next(invert) then
117    return nil
118  end
119
120  -- Now reverse the representation so that strings are listed under probes
121  local labels = {}
122  local lookup = {}
123  for plain, plist in pairs(invert) do
124    local label = table.concat(plist, ", ")
125    labels[#labels+1] = label
126    lookup[label] = plain
127  end
128  -- Always keep sorted order!
129  table.sort(labels)
130  local out = stdnse.output_table()
131  for i=1, #labels do
132    out[labels[i]] = lookup[labels[i]]
133  end
134  -- XML output will not be very useful because this is intended for users eyes only.
135  return out
136end
137