From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/ssh-hostkey.nse | 424 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 scripts/ssh-hostkey.nse (limited to 'scripts/ssh-hostkey.nse') diff --git a/scripts/ssh-hostkey.nse b/scripts/ssh-hostkey.nse new file mode 100644 index 0000000..5b50697 --- /dev/null +++ b/scripts/ssh-hostkey.nse @@ -0,0 +1,424 @@ +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 stringaux = require "stringaux" +local table = require "table" +local tableaux = require "tableaux" +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 nmap.registry for use by other scripts. Output can be +controlled with the ssh_hostkey script argument. + +You may also compare the retrieved key with the keys in your known-hosts +file using the known-hosts 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 +-- * "full": The entire key, not just the fingerprint. +-- * "sha256": Base64-encoded SHA256 fingerprint. +-- * "md5": hex-encoded MD5 fingerprint (the default). +-- * "bubble": Bubble Babble output, +-- * "visual": Visual ASCII art representation. +-- * "all": 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: +-- | 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 +-- +-- ssh-dss AAAAB3NzaC1kc3MAAACBANraqxAILTygMTgFu/0snrJck8BkhOpBbN61DAZENgeulLMaJdmNFWZpvhLOJVXSqHt2TCrspbMyvpBH4Fnv7Kb+QBAhXyzeCNnOQ7OVBfqNzkfezoFrQJgOQZSEenP6sCVDqcW2j0KVumnYdPU7FGa8SLfNqA+hUOR2HSSluynFAAAAFQDWKNq4PVbpDA7UExE8JSHnWxv4AwAAAIAWEDdNu5mWfTz52IdxELNjsmn5FvKRmnhPqq/PrTkYqAADL5WYazg7POQZ4yI2nqTq++47ONDK87Wke3qbeIhMrV13Mrgf2JuCUSNqrfEmvzZ2l9x3QyZrj+bJRPRuhwYq8rFup01qaANJ0p4WS/7voNbRhh+l57FkJF+XAJRRTAAAAIEAts1Se+u+hV9ZedXopzfXv1I5ZOSONxZanM10wjM2GRWygCYsHqDM315swBPkzhmB73oBesnhDW3bq0dmW3wvk4gzQZ2E2SHhzVGjlgDpjEahlQ+XGpDZsvqqFGGGx8lvKYFUxBR+UkqMRGmjkHw5sK5ydO1n4R3XJ4FfQFqmoyU= +-- 1024 +-- 18782fd3be7178a38e584b5a83bd60a8 +-- ssh-dss +--
+-- +-- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ== +-- 2048 +-- f058cef4aaa4591c8edd4d0744c82511 +-- ssh-rsa +--
+-- +--
+--
+-- 5 +-- localhost +-- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ== +--
+-- +-- +-- +--@xmloutput +-- +--
+-- 192.168.1.1 +-- 192.168.1.2 +--
+-- +-- 2c2275604bc33b18a2972c967e28dcdd +-- 2048 +-- ssh-rsa +--
+-- +-- +--
+-- 192.168.1.1 +-- 192.168.1.2 +--
+-- +-- 60ac4d51b1cd8509121692761d5d276e +-- 1024 +-- ssh-dss +--
+-- + +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.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 = stringaux.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="", key=k, lnumber = lnumber}) + end + end + end + end + else + if tableaux.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 "md5" + local format_bits = { + md5 = 1, + hex = 1, -- compatibility alias for md5 + sha256 = 1 << 1, + bubble = 1 << 2, + visual = 1 << 3, + full = 1 << 4, + all = 0xffff, + } + local format_mask = 0 + for word in format:gmatch("%w+") do + format_mask = format_mask | (format_bits[word] or 0) + end + + 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_mask & format_bits.md5 ~= 0 then + table.insert( output, ssh1.fingerprint_hex( key.fingerprint, key.algorithm, key.bits ) ) + end + if format_mask & format_bits.sha256 ~= 0 then + table.insert( output, ssh1.fingerprint_base64( key.fp_sha256, "SHA256", key.algorithm, key.bits ) ) + end + if format_mask & format_bits.bubble ~= 0 then + table.insert( output, ssh1.fingerprint_bubblebabble( openssl.sha1(key.fp_input), key.algorithm, key.bits ) ) + end + if format_mask & format_bits.visual ~= 0 then + table.insert( output, ssh1.fingerprint_visual( key.fingerprint, key.algorithm, key.bits ) ) + end + if nmap.verbosity() > 1 or format_mask & format_bits.full ~= 0 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 tableaux.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 + -- cgit v1.2.3