Nmap Development mailing list archives

Re: [NSE script] SSH1 Hostkey


From: Sven Klemm <sven () c3d2 de>
Date: Mon, 01 Sep 2008 14:57:18 +0200

doug () hcsw org wrote:
On Tue, Aug 05, 2008 at 05:57:04PM +0000 or thereabouts, Brandon Enright wrote:
Also, since you seem to be a NSE ninja ;-), you might think about
adding a bubblebabble output option for the fingerprints.  Around here
all the Solaris guys still use that output...

Or, if you're feeling really ambitious, there's the new "visual"
format included with OpenSSH 5.1:


I've added the visual and bubblebabble fingerprint format to the script. You can now specify which format you want with the ssh_hostkey script argument. Possible values are hex,bubble,visual,full and all. If you specify no output format the hex fingerprint will be shown.

Here is an example:

./nmap --script SSH-hostkey -p22 localhost --script-args ssh_hostkey='hex bubble visual'

Starting Nmap 4.68 ( http://nmap.org ) at 2008-09-01 14:53 CEST
Interesting ports on localhost (127.0.0.1):
PORT   STATE SERVICE
22/tcp open  ssh
|  SSH Hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
| 2048 xuvah-degyp-nabus-zegah-hebur-nopig-bubig-difeg-hisym-rumef-cuxex (RSA)
|  +--[ RSA 2048]----+
|  |       .E*+      |
|  |        oo       |
|  |      . o .      |
|  |       O . .     |
|  |      o S o .    |
|  |     = o + .     |
|  |    . * o .      |
|  |     = .         |
|  |    o .          |
|_ +-----------------+

Nmap done: 1 IP address (1 host up) scanned in 0.06 seconds

Cheers,
Sven


--
Sven Klemm
http://cthulhu.c3d2.de/~sven/

--- Shows SSH Hostkeys 
--
-- Shows fingerprint or fingerprint and key depending on verbosity level.
-- Puts the found hostkeys in nmap.registry for other scripts to use them.
-- You can control the output with the ssh_hostkey script argument. Possible
-- values are bubble,visual,full and all.
--
-- 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'
--
--@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
-- |  SSH Hostkey: 2048 xuvah-degyp-nabus-zegah-hebur-nopig-bubig-difeg-hisym-rumef-cuxex (RSA)
-- |_ ssh-rsa 
AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==

require("stdnse")
require("shortport")
require("openssl")
require("bin")
require("bit")
require("base64")
require("hash")

id = "SSH Hostkey"
author = "Sven Klemm <sven () c3d2 de>"
description = "Show SSH Hostkeys"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html";
categories = {"safe","default","intrusive"}

portrule = shortport.port_or_service(22, "ssh")

local key_type_algorithm = {rsa1="RSA1",['ssh-rsa']="RSA",['ssh-dss']="DSA"}

--- format key as hexadecimal fingerprint
local format_key_hex_fingerprint = function( key )
  local fingerprint = hash.md5( key.fp_input )
  local s = fingerprint:sub( 1, 2 )
  for i = 3, #fingerprint, 2 do
    s = s .. ':' .. fingerprint:sub( i, i + 1 )
  end
  return ("%d %s (%s)"):format( key.bits, s, key_type_algorithm[key.key_type] )
end

--- format key as bubblebabble fingerprint
local format_key_bubble_fingerprint = function( key )
  local vowels = {'a','e','i','o','u','y'}
  local consonants = {'b','c','d','f','g','h','k','l','m','n','p','r','s','t','v','z','x'}
  local s = "x"
  local seed = 1
  local fingerprint = hash.sha1( key.fp_input )

  for i=1,#fingerprint+4,4 do
    local in1,in2,idx1,idx2,idx3,idx4,idx5
    if i < #fingerprint or #fingerprint / 2 % 2 ~= 0 then
      in1 = tonumber( fingerprint:sub(i,i+1), 16 )
      idx1 = (bit.band(bit.rshift(in1,6),3) + seed) % 6 + 1
      idx2 = bit.band(bit.rshift(in1,2),15) + 1
      idx3 = (bit.band(in1,3) + math.floor(seed/6)) % 6 + 1
      s = s .. vowels[idx1] .. consonants[idx2] .. vowels[idx3]
      if i < #fingerprint then
        in2 = tonumber( fingerprint:sub(i+2,i+3), 16 )
        idx4 = bit.band(bit.rshift(in2,4),15) + 1
        idx5 = bit.band(in2,15) + 1
        s = s .. consonants[idx4] .. '-' .. consonants[idx5]
        seed = (seed * 5 + in1 * 7 + in2) % 36
      end
    else
      idx1 = seed % 6 + 1
      idx2 = 16 + 1
      idx3 = math.floor(seed/6) + 1
      s = s .. vowels[idx1] .. consonants[idx2] .. vowels[idx3]
    end
  end
  s = s .. 'x'
  return ("%d %s (%s)"):format( key.bits, s, key_type_algorithm[key.key_type] )
end

--- format key as visual fingerprint
-- ported from http://www.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/key.c
local format_key_visual_fingerprint = function( key )
  local i,j,field,characters,input,fieldsize_x,fieldsize_y,s,fingerprint
  fingerprint = hash.md5( key.fp_input )
  fieldsize_x, fieldsize_y = 17, 9
  characters = {' ','.','o','+','=','*','B','O','X','@','%','&','#','/','^','S','E'}
  -- initialize drawing area
  field = {}
  for i=1,fieldsize_x do
    field[i]={}
    for j=1,fieldsize_y do field[i][j]=1 end
  end

  -- we start in the center and mark it
  x, y = math.ceil(fieldsize_x/2), math.ceil(fieldsize_y/2)
  field[x][y] = #characters - 1;

  -- iterate over fingerprint 
  for i=1,#fingerprint,2 do
    input = tonumber( fingerprint:sub(i,i+1), 16 )
    -- each byte conveys four 2-bit move commands 
    for j=1,4 do
      if bit.band( input, 1) == 1 then x = x + 1 else x = x - 1 end
      if bit.band( input, 2) == 2 then y = y + 1 else y = y - 1 end

      x = math.max(x,1); x = math.min(x,fieldsize_x)
      y = math.max(y,1); y = math.min(y,fieldsize_y)

      if field[x][y] < #characters - 2 then
        field[x][y] = field[x][y] + 1
      end
      input = bit.rshift( input, 2 )
    end
  end

  -- mark end point
  field[x][y] = #characters;

  -- build output
  s = ('\n+--[%4s %4d]----+\n'):format( key_type_algorithm[key.key_type], key.bits )
  for i=1,fieldsize_y do
    s = s .. '|'
    for j=1,fieldsize_x do s = s .. characters[ field[j][i] ] end
    s = s .. '|\n'
  end
  s = s .. '+-----------------+\n'
  return s
end

--- format full key for displaying
--@param key table as returned by fetch_host_key
--@return Formated key
local format_key_full = function( key )
  local full_key = ""
  if key.key_type == 'rsa1' then
    full_key = key.exp:to_dec() .. ' ' .. key.mod:to_dec()
  elseif key.key_type == 'ssh-dss' or key.key_type == 'ssh-rsa' then
    full_key = ('%s %s'):format( key.key_type, base64.enc( key.key ) )
  else
    stdnse.print_debug( "Unsupported key type: %s", key.key_type )
  end
  return full_key
end

--- SSH1 functions
local ssh1 = {
  --- fetch SSH1 host key
  --@param host nmap host table
  --@param port nmap port table
  fetch_host_key = function(host, port)
    local socket = nmap.new_socket()
    local catch = function() socket:close() end
    local try = nmap.new_try(catch)

    try(socket:connect(host.ip, port.number))
    -- fetch banner
    try(socket:receive_lines(1))
    -- send our banner
    try(socket:send("SSH-1.5-Nmap-SSH1-Hostkey\r\n"))

    local data, packet_length, padding, offset
    data = try(socket:receive())
    socket:close()
    offset, packet_length = bin.unpack( ">i", data )
    padding = 8 - packet_length % 8
    offset = offset + padding

    if padding + packet_length + 4 == data:len() then
      -- seems to be a proper SSH1 packet
      local msg_code,host_key_bits,exp,mod,length
      offset, msg_code = bin.unpack( ">c", data, offset )
      if msg_code == 2 then -- 2 => SSH_SMSG_PUBLIC_KEY
        -- ignore cookie and server key bits
        offset, _, _ = bin.unpack( ">A8i", data, offset )
        -- skip server key exponent and modulus
        offset, length = bin.unpack( ">S", data, offset )
        offset = offset + math.ceil( length / 8 )
        offset, length = bin.unpack( ">S", data, offset )
        offset = offset + math.ceil( length / 8 )

        offset, host_key_bits = bin.unpack( ">i", data, offset )
        offset, length = bin.unpack( ">S", data, offset )
        offset, exp = bin.unpack( ">A" .. math.ceil( length / 8 ), data, offset )
        exp = openssl.bignum_bin2bn( exp )
        offset, length = bin.unpack( ">S", data, offset )
        offset, mod = bin.unpack( ">A" .. math.ceil( length / 8 ), data, offset )
        mod = openssl.bignum_bin2bn( mod )

        return {exp=exp,mod=mod,bits=host_key_bits,key_type='rsa1',fp_input=mod:to_bin()..exp:to_bin()}
      end
    end
  end
}

--- SSH2 functions
local ssh2
ssh2 = {
  transport = {
    --- pack multiprecision integer for sending
    --@param bn openssl bignum 
    --@return packed multiprecision integer
    pack_mpint = function( bn )
      local bytes, packed
      bytes = bn:num_bytes()
      packed = bn:to_bin()
      if bytes % 8 == 0 then
        bytes = bytes + 1
        packed = string.char(0) .. packed
      end
      return bin.pack( ">IA", bytes, packed )
    end,

    --- build a ssh2 packet
    --@param payload payload of the packet
    --@return packet to send on the wire
    build = function( payload )
      local packet_length, padding_length
      padding_length = 8 - ( (payload:len() + 1 + 4 ) % 8 )
      packet_length = payload:len() + padding_length + 1
      return bin.pack( ">IcAA", packet_length, padding_length, payload, openssl.rand_pseudo_bytes( padding_length ) )
    end,

    --- extract the payload from a received SSH2 packet
    --@param received SSH2 packet
    --@return payload of the SSH2 packet
    payload = function( packet )
      local packet_length, padding_length, payload_length, payload, offset
      offset, packet_length, padding_length = bin.unpack( ">Ic", packet )
      payload_length = packet_length - padding_length - 1
      offset, payload = bin.unpack( ">A" .. payload_length, packet, offset )
      return payload
    end,

    --- build dh_gex_request packet
    dh_gex_request = function( min, opt, max )
      return bin.pack( ">cIII", 34, min, opt, max )
    end,

    --- build kexdh_init packet
    kexdh_init = function( e )
      return bin.pack( ">cA", 30, ssh2.transport.pack_mpint( e ) )
    end,

    --- build kex_init packet
    kex_init = function( cookie, options )
      options = options or {}
      kex_algorithms = "diffie-hellman-group1-sha1"
      host_key_algorithms = options['host_key_algorithms'] or "ssh-dss,ssh-rsa"
      encryption_algorithms = "aes128-cbc,3des-cbc,blowfish-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr"
      mac_algorithms = "hmac-md5,hmac-sha1,hmac-ripemd160"
      compression_algorithms = "none"
      languages = ""

      local payload = bin.pack( ">cAaa", 20, cookie, kex_algorithms, host_key_algorithms )
      payload = payload .. bin.pack( ">aa", encryption_algorithms, encryption_algorithms )
      payload = payload .. bin.pack( ">aa", mac_algorithms, mac_algorithms )
      payload = payload .. bin.pack( ">aa", compression_algorithms, compression_algorithms )
      payload = payload .. bin.pack( ">aa", languages, languages )
      payload = payload .. bin.pack( ">cI", 0, 0 )

      return payload
    end,

    --- parse kexinit package
    -- returns an empty table in case of an error
    parse_kex_init = function( payload ) 
      local _, offset, msg_code, parsed, fields, fieldname
      parsed = {}

      -- check for proper msg code
      offset, msg_code = bin.unpack( ">c", payload )
      if msg_code ~= 20 then return {} end

      offset, parsed.cookie = bin.unpack( ">A16", payload, offset )

      fields = {'kex_algorithms','server_host_key_algorithms',
        'encryption_algorithms_client_to_server','encryption_algorithms_server_to_client',
        'mac_algorithms_client_to_server','mac_algorithms_server_to_client',
        'compression_algorithms_client_to_server','compression_algorithms_server_to_client',
        'languages_client_to_server','languages_server_to_client'}
      for _, fieldname in pairs( fields ) do
        offset, parsed[fieldname] = bin.unpack( ">a", payload, offset )
      end

      return parsed
    end
  },

  --- fetch SSH2 host key
  --@param host nmap host table
  --@param port nmap port table
  --@param key_type key type to fetch
  --@return table containing the key and fingerprint
  fetch_host_key = function( host, port, key_type )
    local socket = nmap.new_socket()
    local catch = function() socket:close() end
    local try = nmap.new_try(catch)
    -- oakley group 2 prime taken from rfc 2409
    local prime = 
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF"

    try(socket:connect(host.ip, port.number))
    -- fetch banner
    try(socket:receive_lines(1))
    -- send our banner
    try(socket:send("SSH-2.0-Nmap-SSH2-Hostkey\r\n"))

    local cookie = openssl.rand_bytes( 16 )
    local packet = ssh2.transport.build( ssh2.transport.kex_init( cookie, {host_key_algorithms=key_type} ) )
    try(socket:send( packet ))

    local kex_init = try(socket:receive_bytes(1))
    kex_init = ssh2.transport.parse_kex_init( ssh2.transport.payload( kex_init ) )

    if not tostring(kex_init.server_host_key_algorithms):find( key_type, 1, true ) then
      -- server does not support host key type
      stdnse.print_debug( 2, "Hostkey type '%s' not supported by server.", key_type )
      return
    end

    local e, g, x, p
    -- e = g^x mod p
    g = openssl.bignum_dec2bn( "2" )
    p = openssl.bignum_hex2bn( prime )
    x = openssl.bignum_pseudo_rand( 1024 )
    e = openssl.bignum_mod_exp( g, x, p )

    packet = ssh2.transport.build( ssh2.transport.kexdh_init( e ) )
    try(socket:send( packet ))

    kexdh_reply = try(socket:receive_bytes(1))
    kexdh_reply = ssh2.transport.payload( kexdh_reply )
    -- check for proper msg code
    if kexdh_reply:byte(1) ~= 31 then
      return
    end

    local _,public_host_key,bits
    _, _, public_host_key = bin.unpack( ">ca", kexdh_reply )

    if key_type == 'ssh-dss' then
      local p
      _, _, p = bin.unpack( ">aa", public_host_key )
      bits = openssl.bignum_bin2bn( p ):num_bits()
    elseif key_type == 'ssh-rsa' then
      local n
      _, _, _, n = bin.unpack( ">aaa", public_host_key )
      bits = openssl.bignum_bin2bn( n ):num_bits()
    else
      stdnse.print_debug( "Unsupported key type: %s", key_type )
    end

    return {key=public_host_key,key_type=key_type,fp_input=public_host_key,bits=bits}
  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[id] = nmap.registry[id] or {}
  nmap.registry[id][host.ip] = nmap.registry[id][host.ip] or {}
  local registry = nmap.registry[id][host.ip]
  table.insert( registry, key )
end

action = function(host, port)
  local output = {}
  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

  for _, key in ipairs( keys ) do
    add_key_to_registry( host, key )
    if format:find( 'hex', 1, true ) or all_formats then
      table.insert( output, format_key_hex_fingerprint( key ) )
    end
    if format:find( 'bubble', 1, true ) or all_formats then
      table.insert( output, format_key_bubble_fingerprint( key ) )
    end
    if format:find( 'visual', 1, true ) or all_formats then
      table.insert( output, format_key_visual_fingerprint( key ) )
    end
    if nmap.verbosity() > 1 or format:find( 'full', 1, true ) or all_formats then
      table.insert( output, format_key_full( key ) )
    end
  end

  if #output > 0 then
    return table.concat( output, '\n' )
  else
    return nil
  end
end


_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org

Current thread: