mirror of
https://github.com/nmap/nmap.git
synced 2025-12-11 10:19:03 +00:00
[NSE] Added an enhancement to ssh-hostkey that makes a comparison with your known-hosts file. http://seclists.org/nmap-dev/2013/q3/587
This commit is contained in:
@@ -52,7 +52,7 @@ end
|
|||||||
--- Fetch an SSH-1 host key.
|
--- Fetch an SSH-1 host key.
|
||||||
-- @param host Nmap host table.
|
-- @param host Nmap host table.
|
||||||
-- @param port Nmap port table.
|
-- @param port Nmap port table.
|
||||||
-- @return A table with the following fields: <code>key</code>, <code>exp</code>,
|
-- @return A table with the following fields: <code>exp</code>,
|
||||||
-- <code>mod</code>, <code>bits</code>, <code>key_type</code>,
|
-- <code>mod</code>, <code>bits</code>, <code>key_type</code>,
|
||||||
-- <code>fp_input</code>, <code>full_key</code>, <code>algorithm</code>, and
|
-- <code>fp_input</code>, <code>full_key</code>, <code>algorithm</code>, and
|
||||||
-- <code>fingerprint</code>.
|
-- <code>fingerprint</code>.
|
||||||
@@ -102,7 +102,6 @@ fetch_host_key = function(host, port)
|
|||||||
fp_input = mod:tobin()..exp:tobin()
|
fp_input = mod:tobin()..exp:tobin()
|
||||||
|
|
||||||
return {exp=exp,mod=mod,bits=host_key_bits,key_type='rsa1',fp_input=fp_input,
|
return {exp=exp,mod=mod,bits=host_key_bits,key_type='rsa1',fp_input=fp_input,
|
||||||
key=exp:todec()..' '..mod:todec(),
|
|
||||||
full_key=exp:todec()..' '..mod:todec(),algorithm="RSA1",
|
full_key=exp:todec()..' '..mod:todec(),algorithm="RSA1",
|
||||||
fingerprint=openssl.md5(fp_input)}
|
fingerprint=openssl.md5(fp_input)}
|
||||||
end
|
end
|
||||||
@@ -208,5 +207,53 @@ fingerprint_visual = function( fingerprint, algorithm, bits )
|
|||||||
return s
|
return s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- A lazy parsing function for known_hosts_file.
|
||||||
|
-- The script checks for the known_hosts file in this order:
|
||||||
|
--
|
||||||
|
-- (1) If known_hosts is specified in a script arg, use that. If turned
|
||||||
|
-- off (false), then don't do any known_hosts checking.
|
||||||
|
-- (2) Look at ~/.ssh/config to see if user known_hosts is in an
|
||||||
|
-- alternate location*. Look for "UserKnownHostsFile". If
|
||||||
|
-- UserKnownHostsFile is specified, open that known_hosts.
|
||||||
|
-- (3) Otherwise, open ~/.ssh/known_hosts.
|
||||||
|
parse_known_hosts_file = function(path)
|
||||||
|
common_paths = {}
|
||||||
|
local f, knownhostspath
|
||||||
|
|
||||||
|
if path and io.open(path) then
|
||||||
|
knownhostspath = path
|
||||||
|
end
|
||||||
|
|
||||||
|
if not knownhostspath then
|
||||||
|
for l in io.lines(os.getenv("HOME") .. "/.ssh/config") do
|
||||||
|
if l and string.find(l, "UserKnownHostsFile") then
|
||||||
|
knownhostspath = string.match(l, "UserKnownHostsFile%s(.*)")
|
||||||
|
if string.sub(knownhostspath,1,1)=="~" then
|
||||||
|
knownhostspath = os.getenv("HOME") .. string.sub(knownhostspath, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not knownhostspath then
|
||||||
|
knownhostspath = os.getenv("HOME") .."/.ssh/known_hosts"
|
||||||
|
end
|
||||||
|
|
||||||
|
if not knownhostspath then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
known_host_entries = {}
|
||||||
|
lnumber = 0
|
||||||
|
|
||||||
|
for l in io.lines(knownhostspath) do
|
||||||
|
lnumber = lnumber + 1
|
||||||
|
if l and string.sub(l, 1, 1) ~= "#" then
|
||||||
|
parts = stdnse.strsplit(" ", l)
|
||||||
|
table.insert(known_host_entries, {entry=parts, linenumber=lnumber})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return known_host_entries
|
||||||
|
end
|
||||||
|
|
||||||
return _ENV;
|
return _ENV;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
local base64 = require "base64"
|
||||||
local ipOps = require "ipOps"
|
local ipOps = require "ipOps"
|
||||||
local nmap = require "nmap"
|
local nmap = require "nmap"
|
||||||
local shortport = require "shortport"
|
local shortport = require "shortport"
|
||||||
@@ -14,14 +15,16 @@ 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.
|
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.
|
The script also includes a postrule that check for duplicate hosts using the gathered keys.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
---
|
---
|
||||||
--@usage
|
--@usage
|
||||||
-- nmap host --script SSH-hostkey --script-args ssh_hostkey=full
|
-- 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=all
|
||||||
-- nmap host --script SSH-hostkey --script-args ssh_hostkey='visual bubble'
|
-- nmap host --script ssh-hostkey --script-args ssh_hostkey='visual bubble'
|
||||||
--
|
--
|
||||||
--@args ssh_hostkey Controls the output format of keys. Multiple values may be
|
--@args ssh_hostkey Controls the output format of keys. Multiple values may be
|
||||||
-- given, separated by spaces. Possible values are
|
-- given, separated by spaces. Possible values are
|
||||||
@@ -29,7 +32,13 @@ The script also includes a postrule that check for duplicate hosts using the gat
|
|||||||
-- * <code>"bubble"</code>: Bubble Babble output,
|
-- * <code>"bubble"</code>: Bubble Babble output,
|
||||||
-- * <code>"visual"</code>: Visual ASCII art representation.
|
-- * <code>"visual"</code>: Visual ASCII art representation.
|
||||||
-- * <code>"all"</code>: All of the above.
|
-- * <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
|
--@output
|
||||||
-- 22/tcp open ssh
|
-- 22/tcp open ssh
|
||||||
-- | ssh-hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
|
-- | ssh-hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
|
||||||
@@ -46,9 +55,18 @@ The script also includes a postrule that check for duplicate hosts using the gat
|
|||||||
-- | | = . |
|
-- | | = . |
|
||||||
-- | | o . |
|
-- | | o . |
|
||||||
-- |_ +-----------------+
|
-- |_ +-----------------+
|
||||||
-- 22/tcp open ssh
|
-- 22/tcp open ssh syn-ack
|
||||||
-- | ssh-hostkey: 2048 xuvah-degyp-nabus-zegah-hebur-nopig-bubig-difeg-hisym-rumef-cuxex (RSA)
|
-- | ssh-hostkey: Key comparison with known_hosts file:
|
||||||
-- |_ ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==
|
-- | 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
|
--@output
|
||||||
-- Post-scan script results:
|
-- Post-scan script results:
|
||||||
@@ -98,7 +116,7 @@ The script also includes a postrule that check for duplicate hosts using the gat
|
|||||||
-- </table>
|
-- </table>
|
||||||
-- </table>
|
-- </table>
|
||||||
|
|
||||||
author = "Sven Klemm"
|
author = "Sven Klemm" -- comparing keys from known_hosts file added by Piotr Olma and George Chatzisofroniou
|
||||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||||
categories = {"safe","default","discovery"}
|
categories = {"safe","default","discovery"}
|
||||||
|
|
||||||
@@ -107,6 +125,19 @@ portrule = shortport.port_or_service(22, "ssh")
|
|||||||
|
|
||||||
postrule = function() return (nmap.registry.sshhostkey ~= nil) end
|
postrule = function() return (nmap.registry.sshhostkey ~= nil) end
|
||||||
|
|
||||||
|
--- check for the presence of a value in a table
|
||||||
|
--@param tab the table to search into
|
||||||
|
--@param item the searched value
|
||||||
|
--@return a boolean indicating whether the value has been found or not
|
||||||
|
local function contains(tab, item)
|
||||||
|
for _, val in pairs(tab) do
|
||||||
|
if val == item then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
--- put hostkey in the nmap registry for usage by other scripts
|
--- put hostkey in the nmap registry for usage by other scripts
|
||||||
--@param host nmap host table
|
--@param host nmap host table
|
||||||
--@param key host key table
|
--@param key host key table
|
||||||
@@ -116,6 +147,114 @@ local add_key_to_registry = function( host, key )
|
|||||||
table.insert( nmap.registry.sshhostkey[host.ip], key )
|
table.insert( nmap.registry.sshhostkey[host.ip], key )
|
||||||
end
|
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
|
||||||
|
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.print_debug(2, "%s: found a hash that matches: %s for hostname: %s", SCRIPT_NAME, 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, {lnumber = lnumber})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if contains(possible_host_names, parts[1]) then
|
||||||
|
stdnse.print_debug(2, "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 return_string = "Key comparison with known_hosts file: "
|
||||||
|
if #keys_from_file == 0 then
|
||||||
|
return_string = return_string .. "\n\t" .. "No entry for scanned host found in known_hosts file."
|
||||||
|
else
|
||||||
|
if next(matched_keys) or next(same_key_hashed) or next(same_key) then
|
||||||
|
return_string = return_string .. "\n\tGOOD Matches in known_hosts file: "
|
||||||
|
if next(matched_keys) then
|
||||||
|
for __, gm in ipairs(matched_keys) do
|
||||||
|
return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if next(same_key) then
|
||||||
|
for __, gm in ipairs(same_key) do
|
||||||
|
return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(same_key_hashed) then
|
||||||
|
for __, gm in ipairs(same_key_hashed) do
|
||||||
|
return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": <unknown>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if different_keys ~= 0 then
|
||||||
|
return_string = return_string .. "\n\tWRONG Matches in known_hosts file: "
|
||||||
|
for __, gm in ipairs(different_keys) do
|
||||||
|
return_string = return_string .. "\n\t\tL" .. gm.lnumber .. ": " .. gm.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true, return_string
|
||||||
|
end
|
||||||
|
|
||||||
--- gather host keys
|
--- gather host keys
|
||||||
--@param host nmap host table
|
--@param host nmap host table
|
||||||
--@param port nmap port table of the currently probed port
|
--@param port nmap port table of the currently probed port
|
||||||
@@ -151,7 +290,7 @@ local function portaction(host, port)
|
|||||||
fingerprint=stdnse.tohex(key.fingerprint),
|
fingerprint=stdnse.tohex(key.fingerprint),
|
||||||
type=key.key_type,
|
type=key.key_type,
|
||||||
bits=key.bits,
|
bits=key.bits,
|
||||||
key=key.key,
|
key=base64.enc(key.key),
|
||||||
})
|
})
|
||||||
if format:find( 'hex', 1, true ) or all_formats then
|
if format:find( 'hex', 1, true ) or all_formats then
|
||||||
table.insert( output, ssh1.fingerprint_hex( key.fingerprint, key.algorithm, key.bits ) )
|
table.insert( output, ssh1.fingerprint_hex( key.fingerprint, key.algorithm, key.bits ) )
|
||||||
@@ -170,6 +309,16 @@ local function portaction(host, port)
|
|||||||
end
|
end
|
||||||
end
|
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)
|
||||||
|
local res, status
|
||||||
|
res, status = check_keys(host, keys, known_hosts)
|
||||||
|
table.insert(output, 1, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
if #output > 0 then
|
if #output > 0 then
|
||||||
return output_tab, table.concat( output, '\n' )
|
return output_tab, table.concat( output, '\n' )
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user