summaryrefslogtreecommitdiffstats
path: root/scripts/duplicates.nse
blob: 04543d47e5425969a195551a4219476337ae895f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
local ipOps = require "ipOps"
local nmap = require "nmap"
local ssh1 = require "ssh1"
local stdnse = require "stdnse"
local table = require "table"
local tableaux = require "tableaux"

description = [[
Attempts to discover multihomed systems by analysing and comparing
information collected by other scripts. The information analyzed
currently includes, SSL certificates, SSH host keys, MAC addresses,
and Netbios server names.

In order for the script to be able to analyze the data it has dependencies to
the following scripts: ssl-cert,ssh-hostkey,nbtstat.

One or more of these scripts have to be run in order to allow the duplicates
script to analyze the data.
]]

---
-- @usage
-- sudo nmap -PN -p445,443 --script duplicates,nbstat,ssl-cert <ips>
--
-- @output
-- | duplicates:
-- |   ARP
-- |       MAC: 01:23:45:67:89:0a
-- |           192.168.99.10
-- |           192.168.99.11
-- |   Netbios
-- |       Server Name: WIN2KSRV001
-- |           192.168.0.10
-- |_          192.168.1.10
--


--
-- While the script provides basic duplicate functionality, here are some ideas
-- on improvements.
--
-- Possible additional information sources:
-- * Microsoft SQL Server instance names (Match hostname, version, instance
--   names and ports) - Reliable given several instances
-- * Oracle TNS names - Not very reliable
--
-- Possible enhancements:
-- * Compare hosts across information sources and create a global category
--   in which system duplicates are reported based on more than one source.
-- * Add a reliability index for each information source that indicates how
--   reliable the duplicate match was. This could be an index compared to
--   other information sources as well as an indicator of how good the match
--   was for a particular information source.

author = "Patrik Karlsson"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"safe"}
dependencies = {"ssl-cert", "ssh-hostkey", "nbstat"}


hostrule = function() return true end
postrule = function() return true end

local function processSSLCerts(tab)

  -- Handle SSL-certificates
  -- We create a new table using the SHA1 digest as index
  local ssl_certs = {}
  for host, v in pairs(tab) do
    for port, sha1 in pairs(v) do
      ssl_certs[sha1] = ssl_certs[sha1] or {}
      if ( not tableaux.contains(ssl_certs[sha1], host.ip) ) then
        table.insert(ssl_certs[sha1], host.ip)
      end
    end
  end

  local results = {}
  for sha1, hosts in pairs(ssl_certs) do
    table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
    if ( #hosts > 1 ) then
      table.insert(results, { name = ("Certficate (%s)"):format(sha1), hosts } )
    end
  end

  return results
end

local function processSSHKeys(tab)

  local hostkeys = {}

  -- create a reverse mapping key_fingerprint -> host(s)
  for ip, keys in pairs(tab) do
    for _, key in ipairs(keys) do
      local fp = ssh1.fingerprint_hex(key.fingerprint, key.algorithm, key.bits)
      if not hostkeys[fp] then
        hostkeys[fp] = {}
      end
      -- discard duplicate IPs
      if not tableaux.contains(hostkeys[fp], ip) then
        table.insert(hostkeys[fp], ip)
      end
    end
  end

  -- look for hosts using the same hostkey
  local results = {}
  for key, hosts in pairs(hostkeys) do
    if #hostkeys[key] > 1 then
      table.sort(hostkeys[key], function(a, b) return ipOps.compare_ip(a, "lt", b) end)
      local str = 'Key ' .. key .. ':'
      table.insert( results, { name = str, hostkeys[key] } )
    end
  end

  return results
end

local function processNBStat(tab)

  local results, mac_table, name_table = {}, {}, {}
  for host, v in pairs(tab) do
    mac_table[v.mac] = mac_table[v.mac] or {}
    if ( not(tableaux.contains(mac_table[v.mac], host.ip)) ) then
      table.insert(mac_table[v.mac], host.ip)
    end

    name_table[v.server_name] = name_table[v.server_name] or {}
    if ( not(tableaux.contains(name_table[v.server_name], host.ip)) ) then
      table.insert(name_table[v.server_name], host.ip)
    end
  end

  for mac, hosts in pairs(mac_table) do
    if ( #hosts > 1 ) then
      table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
      table.insert(results, { name = ("MAC: %s"):format(mac), hosts })
    end
  end

  for srvname, hosts in pairs(name_table) do
    if ( #hosts > 1 ) then
      table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
      table.insert(results, { name = ("Server Name: %s"):format(srvname), hosts })
    end
  end

  return results
end

local function processMAC(tab)

  local mac
  local mac_table = {}

  for host in pairs(tab) do
    if ( host.mac_addr ) then
      mac = stdnse.format_mac(host.mac_addr)
      mac_table[mac] = mac_table[mac] or {}
      if ( not(tableaux.contains(mac_table[mac], host.ip)) ) then
        table.insert(mac_table[mac], host.ip)
      end
    end
  end

  local results = {}
  for mac, hosts in pairs(mac_table) do
    if ( #hosts > 1 ) then
      table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
      table.insert(results, { name = ("MAC: %s"):format(mac), hosts })
    end
  end

  return results
end

postaction = function()

  local handlers = {
    ['ssl-cert'] = { func = processSSLCerts, name = "SSL" },
    ['sshhostkey'] = { func = processSSHKeys, name = "SSH" },
    ['nbstat'] = { func = processNBStat, name = "Netbios" },
    ['mac'] = { func = processMAC, name = "ARP" }
  }

  -- temporary re-allocation code for SSH keys
  for k, v in pairs(nmap.registry.sshhostkey or {}) do
    nmap.registry['duplicates'] = nmap.registry['duplicates'] or {}
    nmap.registry['duplicates']['sshhostkey'] = nmap.registry['duplicates']['sshhostkey'] or {}
    nmap.registry['duplicates']['sshhostkey'][k] = v
  end

  if ( not(nmap.registry['duplicates']) ) then
    return
  end

  local results = {}
  for key, handler in pairs(handlers) do
    if ( nmap.registry['duplicates'][key] ) then
      local result_part = handler.func( nmap.registry['duplicates'][key] )
      if ( result_part and #result_part > 0 ) then
        table.insert(results, { name = handler.name, result_part } )
      end
    end
  end

  return stdnse.format_output(true, results)
end

-- we have no real action in here. In essence we move information from the
-- host based registry to the global one, so that our postrule has access to
-- it when we need it.
hostaction = function(host)

  nmap.registry['duplicates'] = nmap.registry['duplicates'] or {}

  for port, cert in pairs(host.registry["ssl-cert"] or {}) do
    nmap.registry['duplicates']['ssl-cert'] = nmap.registry['duplicates']['ssl-cert'] or {}
    nmap.registry['duplicates']['ssl-cert'][host] = nmap.registry['duplicates']['ssl-cert'][host] or {}
    nmap.registry['duplicates']['ssl-cert'][host][port] = stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 })
  end

  if ( host.registry['nbstat'] ) then
    nmap.registry['duplicates']['nbstat'] = nmap.registry['duplicates']['nbstat'] or {}
    nmap.registry['duplicates']['nbstat'][host] = host.registry['nbstat']
  end

  if ( host.mac_addr_src ) then
    nmap.registry['duplicates']['mac'] = nmap.registry['duplicates']['mac'] or {}
    nmap.registry['duplicates']['mac'][host] = true
  end

  return
end

local Actions = {
  hostrule = hostaction,
  postrule = postaction
}

-- execute the action function corresponding to the current rule
action = function(...) return Actions[SCRIPT_TYPE](...) end