1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-23 16:09:02 +00:00
Files
nmap/scripts/ASN.nse
2008-09-06 03:45:37 +00:00

313 lines
9.0 KiB
Lua

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.
]]
---
-- @usage
-- nmap <target> --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
--
author = "jah, Michael"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery"}
runlevel = 1
local dns = require "dns"
local comm = require "comm"
local ipOps = require "ipOps"
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 ipOps.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 )
mutex "lock"
-- check for cached data
local in_cache, records
local combined_records = {}
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 = dns.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 ipOps.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
---
-- 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 = ipOps.get_ips_from_range( range )
if err then return nil end
first = ipOps.ip_to_bin( first ):reverse()
last = ipOps.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