From 205e7ab28bc2d223aad448fb9f8db9c415b815f0 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 6 Sep 2008 02:47:46 +0000 Subject: [PATCH] Add the latest ASN.nse script. This version uses the new Nmap-specific query servers, groups output intelligently, and supports IPv6. See sample output at http://seclists.org/nmap-dev/2008/q3/0675.html. --- CHANGELOG | 2 +- scripts/ASN.nse | 847 +++++++++++++++++++++++++++++++++++++++------- scripts/script.db | 146 ++++---- 3 files changed, 796 insertions(+), 199 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7031ca3f6..1650d589c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -324,7 +324,7 @@ o The loading of the nmap-services file has been made much o Added a script (ASN.nse) which uses Team Cymru's DNS interface to determine the routing AS numbers of scanned IP addresses. They even set up a special domain just for Nmap queries. The script is still - experimental and non-default. [Michael] + experimental and non-default. [Jah, Michael] o The shtool build helper script has been updated to version 2.0.8. An older version of shutil caused installation to fail when the locale diff --git a/scripts/ASN.nse b/scripts/ASN.nse index f1795b173..7503f230e 100644 --- a/scripts/ASN.nse +++ b/scripts/ASN.nse @@ -1,158 +1,755 @@ ---[[ +id = "AS Numbers" +description = [[ +This script performs IP address to Autonomous System Numbers (ASN) lookups. It +sends DNS TXT queries to a DNS server which in turn queries a third party service +provided by Team Cymru (team-cymru.org) using an in-addr.arpa style zone set-up +especially for use by Nmap. +\n +The respnses to these queries contain both Origin and Peer ASNs and their descriptions, +displayed along with the BG Prefix and Country Code. +\n +The script caches results to reduce the number of queries and should perform a single +query for all scanned targets in a BG Prefix present in Team Cymru's database. +\n\n +Please be aware that any targets for which a query is performed will be revealed +to a Team Cymru. +]] -Query Autonomous System Numbers Based on IP -------------------------------------------- -Uses a 3rd party service provided by team-cymru.org to -find the autonomous system numbers of a specific IP. -This scan is performed on all hosts in a portscan but -will only output a result once for every netblock. -Be aware that because this scan uses a 3rd party -service it may result in a loss of privacy, all hosts -that you scan with this script will be sent to team- -cymru.org. ---]] +--- +-- @usage +-- nmap --script asn +-- +-- @args dns Optional recursive nameserver. e.g. --script-args dns=192.168.1.1 +-- +-- @output +-- Host script results: +-- \n| AS Numbers: +-- \n| BGP: 64.13.128.0/21 | Country: US +-- \n| Origin AS: 10565 SVCOLO-AS - Silicon Valley Colocation, Inc. +-- \n| Peer AS: 3561 6461 +-- \n| BGP: 64.13.128.0/18 | Country: US +-- \n| Origin AS: 10565 SVCOLO-AS - Silicon Valley Colocation, Inc. +-- \n|_ Peer AS: 174 2914 6461 +-- -id = "ASN" -description = "This script discovers the autonomous system numbers of the \ -netblock ranges that you scanned. It will return other information as well \ -such as a country code, and BGP prefix. \ -Caution: This script access a 3rd party database provided by team-cymru.com \ -to discover this information. Using this script will expose the hosts that \ -you scan to team-cymru.com in order to get the results. \ -Usage: nmap --script asn --script-args dns=" -author = "Jah, Michael" +author = "jah, Michael" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery"} +runlevel = 1 -require "comm" -require "ipOps" -hostrule = function( host ) - return true -end -if not nmap.registry.asn then - nmap.registry.asn = {} - nmap.registry.asn.cache = {} -end +local dns = require "dns" +local comm = require "comm" + local mutex = nmap.mutex( id ) +if not nmap.registry.asn then + nmap.registry.asn = {} + nmap.registry.asn.cache = {} + nmap.registry.asn.descr = {} +end + + + +--- +-- This script will run for any non-private IP address. + +hostrule = function( host ) + return not isPrivate( host.ip ) +end + + + +--- +-- Cached results are checked before sending a query for the target and extracting the +-- relevent information from the response. Mutual exclusion is used so that results can be +-- cached and so a single thread will be active at any time. +-- @param host Host Table. +-- @return Formatted answers or nil on NXDOMAIN/errors. action = function( host ) - -- get args or die - local dns_server - if nmap.registry.args.dns then - dns_server = nmap.registry.args.dns - else - return - end + mutex "lock" - -- wait - mutex "lock" + -- check for cached data + local in_cache, records + local combined_records = {} - -- check for cached data - for _, cache in ipairs( nmap.registry.asn.cache ) do - if ip_in_net( host.ip, cache.bgp) then - mutex "done" - return " \nBGP Prefix: " .. cache.bgp .. "\nAS number: " .. cache.asn .. "\nCountry Code: " .. cache.co_id + in_cache, records = check_cache( host.ip ) + records = records or {} + + if not in_cache then + + --- + -- @class table + -- @name cymru + -- Team Cymru zones for rDNS like queries. The zones are as follows: + -- \n nmap.asn.cymru.com for IPv4 to Origin AS lookup. + -- \n peer-nmap.asn.cymru.com for IPv4 to Peer AS lookup. + -- \n nmap6.asn.cymru.com for IPv6 to Origin AS lookup. + local cymru = { [4] = { ["Origin"] = ".nmap.asn.cymru.com", ["Peer"] = ".peer-nmap.asn.cymru.com" }, + [6] = { ["Origin"] = ".nmap6.asn.cymru.com" } + } + local zone_repl, IPv = "%.in%-addr%.arpa", 4 + if host.ip:match( ":" ) then + zone_repl, IPv = "%.ip6%.arpa", 6 + end + + -- name to query for + local dname = reverse( host.ip ) + + -- perform queries for each applicable zone + for asn_type, zone in pairs( cymru[IPv] ) do + -- replace arpa with cymru zone + local temp = dname + dname = dname:gsub( zone_repl, zone ) + -- send query and recognise and organise fields from response + local success, retval = result_recog( ip_to_asn( dname ), asn_type, records ) + -- if success then records = retval end + -- un-replace arpa zone + dname = temp + end + + -- combine records into unique BGP + for _, record in ipairs( records ) do + if not combined_records[record.cache_bgp] then + combined_records[record.cache_bgp] = record + elseif combined_records[record.cache_bgp].asn_type ~= record.asn_type then + -- origin before peer. + if record.asn_type == "Origin" then + combined_records[record.cache_bgp].asn = { unpack( record.asn ), unpack( combined_records[record.cache_bgp].asn ) } + else + combined_records[record.cache_bgp].asn = { unpack( combined_records[record.cache_bgp].asn ), unpack( record.asn ) } + end + end + end + + -- cache combined records + for _, rec in pairs( combined_records ) do + table.insert( nmap.registry.asn.cache, rec ) + end + + else -- records were in the cache + combined_records = records + end + + -- format each combined_record for output + local output = {} + for _, rec in pairs( combined_records ) do + local r = {} + if rec.bgp then r[#r+1] = rec.bgp end + if rec.co then r[#r+1] = rec.co end + output[#output+1] = ( "%s\n %s" ):format( table.concat( r, " | " ), table.concat( rec.asn, "\n " ) ) + end + + mutex "done" + + if type( output ) ~= "table" or #output == 0 then return nil end + -- sort BGP asc. + table.sort( output, function(a,b) return (get_prefix_length(a) or 0) > (get_prefix_length(b) or 0) end ) + + -- return combined and formatted answers + return (" \n%s"):format( table.concat( output, "\n" ) ) + +end + + +--- +-- Checks whether the target IP address is within any BGP prefixes for which a query has +-- already been performed and returns any applicable answers. +-- @param ip String representing the target IP address. +-- @return Boolean True if there are cached answers for the supplied target, otherwise +-- false. +-- @return Table containing a string for each answer or nil if there are none. + +function check_cache( ip ) + local ret = {} + for _, cache_entry in ipairs( nmap.registry.asn.cache ) do + if ip_in_range( ip, cache_entry.cache_bgp ) then + ret[#ret+1] = cache_entry + end + end + if #ret > 0 then return true, ret end + return false, nil +end + + +--- +-- Extracts fields from the supplied DNS answer sections. +-- @param answers Table containing string DNS answers. +-- @param asn_type String denoting whether the query is for Origin or Peer ASN. +-- @param recs Table of existing recognised answers to which to add (ref to actions() records{}. +-- @return Boolean true if successful otherwise false. + +function result_recog( answers, asn_type, recs ) + + if type( answers ) ~= "table" or #answers == 0 then return false end + + for _, answer in ipairs( answers ) do + local t = {} + -- break the answer up into fields and strip whitespace + local fields = { answer:match( ("([^|]*)|" ):rep(3) ) } + for i, field in ipairs( fields ) do + fields[i] = field:gsub( "^%s*(.-)%s*$", "%1" ) + end + -- assign fields with labels to table + t.cache_bgp = fields[2] + t.asn_type = asn_type + t.asn = { asn_type .. " AS: " .. fields[1] } + t.bgp = "BGP: " .. fields[2] + if fields[3] ~= "" then t.co = "Country: " .. fields[3] end + recs[#recs+1] = t + -- lookup AS descriptions for Origin AS numbers + local asn_descr = nmap.registry.asn.descr + local u = {} + if asn_type == "Origin" then + for num in fields[1]:gmatch( "%d+" ) do + if not asn_descr[num] then + asn_descr[num] = asn_description( num ) + end + u[#u+1] = ( "%s AS: %s%s%s" ):format( asn_type, num, ( asn_descr[num] ~= "" and " - " ) or "", asn_descr[num] ) + end + t.asn = { table.concat(u, "\n " ) } + end + end + + return true + +end + + +--- +-- Performs an IP address to ASN lookup. See http://www.team-cymru.org/Services/ip-to-asn.html#dns +-- @param query String - PTR like DNS query. +-- @return Table containing string answers or Boolean false. + +function ip_to_asn( query ) + + if type( query ) ~= "string" or query == "" then + return nil + end + + -- dns query options + local options = {} + options.dtype = "TXT" + options.retAll = true + if type( nmap.registry.args.dns ) == "string" and nmap.registry.args.dns ~= "" then + options.host = nmap.registry.args.dns + options.port = 53 + end + + local decoded_response, other_response = dns.query( query, options) + + return decoded_response + +end + + +--- +-- Performs an AS Number to AS Description lookup. +-- @param asn String AS Number +-- @return String Description or "" + +function asn_description( asn ) + + if type( asn ) ~= "string" or asn == "" then + return "" + end + + -- dns query options + local options = {} + options.dtype = "TXT" + if type( nmap.registry.args.dns ) == "string" and nmap.registry.args.dns ~= "" then + options.host = nmap.registry.args.dns + options.port = 53 + end + + -- send query + local query = ( "AS%s.asn.cymru.com" ):format( asn ) + local decoded_response, other_response = dns.query( query, options) + if type( decoded_response ) ~= "string" then + return "" + end + + return decoded_response:match( "|%s*([^|$]+)%s*$" ) or "" + +end + + + +-- *** UTILITY FUNCTIONS *** +-- remove when these functions are available in libraries + + +--- +-- Formats IP for reverse lookup. +-- @param ip String IP address. +-- @return "Domain" style representation of IP as subdomain of in-addr.arpa or ip6.arpa + +function reverse(ip) + ip = expand_ip(ip) + if type(ip) ~= "string" then return nil end + local delim = "%." + local arpa = ".in-addr.arpa" + if ip:match(":") then + delim = ":" + arpa = ".ip6.arpa" end - end + local ipParts = stdnse.strsplit(delim, ip) + if #ipParts == 8 then + -- padding + local mask = "0000" + for i, part in ipairs(ipParts) do + ipParts[i] = mask:sub(1, string.len(mask) - string.len(part)) .. part + end + -- 32 parts from 8 + local temp = {} + for i, hdt in ipairs(ipParts) do + for part in hdt:gmatch("%x") do + temp[#temp+1] = part + end + end + ipParts = temp + end + local ipReverse = {} + for i = #ipParts, 1, -1 do + table.insert(ipReverse, ipParts[i]) + end + return table.concat(ipReverse, ".") .. arpa +end - -- format data - local t = {} - t[4], t[3], t[2], t[1] = host.ip:match( "([^\.]*)\.([^\.]*)\.([^\.]*)\.([^\.]*)" ) - local tsoh = labels( t ) - local z = { "nmap", "asn", "cymru", "com" } - local zone = labels( z ) - local t_id = string.char( tonumber( t[2] ), tonumber( t[3] ) ) -- not at all random... - local dns_std = string.char( 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ) - local null_char = string.char( 0x00 ) - local qtype = string.char( 0x00, 0x10 ) - local qclass = string.char( 0x00, 0x01 ) - local query = tsoh .. zone .. null_char .. qtype .. qclass +--- +-- Checks to see if the supplied IP address is part of the following non-internet-routable address spaces: +-- IPv4 Loopback (RFC3330), +-- IPv4 Private Use (RFC1918), +-- IPv4 Link Local (RFC3330), +-- IPv6 Unspecified and Loopback (RFC3513), +-- IPv6 Unique Local Unicast (RFC4193), +-- IPv6 Link Local Unicast (RFC4291) +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @usage local is_private = isPrivate( "192.168.1.1" ) +-- @return Boolean True or False (or nil in case of an error). +-- @return Nil (or String error message in case of an error). - local data = t_id .. dns_std .. query +isPrivate = function( ip ) - -- send data - local options = {} - options.proto = "udp" - options.lines = 1 - options.timeout = 1000 - local status, result = comm.exchange( dns_server, 53, data, options ) - if not status then - mutex "done" - return - end + ip, err = expand_ip( ip ) + if err then return nil, err end - -- read result - this method is tenuous!! - local _, offset = string.find( result, query ) - local line = string.sub( result, offset + 13 ) - fields = {line:match( ("([^|]*)|"):rep(3) )} + local ipv4_private = { "10/8", "127/8", "169.254/16", "172.15/12", "192.168/16" } + local ipv6_private = { "::/127", "FC00::/7", "FE80::/10" } + local t, is_private = {} + if ip:match( ":" ) then + t = ipv6_private + else + t = ipv4_private + end - -- cache result - local blob = {} - blob.bgp = fields[2]:gsub( "^%s*(.-)%s*$", "%1" ) - blob.asn = fields[1]:gsub( "^%s*[^0](.-)%s*$", "%1" ) - blob.co_id = fields[3]:gsub( "^%s*(.-)%s*$", "%1" ) - table.insert( nmap.registry.asn.cache, blob ) - mutex "done" - - -- return result - return " \nBGP Prefix: " .. blob.bgp .. "\nAS number: " .. blob.asn .. "\nCountry Code: " .. blob.co_id + for _, range in ipairs( t ) do + is_private, err = ip_in_range( ip, range ) + -- return as soon as is_private is true or err + if is_private then return true end + if err then return nil, err end + end + return false end --- labels --- given a table of strings, return a string made up of concateneted labels --- where each label consists of a length value (cast as char) followed by that number of characters. -function labels( t ) - local ret = "" - for _, v in ipairs(t) do - ret = ret .. string.char( string.len(v) ) .. v - end - return ret +--- +-- Checks whether the supplied IP address is within the supplied Range of IP addresses if they belong to the same address family. +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @param range String representing a range of IPv4 or IPv6 addresses in first-last or cidr notation (e.g. "192.168.1.1 - 192.168.255.255" or "2001:0A00::/23"). +-- @usage if ip_in_range( "192.168.1.1", "192/8" ) then ... +-- @return Boolean True or False (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +ip_in_range = function( ip, range ) + + local first, last, err = get_ips_from_range( range ) + if err then return nil, err end + ip, err = expand_ip( ip ) + if err then return nil, err end + if ( ip:match( ":" ) and not first:match( ":" ) ) or ( not ip:match( ":" ) and first:match( ":" ) ) then + return nil, "Error in ip_in_range: IP address is of a different address family to Range." + end + + err = {} + local ip_ge_first, ip_le_last + ip_ge_first, err[#err+1] = compare_ip( ip, "ge", first ) + ip_le_last, err[#err+1] = compare_ip( ip, "le", last ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + if ip_ge_first and ip_le_last then + return true + else + return false + end + end --- ip_in_net --- returns true if the supplied ip address falls inside the supplied range -function ip_in_net(ip, net) - local i, j, net_lo, net_hi, dw_ip - local m_dotted = "(%d+%.%d+%.%d+%.%d+)[%s]*[-][%s]*(%d+%.%d+%.%d+%.%d+)" - local m_cidr = "(%d+)[.]*(%d*)[.]*(%d*)[.]*(%d*)[/]+(%d+)" - if net:match(m_dotted) then - net_lo, net_hi = net:match(m_dotted) - net_lo = ipOps.todword(net_lo) - net_hi = ipOps.todword(net_hi) - elseif net:match(m_cidr) then - net_lo, net_hi = two_dwords(net, m_cidr) - end +--- +-- Expands an IP address supplied in shortened notation. +-- Serves also to check the well-formedness of an IP address. +-- Note: IPv4in6 notated addresses will be returned in pure IPv6 notation unless the IPv4 portion +-- is shortened and does not contain a dot - in which case the address will be treated as IPv6. +-- @param ip String representing an IPv4 or IPv6 address in shortened or full notation. +-- @usage local ip = expand_ip( "2001::" ) +-- @return String representing a fully expanded IPv4 or IPv6 address (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +expand_ip = function( ip ) + + if type( ip ) ~= "string" or ip == "" then + return nil, "Error in expand_ip: Expected IP address as a string." + end + + local err4 = "Error in expand_ip: An address assumed to be IPv4 was malformed." + + if not ip:match( ":" ) then + -- ipv4: missing octets should be "0" appended + if ip:match( "[^\.0-9]" ) then + return nil, err4 + end + local octets = {} + for octet in string.gfind( ip, "%d+" ) do + if tonumber( octet, 10 ) > 255 then return nil, err4 end + octets[#octets+1] = octet + end + if #octets > 4 then return nil, err4 end + while #octets < 4 do + octets[#octets+1] = "0" + end + return ( table.concat( octets, "." ) ) + end + + if ip:match( "[^\.:%x]" ) then + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + end + + -- preserve :: + ip = string.gsub(ip, "::", ":z:") + + -- get a table of each hexadectet + local hexadectets = {} + for hdt in string.gfind( ip, "[\.z%x]+" ) do + hexadectets[#hexadectets+1] = hdt + end + + -- deal with IPv4in6 (last hexadectet only) + local t = {} + if hexadectets[#hexadectets]:match( "[\.]+" ) then + hexadectets[#hexadectets], err = expand_ip( hexadectets[#hexadectets] ) + if err then return nil, ( err:gsub( "IPv4", "IPv4in6" ) ) end + t = stdnse.strsplit( "[\.]+", hexadectets[#hexadectets] ) + for i, v in ipairs( t ) do + t[i] = tonumber( v, 10 ) + end + hexadectets[#hexadectets] = stdnse.tohex( 256*t[1]+t[2] ) + hexadectets[#hexadectets+1] = stdnse.tohex( 256*t[3]+t[4] ) + end + + -- deal with :: and check for invalid address + local z_done = false + for index, value in ipairs( hexadectets ) do + if value:match( "[\.]+" ) then + -- shouldn't have dots at this point + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + elseif value == "z" and z_done then + -- can't have more than one :: + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + elseif value == "z" and not z_done then + z_done = true + hexadectets[index] = "0" + local bound = 8 - #hexadectets + for i = 1, bound, 1 do + table.insert( hexadectets, index+i, "0" ) + end + elseif tonumber( value, 16 ) > 65535 then + -- more than FFFF! + return nil, ( err4:gsub( "IPv4", "IPv6" ) ) + end + end + + -- make sure we have exactly 8 hexadectets + if #hexadectets > 8 then return nil, ( err4:gsub( "IPv4", "IPv6" ) ) end + while #hexadectets < 8 do + hexadectets[#hexadectets+1] = "0" + end + + return ( table.concat( hexadectets, ":" ) ) - dw_ip = ipOps.todword(ip) - if net_lo <= dw_ip and dw_ip <= net_hi then return true end - return false end --- two_dwords --- returns the two ip addresses at either end of a cidr range, as dwords -function two_dwords(str, patt) - local a, b, c, d, e, lo_net, host - a, b, c, d, e = str:match(patt) - local ipt = {b, c, d} - local strip = "" - for _, cap in ipairs(ipt) do - if cap == "" then cap = "0" end - strip = strip .. "." .. cap - end - lo_net = a .. strip - if e ~= "" then e = tonumber(e) - if e and e <=32 then - host = 32 - e end - end - return ipOps.todword(lo_net), ipOps.todword(lo_net) + 2^host - 1 -end \ No newline at end of file + +--- +-- Compares two IP addresses (from the same address family). +-- @param left String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @param op A comparison operator which may be one of the following strings: "eq", "ge", "le", "gt" or "lt" (respectively ==, >=, <=, >, <). +-- @param right String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @usage if compare_ip( "2001::DEAD:0:0:0", "eq", "2001:0:0:0:DEAD::" ) then ... +-- @return Boolean True or False (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +compare_ip = function( left, op, right ) + + if type( left ) ~= "string" or type( right ) ~= "string" then + return nil, "Error in compare_ip: Expected IP address as a string." + end + + if ( left:match( ":" ) and not right:match( ":" ) ) or ( not left:match( ":" ) and right:match( ":" ) ) then + return nil, "Error in compare_ip: IP addresses must be from the same address family." + end + + if op == "lt" or op == "le" then + left, right = right, left + elseif op ~= "eq" and op ~= "ge" and op ~= "gt" then + return nil, "Error in compare_ip: Invalid Operator." + end + + local err ={} + left, err[#err+1] = ip_to_bin( left ) + right, err[#err+1] = ip_to_bin( right ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + if string.len( left ) ~= string.len( right ) then + -- shouldn't happen... + return nil, "Error in compare_ip: Binary IP addresses were of different lengths." + end + + -- equal? + if ( op == "eq" or op == "le" or op == "ge" ) and left == right then + return true + elseif op == "eq" then + return false + end + + -- starting from the leftmost bit, subtract the bit in right from the bit in left + local compare + for i = 1, string.len( left ), 1 do + compare = tonumber( string.sub( left, i, i ) ) - tonumber( string.sub( right, i, i ) ) + if compare == 1 then + return true + elseif compare == -1 then + return false + end + end + return false + +end + + +--- +-- Returns the first and last IP addresses in the supplied range of addresses. +-- @param range String representing a range of IPv4 or IPv6 addresses in either cidr or first-last notation. +-- @usage first, last = get_ips_from_range( "192.168.0.0/16" ) +-- @return String representing the first address in the supplied range (or nil in case of an error). +-- @return String representing the last address in the supplied range (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +get_ips_from_range = function( range ) + + if type( range ) ~= "string" then + return nil, nil, "Error in get_ips_from_range: Expected a range as a string." + end + + local first, last, prefix + if range:match( "/" ) then + first, prefix = range:match( "([%x%d:\.]+)/(%d+)" ) + elseif range:match( "-" ) then + first, last = range:match( "([%x%d:\.]+)%s*\-%s*([%x%d:\.]+)" ) + end + + local err = {} + if first and ( last or prefix ) then + first, err[#err+1] = expand_ip( first ) + else + return nil, nil, "Error in get_ips_from_range: The range supplied could not be interpreted." + end + if last then + last, err[#err+1] = expand_ip( last ) + elseif first and prefix then + last, err[#err+1] = get_last_ip( first, prefix ) + end + + if first and last then + if ( first:match( ":" ) and not last:match( ":" ) ) or ( not first:match( ":" ) and last:match( ":" ) ) then + return nil, nil, "Error in get_ips_from_range: First IP address is of a different address family to last IP address." + end + return first, last + else + return nil, nil, table.concat( err, " " ) + end + +end + + +--- +-- Calculates the last IP address of a range of addresses given an IP address in the range and prefix length for that range. +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @param prefix Decimal number or a string representing a decimal number corresponding to a Prefix length. +-- @usage last = get_last_ip( "192.0.0.0", 26 ) +-- @return String representing the last IP address of the range denoted by the supplied parameters (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +get_last_ip = function( ip, prefix ) + + local first, err = ip_to_bin( ip ) + if err then return nil, err end + + prefix = tonumber( prefix ) + if not prefix or ( prefix < 0 ) or ( prefix > string.len( first ) ) then + return nil, "Error in get_last_ip: Invalid prefix length." + end + + local hostbits = string.sub( first, prefix + 1 ) + hostbits = string.gsub( hostbits, "0", "1" ) + local last = string.sub( first, 1, prefix ) .. hostbits + last, err = bin_to_ip( last ) + if err then return nil, err end + return last + +end + + +--- +-- Converts an IP address into a string representing the address as binary digits. +-- @param ip String representing an IPv4 or IPv6 address. Shortened notation is permitted. +-- @usage bit_string = ip_to_bin( "2001::" ) +-- @return String representing the supplied IP address as 32 or 128 binary digits (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +ip_to_bin = function( ip ) + + ip, err = expand_ip( ip ) + if err then return nil, err end + + local t, mask = {} + + if not ip:match( ":" ) then + -- ipv4 string + for octet in string.gfind( ip, "%d+" ) do + t[#t+1] = stdnse.tohex( octet ) + end + mask = "00" + else + -- ipv6 string + for hdt in string.gfind( ip, "%x+" ) do + t[#t+1] = hdt + end + mask = "0000" + end + + -- padding + for i, v in ipairs( t ) do + t[i] = mask:sub( 1, string.len( mask ) - string.len( v ) ) .. v + end + + return hex_to_bin( table.concat( t ) ) + +end + + +--- +-- Converts a string representing binary digits into an IP address. +-- @param binstring String representing an IP address as 32 or 128 binary digits. +-- @usage ip = bin_to_ip( "01111111000000000000000000000001" ) +-- @return String representing an IP address (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +bin_to_ip = function( binstring ) + + if type( binstring ) ~= "string" or binstring:match( "[^01]+" ) then + return nil, "Error in bin_to_ip: Expected string of binary digits." + end + + if string.len( binstring ) == 32 then + af = 4 + elseif string.len( binstring ) == 128 then + af = 6 + else + return nil, "Error in bin_to_ip: Expected exactly 32 or 128 binary digits." + end + + t = {} + if af == 6 then + local pattern = string.rep( "[01]", 16 ) + for chunk in string.gfind( binstring, pattern ) do + t[#t+1] = stdnse.tohex( tonumber( chunk, 2 ) ) + end + return table.concat( t, ":" ) + end + + if af == 4 then + local pattern = string.rep( "[01]", 8 ) + for chunk in string.gfind( binstring, pattern ) do + t[#t+1] = tonumber( chunk, 2 ) .. "" + end + return table.concat( t, "." ) + end + +end + + +--- +-- Converts a string representing a hexadecimal number into a string representing that number as binary digits. +-- Each hex digit results in four bits - this function is really just a wrapper around stdnse.tobinary(). +-- @param hex String representing a hexadecimal number. +-- @usage bin_string = hex_to_bin( "F00D" ) +-- @return String representing the supplied number in binary digits (or nil in case of an error). +-- @return Nil (or String error message in case of an error). + +hex_to_bin = function( hex ) + + if type( hex ) ~= "string" or hex == "" or hex:match( "[^%x]+" ) then + return nil, "Error in hex_to_bin: Expected string representing a hexadecimal number." + end + + local t, mask, binchar = {}, "0000" + for hexchar in string.gfind( hex, "%x" ) do + binchar = stdnse.tobinary( tonumber( hexchar, 16 ) ) + t[#t+1] = mask:sub( 1, string.len( mask ) - string.len( binchar ) ) .. binchar + end + return table.concat( t ) + +end + + +--- +-- Calculates the prefix length for the given IP address range. +-- @param range String representing an IP address range +-- @return Number - prefix length of the range + +function get_prefix_length( range ) + + if type( range ) ~= "string" or range == "" then return nil end + + local first, last, err = get_ips_from_range( range ) + if err then return nil end + + first = ip_to_bin( first ):reverse() + last = ip_to_bin( last ):reverse() + + local hostbits = 0 + for pos = 1, string.len( first ), 1 do + + if first:sub( pos, pos ) == "0" and last:sub( pos, pos ) == "1" then + hostbits = hostbits + 1 + else + break + end + + end + + return ( string.len( first ) - hostbits ) + +end diff --git a/scripts/script.db b/scripts/script.db index f8cdfe575..8865d156d 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -1,88 +1,88 @@ -Entry{ category = "default", filename = "dns-test-open-recursion.nse" } -Entry{ category = "intrusive", filename = "dns-test-open-recursion.nse" } +Entry{ category = "default", filename = "showOwner.nse" } +Entry{ category = "safe", filename = "showOwner.nse" } +Entry{ category = "demo", filename = "daytimeTest.nse" } Entry{ category = "default", filename = "RealVNC_auth_bypass.nse" } Entry{ category = "malware", filename = "RealVNC_auth_bypass.nse" } Entry{ category = "vuln", filename = "RealVNC_auth_bypass.nse" } -Entry{ category = "intrusive", filename = "dns-safe-recursion-port.nse" } -Entry{ category = "intrusive", filename = "SNMPcommunitybrute.nse" } -Entry{ category = "auth", filename = "SNMPcommunitybrute.nse" } -Entry{ category = "default", filename = "showOwner.nse" } -Entry{ category = "safe", filename = "showOwner.nse" } -Entry{ category = "default", filename = "SSLv2-support.nse" } -Entry{ category = "safe", filename = "SSLv2-support.nse" } -Entry{ category = "malware", filename = "ircZombieTest.nse" } -Entry{ category = "version", filename = "skype_v2-version.nse" } -Entry{ category = "discovery", filename = "HTTPtrace.nse" } -Entry{ category = "demo", filename = "echoTest.nse" } -Entry{ category = "default", filename = "UPnP-info.nse" } -Entry{ category = "safe", filename = "UPnP-info.nse" } -Entry{ category = "default", filename = "rpcinfo.nse" } -Entry{ category = "safe", filename = "rpcinfo.nse" } -Entry{ category = "discovery", filename = "rpcinfo.nse" } -Entry{ category = "auth", filename = "bruteTelnet.nse" } -Entry{ category = "intrusive", filename = "bruteTelnet.nse" } -Entry{ category = "intrusive", filename = "dns-safe-recursion-txid.nse" } -Entry{ category = "default", filename = "SMTPcommands.nse" } -Entry{ category = "discovery", filename = "SMTPcommands.nse" } -Entry{ category = "safe", filename = "SMTPcommands.nse" } -Entry{ category = "default", filename = "robots.nse" } -Entry{ category = "safe", filename = "robots.nse" } -Entry{ category = "default", filename = "zoneTrans.nse" } -Entry{ category = "intrusive", filename = "zoneTrans.nse" } -Entry{ category = "discovery", filename = "zoneTrans.nse" } -Entry{ category = "discovery", filename = "whois.nse" } -Entry{ category = "safe", filename = "whois.nse" } -Entry{ category = "discovery", filename = "ripeQuery.nse" } -Entry{ category = "demo", filename = "chargenTest.nse" } -Entry{ category = "malware", filename = "strangeSMTPport.nse" } -Entry{ category = "version", filename = "iax2Detect.nse" } -Entry{ category = "demo", filename = "showSMTPVersion.nse" } -Entry{ category = "discovery", filename = "ASN.nse" } -Entry{ category = "default", filename = "showHTMLTitle.nse" } -Entry{ category = "demo", filename = "showHTMLTitle.nse" } -Entry{ category = "safe", filename = "showHTMLTitle.nse" } -Entry{ category = "discovery", filename = "promiscuous.nse" } -Entry{ category = "version", filename = "netbios-smb-os-discovery.nse" } -Entry{ category = "default", filename = "anonFTP.nse" } -Entry{ category = "auth", filename = "anonFTP.nse" } -Entry{ category = "intrusive", filename = "anonFTP.nse" } Entry{ category = "intrusive", filename = "SQLInject.nse" } Entry{ category = "vuln", filename = "SQLInject.nse" } +Entry{ category = "auth", filename = "bruteTelnet.nse" } +Entry{ category = "intrusive", filename = "bruteTelnet.nse" } +Entry{ category = "discovery", filename = "HTTPtrace.nse" } Entry{ category = "demo", filename = "SMTP_openrelay_test.nse" } -Entry{ category = "default", filename = "nbstat.nse" } -Entry{ category = "discovery", filename = "nbstat.nse" } -Entry{ category = "safe", filename = "nbstat.nse" } Entry{ category = "default", filename = "HTTPAuth.nse" } Entry{ category = "auth", filename = "HTTPAuth.nse" } Entry{ category = "intrusive", filename = "HTTPAuth.nse" } -Entry{ category = "default", filename = "finger.nse" } -Entry{ category = "discovery", filename = "finger.nse" } -Entry{ category = "demo", filename = "showHTTPVersion.nse" } -Entry{ category = "default", filename = "SSHv1-support.nse" } -Entry{ category = "safe", filename = "SSHv1-support.nse" } -Entry{ category = "default", filename = "popcapa.nse" } -Entry{ category = "default", filename = "SNMPsysdescr.nse" } -Entry{ category = "discovery", filename = "SNMPsysdescr.nse" } -Entry{ category = "safe", filename = "SNMPsysdescr.nse" } -Entry{ category = "intrusive", filename = "brutePOP3.nse" } -Entry{ category = "auth", filename = "brutePOP3.nse" } -Entry{ category = "default", filename = "MySQLinfo.nse" } -Entry{ category = "discovery", filename = "MySQLinfo.nse" } -Entry{ category = "safe", filename = "MySQLinfo.nse" } -Entry{ category = "default", filename = "ftpbounce.nse" } -Entry{ category = "intrusive", filename = "ftpbounce.nse" } -Entry{ category = "auth", filename = "xamppDefaultPass.nse" } -Entry{ category = "vuln", filename = "xamppDefaultPass.nse" } -Entry{ category = "intrusive", filename = "HTTPpasswd.nse" } -Entry{ category = "vuln", filename = "HTTPpasswd.nse" } -Entry{ category = "demo", filename = "showSSHVersion.nse" } -Entry{ category = "version", filename = "PPTPversion.nse" } -Entry{ category = "default", filename = "ircServerInfo.nse" } -Entry{ category = "discovery", filename = "ircServerInfo.nse" } +Entry{ category = "default", filename = "dns-test-open-recursion.nse" } +Entry{ category = "intrusive", filename = "dns-test-open-recursion.nse" } +Entry{ category = "demo", filename = "chargenTest.nse" } +Entry{ category = "default", filename = "showHTMLTitle.nse" } +Entry{ category = "demo", filename = "showHTMLTitle.nse" } +Entry{ category = "safe", filename = "showHTMLTitle.nse" } Entry{ category = "default", filename = "MSSQLm.nse" } Entry{ category = "discovery", filename = "MSSQLm.nse" } Entry{ category = "intrusive", filename = "MSSQLm.nse" } +Entry{ category = "demo", filename = "echoTest.nse" } +Entry{ category = "default", filename = "SSHv1-support.nse" } +Entry{ category = "safe", filename = "SSHv1-support.nse" } +Entry{ category = "default", filename = "MySQLinfo.nse" } +Entry{ category = "discovery", filename = "MySQLinfo.nse" } +Entry{ category = "safe", filename = "MySQLinfo.nse" } +Entry{ category = "auth", filename = "xamppDefaultPass.nse" } +Entry{ category = "vuln", filename = "xamppDefaultPass.nse" } +Entry{ category = "default", filename = "SSLv2-support.nse" } +Entry{ category = "safe", filename = "SSLv2-support.nse" } +Entry{ category = "default", filename = "zoneTrans.nse" } +Entry{ category = "intrusive", filename = "zoneTrans.nse" } +Entry{ category = "discovery", filename = "zoneTrans.nse" } +Entry{ category = "default", filename = "ftpbounce.nse" } +Entry{ category = "intrusive", filename = "ftpbounce.nse" } +Entry{ category = "version", filename = "skype_v2-version.nse" } +Entry{ category = "discovery", filename = "promiscuous.nse" } +Entry{ category = "default", filename = "SNMPsysdescr.nse" } +Entry{ category = "discovery", filename = "SNMPsysdescr.nse" } +Entry{ category = "safe", filename = "SNMPsysdescr.nse" } +Entry{ category = "demo", filename = "showSMTPVersion.nse" } +Entry{ category = "default", filename = "nbstat.nse" } +Entry{ category = "discovery", filename = "nbstat.nse" } +Entry{ category = "safe", filename = "nbstat.nse" } +Entry{ category = "version", filename = "iax2Detect.nse" } +Entry{ category = "default", filename = "rpcinfo.nse" } +Entry{ category = "safe", filename = "rpcinfo.nse" } +Entry{ category = "discovery", filename = "rpcinfo.nse" } Entry{ category = "default", filename = "HTTP_open_proxy.nse" } Entry{ category = "discovery", filename = "HTTP_open_proxy.nse" } Entry{ category = "intrusive", filename = "HTTP_open_proxy.nse" } -Entry{ category = "demo", filename = "daytimeTest.nse" } +Entry{ category = "intrusive", filename = "HTTPpasswd.nse" } +Entry{ category = "vuln", filename = "HTTPpasswd.nse" } +Entry{ category = "demo", filename = "showSSHVersion.nse" } +Entry{ category = "default", filename = "SMTPcommands.nse" } +Entry{ category = "discovery", filename = "SMTPcommands.nse" } +Entry{ category = "safe", filename = "SMTPcommands.nse" } +Entry{ category = "default", filename = "anonFTP.nse" } +Entry{ category = "auth", filename = "anonFTP.nse" } +Entry{ category = "intrusive", filename = "anonFTP.nse" } +Entry{ category = "version", filename = "netbios-smb-os-discovery.nse" } +Entry{ category = "default", filename = "robots.nse" } +Entry{ category = "safe", filename = "robots.nse" } +Entry{ category = "default", filename = "finger.nse" } +Entry{ category = "discovery", filename = "finger.nse" } +Entry{ category = "default", filename = "UPnP-info.nse" } +Entry{ category = "safe", filename = "UPnP-info.nse" } +Entry{ category = "malware", filename = "strangeSMTPport.nse" } +Entry{ category = "default", filename = "ircServerInfo.nse" } +Entry{ category = "discovery", filename = "ircServerInfo.nse" } +Entry{ category = "malware", filename = "ircZombieTest.nse" } +Entry{ category = "discovery", filename = "ripeQuery.nse" } +Entry{ category = "demo", filename = "showHTTPVersion.nse" } +Entry{ category = "version", filename = "PPTPversion.nse" } +Entry{ category = "discovery", filename = "ASN.nse" } +Entry{ category = "intrusive", filename = "brutePOP3.nse" } +Entry{ category = "auth", filename = "brutePOP3.nse" } +Entry{ category = "default", filename = "popcapa.nse" } +Entry{ category = "intrusive", filename = "SNMPcommunitybrute.nse" } +Entry{ category = "auth", filename = "SNMPcommunitybrute.nse" } +Entry{ category = "discovery", filename = "whois.nse" } +Entry{ category = "safe", filename = "whois.nse" } +Entry{ category = "intrusive", filename = "dns-safe-recursion-txid.nse" } +Entry{ category = "intrusive", filename = "dns-safe-recursion-port.nse" }