Nmap Development mailing list archives

Re: [NSE script] SSH Hostkey(s) SSH1 and SSH2


From: Sven Klemm <sven () c3d2 de>
Date: Sat, 16 Aug 2008 17:27:15 +0200

Hi,

I've attached an updated version which fixes a few bugs and adds the found keys to the registry so other scripts can pick them up. I also
added documentation to the script.

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.
--
--@output
-- 22/tcp open  ssh
-- |  SSH Hostkey: rsa1 1024 89:7c:8b:2e:ee:5c:3d:ab:20:bd:d7:b3:a4:5a:a8:80
-- |  ssh-dss 1024 23:23:8c:73:26:22:4a:63:d8:5d:41:eb:86:cf:a0:58
-- |_ ssh-rsa 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11

require("stdnse")
require("shortport")
require("openssl")
require("bin")
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")

--- format fingerprint nicely for displaying
--@param fp fingerprint as hexencoded string
--@return Formated fingerprint
local format_fingerprint = function( fp )
  local s = fp:sub( 1, 2 )
  for i = 3, #fp, 2 do
    s = s .. ':' .. fp:sub( i, i + 1 )
  end
  return s
end

--- format key nicely for displaying depending on key type
--@param key table as returned by fetch_host_key
--@return Formated key
local format_key = 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 = base64.enc( key.key )
  else
    stdnse.print_debug( "Unsupported key type: " .. 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',fingerprint=hash.md5(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
  },

  --- 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.payload( kex_init )
    -- check for proper msg code
    if kex_init:byte(1) ~= 20 then
      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, p, x )

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

    local 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: " .. key_type )
    end

    return {key=public_host_key,key_type=key_type,fingerprint=hash.md5(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,out

  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 )
    out = ("%s %d %s"):format(key.key_type, key.bits, format_fingerprint( key.fingerprint ))
    if nmap.verbosity() > 1 then
      out = ("%s %s"):format( out, format_key( key ) )
    end
    table.insert( output, out )
  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: