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 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
|
local ipOps = require "ipOps"
local nmap = require "nmap"
local shortport = require "shortport"
local ssh1 = require "ssh1"
local ssh2 = require "ssh2"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local base64 = require "base64"
local comm = require "comm"
local openssl = stdnse.silent_require "openssl"
description = [[
Shows SSH hostkeys.
Shows the target SSH server's key fingerprint and (with high enough
verbosity level) the public key itself. It records the discovered host keys
in <code>nmap.registry</code> for use by other scripts. Output can be
controlled with the <code>ssh_hostkey</code> script argument.
You may also compare the retrieved key with the keys in your known-hosts
file using the <code>known-hosts</code> argument.
The script also includes a postrule that check for duplicate hosts using the
gathered keys.
]]
---
--@usage
-- nmap host --script ssh-hostkey --script-args ssh_hostkey=full
-- nmap host --script ssh-hostkey --script-args ssh_hostkey=all
-- nmap host --script ssh-hostkey --script-args ssh_hostkey='visual bubble'
--
--@args ssh_hostkey Controls the output format of keys. Multiple values may be
-- given, separated by spaces. Possible values are
-- * <code>"full"</code>: The entire key, not just the fingerprint.
-- * <code>"bubble"</code>: Bubble Babble output,
-- * <code>"visual"</code>: Visual ASCII art representation.
-- * <code>"all"</code>: All of the above.
-- @args ssh-hostkey.known-hosts If this is set, the script will check if the
-- known hosts file contains a key for the host being scanned and will compare
-- it with the keys that have been found by the script. The script will try to
-- detect your known-hosts file but you can, optionally, pass the path of the
-- file to this option.
--
-- @args ssh-hostkey.known-hosts-path. Path to a known_hosts file.
--@output
-- 22/tcp open ssh
-- | ssh-hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
-- 22/tcp open ssh
-- | ssh-hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
-- | +--[ RSA 2048]----+
-- | | .E*+ |
-- | | oo |
-- | | . o . |
-- | | O . . |
-- | | o S o . |
-- | | = o + . |
-- | | . * o . |
-- | | = . |
-- | | o . |
-- |_ +-----------------+
-- 22/tcp open ssh syn-ack
-- | ssh-hostkey: Key comparison with known_hosts file:
-- | GOOD Matches in known_hosts file:
-- | L7: 199.19.117.60
-- | L11: foo
-- | L15: bar
-- | L19: <unknown>
-- | WRONG Matches in known_hosts file:
-- | L3: 199.19.117.60
-- | ssh-hostkey: 2048 xuvah-degyp-nabus-zegah-hebur-nopig-bubig-difeg-hisym-rumef-cuxex (RSA)
-- |_ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==
--
--@output
-- Post-scan script results:
-- | ssh-hostkey: Possible duplicate hosts
-- | Key 1024 60:ac:4d:51:b1:cd:85:09:12:16:92:76:1d:5d:27:6e (DSA) used by:
-- | 192.168.1.1
-- | 192.168.1.2
-- | Key 2048 2c:22:75:60:4b:c3:3b:18:a2:97:2c:96:7e:28:dc:dd (RSA) used by:
-- | 192.168.1.1
-- |_ 192.168.1.2
--
--@xmloutput
-- <table>
-- <elem key="key">ssh-dss AAAAB3NzaC1kc3MAAACBANraqxAILTygMTgFu/0snrJck8BkhOpBbN61DAZENgeulLMaJdmNFWZpvhLOJVXSqHt2TCrspbMyvpBH4Fnv7Kb+QBAhXyzeCNnOQ7OVBfqNzkfezoFrQJgOQZSEenP6sCVDqcW2j0KVumnYdPU7FGa8SLfNqA+hUOR2HSSluynFAAAAFQDWKNq4PVbpDA7UExE8JSHnWxv4AwAAAIAWEDdNu5mWfTz52IdxELNjsmn5FvKRmnhPqq/PrTkYqAADL5WYazg7POQZ4yI2nqTq++47ONDK87Wke3qbeIhMrV13Mrgf2JuCUSNqrfEmvzZ2l9x3QyZrj+bJRPRuhwYq8rFup01qaANJ0p4WS/7voNbRhh+l57FkJF+XAJRRTAAAAIEAts1Se+u+hV9ZedXopzfXv1I5ZOSONxZanM10wjM2GRWygCYsHqDM315swBPkzhmB73oBesnhDW3bq0dmW3wvk4gzQZ2E2SHhzVGjlgDpjEahlQ+XGpDZsvqqFGGGx8lvKYFUxBR+UkqMRGmjkHw5sK5ydO1n4R3XJ4FfQFqmoyU=</elem>
-- <elem key="bits">1024</elem>
-- <elem key="fingerprint">18782fd3be7178a38e584b5a83bd60a8</elem>
-- <elem key="type">ssh-dss</elem>
-- </table>
-- <table>
-- <elem key="key">ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==</elem>
-- <elem key="bits">2048</elem>
-- <elem key="fingerprint">f058cef4aaa4591c8edd4d0744c82511</elem>
-- <elem key="type">ssh-rsa</elem>
-- </table>
-- <table key="Key comparison with known_hosts file">
-- <table key="GOOD Matches in known_hosts file">
-- <table>
-- <elem key="lnumber">5</elem>
-- <elem key="name">localhost</elem>
-- <elem key="key">ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==</elem>
-- </table>
-- </table>
-- </table>
--
--@xmloutput
-- <table>
-- <table key="hosts">
-- <elem>192.168.1.1</elem>
-- <elem>192.168.1.2</elem>
-- </table>
-- <table key="key">
-- <elem key="fingerprint">2c2275604bc33b18a2972c967e28dcdd</elem>
-- <elem key="bits">2048</elem>
-- <elem key="type">ssh-rsa</elem>
-- </table>
-- </table>
-- <table>
-- <table key="hosts">
-- <elem>192.168.1.1</elem>
-- <elem>192.168.1.2</elem>
-- </table>
-- <table key="key">
-- <elem key="fingerprint">60ac4d51b1cd8509121692761d5d276e</elem>
-- <elem key="bits">1024</elem>
-- <elem key="type">ssh-dss</elem>
-- </table>
-- </table>
author = {"Sven Klemm", "Piotr Olma", "George Chatzisofroniou"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"safe","default","discovery"}
portrule = shortport.port_or_service(22, "ssh")
postrule = function() return (nmap.registry.sshhostkey ~= nil) end
--- put hostkey in the nmap registry for usage by other scripts
--@param host nmap host table
--@param key host key table
local add_key_to_registry = function( host, key )
nmap.registry.sshhostkey = nmap.registry.sshhostkey or {}
nmap.registry.sshhostkey[host.ip] = nmap.registry.sshhostkey[host.ip] or {}
table.insert( nmap.registry.sshhostkey[host.ip], key )
end
--- check if there is a key in known_hosts file for the host that's being scanned
--- and if there is, compare the keys
local function check_keys(host, keys, f)
local keys_found = {}
for _,k in ipairs(keys) do
table.insert(keys_found, k.full_key)
end
local keys_from_file = {}
local same_key, same_key_hashed = {}, {}
local hostname = host.name == "" and nil or host.name
local possible_host_names = {hostname or nil, host.ip or nil, (hostname and host.ip) and ("%s,%s"):format(hostname, host.ip) or nil}
for _p, parts in ipairs(f) do
local lnumber = parts.linenumber
parts = parts.entry
local foundhostname = false
if #parts >= 3 then
-- the line might be hashed
if string.match(parts[1], "^|") then
-- split the first part of the line - it contains base64'ed salt and hashed hostname
local parts_hostname = stdnse.strsplit("|", parts[1])
if #parts_hostname == 4 then
-- check if the hash corresponds to the host being scanned
local salt = base64.dec(parts_hostname[3])
for _,name in ipairs(possible_host_names) do
local hash = base64.enc(openssl.hmac("SHA1", salt, name))
if parts_hostname[4] == hash then
stdnse.debug2("found a hash that matches: %s for hostname: %s", hash, name)
foundhostname = true
table.insert(keys_from_file, {name=name, key=("%s %s"):format(parts[2], parts[3]), lnumber=lnumber})
end
end
-- Is the key the same but the hashed hostname isn't?
if not foundhostname then
for _, k in ipairs(keys_found) do
if ("%s %s"):format(parts[2], parts[3]) == k then
table.insert(same_key_hashed, {name="<unknown>", key=k, lnumber = lnumber})
end
end
end
end
else
if stdnse.contains(possible_host_names, parts[1]) then
stdnse.debug2("Found an entry that matches: %s", parts[1])
table.insert(keys_from_file, ("%s %s"):format(parts[2], parts[3]))
else
-- Is the key the same but the clear text hostname isn't?
for _, k in ipairs(keys_found) do
if ("%s %s"):format(parts[2], parts[3]) == k then
table.insert(same_key, {name=parts[1], key=("%s %s"):format(parts[2], parts[3]), lnumber=lnumber})
end
end
end
end
end
end
local matched_keys, different_keys = {}, {}
local matched
-- Compare the keys found for this hostname and update the counts.
for _,k in ipairs(keys_from_file) do
matched = false
for __,l in ipairs(keys_found) do
if l == k.key then
table.insert(matched_keys, k)
matched = true
end
end
if not matched then
table.insert(different_keys, k)
end
end
-- Start making output.
local out
if #keys_from_file == 0 then
out = "No entry for scanned host found in known_hosts file."
else
out = stdnse.output_table()
local match_mt = {
__tostring = function(self)
return string.format("L%d: %s", self.lnumber, self.name)
end
}
local good = {}
for __, gm in ipairs(matched_keys) do
setmetatable(gm, match_mt)
good[#good+1] = gm
end
for __, gm in ipairs(same_key) do
setmetatable(gm, match_mt)
good[#good+1] = gm
end
for __, gm in ipairs(same_key_hashed) do
setmetatable(gm, match_mt)
good[#good+1] = gm
end
if #good > 0 then
out["GOOD Matches in known_hosts file"] = good
end
local wrong = {}
for __, gm in ipairs(different_keys) do
setmetatable(gm, match_mt)
wrong[#wrong+1] = gm
end
if #wrong > 0 then
out["WRONG Matches in known_hosts file"] = wrong
end
end
return out
end
--- gather host keys
--@param host nmap host table
--@param port nmap port table of the currently probed port
local function portaction(host, port)
if port.version.name_confidence < 8 or port.version.name ~= "ssh" then
-- additional check if version scan was not done or if it doesn't think it's SSH.
-- Since the fetch_host_key functions don't indicate what failed, we could
-- waste a lot of time on e.g. tcpwrapped port 22
-- Using opencon instead of get_banner to avoid trying SSL first in some cases
local status, banner = comm.opencon(host, port, nil, {recv_before=true})
if not string.match(banner, "^SSH") then
stdnse.debug1("Service does not appear to be SSH: quitting.")
return nil
end
end
local output_tab = {}
local keys = {}
local key
local format = nmap.registry.args.ssh_hostkey or "hex"
local all_formats = format:find( 'all', 1, true )
key = ssh1.fetch_host_key( host, port )
if key then table.insert( keys, key ) end
key = ssh2.fetch_host_key( host, port, "ssh-dss" )
if key then table.insert( keys, key ) end
key = ssh2.fetch_host_key( host, port, "ssh-rsa" )
if key then table.insert( keys, key ) end
key = ssh2.fetch_host_key( host, port, "ecdsa-sha2-nistp256" )
if key then table.insert( keys, key ) end
key = ssh2.fetch_host_key( host, port, "ecdsa-sha2-nistp384" )
if key then table.insert( keys, key ) end
key = ssh2.fetch_host_key( host, port, "ecdsa-sha2-nistp521" )
if key then table.insert( keys, key ) end
key = ssh2.fetch_host_key( host, port, "ssh-ed25519" )
if key then table.insert( keys, key ) end
if #keys == 0 then
return nil
end
for _, key in ipairs( keys ) do
add_key_to_registry( host, key )
local output = {}
local out = {
fingerprint=stdnse.tohex(key.fingerprint),
type=key.key_type,
bits=key.bits,
key=key.key,
}
if format:find( 'hex', 1, true ) or all_formats then
table.insert( output, ssh1.fingerprint_hex( key.fingerprint, key.algorithm, key.bits ) )
end
if format:find( 'bubble', 1, true ) or all_formats then
table.insert( output, ssh1.fingerprint_bubblebabble( openssl.sha1(key.fp_input), key.algorithm, key.bits ) )
end
if format:find( 'visual', 1, true ) or all_formats then
table.insert( output, ssh1.fingerprint_visual( key.fingerprint, key.algorithm, key.bits ) )
end
if nmap.verbosity() > 1 or format:find( 'full', 1, true ) or all_formats then
table.insert( output, key.full_key )
end
setmetatable(out, {
__tostring = function(self)
return table.concat(output, "\n")
end
})
table.insert(output_tab, out)
end
-- if a known_hosts file was given, then check if it contains a key for the host being scanned
local known_hosts = stdnse.get_script_args("ssh-hostkey.known-hosts") or false
if known_hosts then
known_hosts = ssh1.parse_known_hosts_file(known_hosts)
output_tab["Key comparison with known_hosts file"] = check_keys(
host, keys, known_hosts)
end
return output_tab
end
--- iterate over the list of gathered keys and look for duplicate hosts (sharing the same hostkeys)
local function postaction()
local hostkeys = {}
local output = {}
local output_tab = {}
local revmap = {}
-- create a reverse mapping key_fingerprint -> host(s)
for ip, keys in pairs(nmap.registry.sshhostkey) 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] = {}
revmap[fp] = {
fingerprint=stdnse.tohex(key.fingerprint,{separator=":"}),
type=key.key_type,
bits=key.bits
}
end
-- discard duplicate IPs
if not stdnse.contains(hostkeys[fp], ip) then
table.insert(hostkeys[fp], ip)
end
end
end
-- look for hosts using the same hostkey
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 .. ' used by:'}
local tab = {key=revmap[key], hosts={}}
for _, host in ipairs(hostkeys[key]) do
str[#str+1] = host
table.insert(tab.hosts, host)
end
table.insert(output, table.concat(str, "\n "))
table.insert(output_tab, tab)
end
end
if #output > 0 then
return output_tab, 'Possible duplicate hosts\n' .. table.concat(output, '\n')
end
end
local ActionsTable = {
-- portrule: retrieve ssh hostkey
portrule = portaction,
-- postrule: look for duplicate hosts (same hostkey)
postrule = postaction
}
-- execute the action function corresponding to the current rule
action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
|