diff --git a/scripts/sstp-discover.nse b/scripts/sstp-discover.nse index 59552ca63..cf3e243de 100644 --- a/scripts/sstp-discover.nse +++ b/scripts/sstp-discover.nse @@ -56,7 +56,13 @@ portrule = function(host, port) return shortport.http(host, port) and shortport.ssl(host, port) end -local request = 'SSTP_DUPLEX_POST /sra_{BA195980-CD49-458b-9E23-C84EE0ADCD75} HTTP/1.1\r\nHost: %s\r\nSSTPCORRELATIONID: {}\r\n\r\nContent-Length: 18446744073709551615\r\n\r\n' +-- The SSTPCORRELATIONID GUID is optional and client-generated. +-- The last 5 bytes are "Nmap!" +local request = +'SSTP_DUPLEX_POST /sra_{BA195980-CD49-458b-9E23-C84EE0ADCD75}/ HTTP/1.1\r\n' .. +'Host: %s\r\n' .. +'SSTPCORRELATIONID: {5a433238-8781-11e3-b2e4-4e6d617021}\r\n' .. +'Content-Length: 18446744073709551615\r\n\r\n' action = function(host, port) local socket, response = comm.tryssl(host,port, diff --git a/scripts/whois-ip.nse b/scripts/whois-ip.nse index c0d819447..2d63440ad 100644 --- a/scripts/whois-ip.nse +++ b/scripts/whois-ip.nse @@ -99,13 +99,13 @@ categories = {"discovery", "external", "safe"} hostrule = function( host ) - local is_private, err = ipOps.isPrivate( host.ip ) - if is_private == nil then - stdnse.print_debug( "%s Error in Hostrule: %s.", SCRIPT_NAME, err ) - return false - end + local is_private, err = ipOps.isPrivate( host.ip ) + if is_private == nil then + stdnse.print_debug( "%s Error in Hostrule: %s.", SCRIPT_NAME, err ) + return false + end - return not is_private + return not is_private end @@ -196,7 +196,7 @@ action = function( host ) status, retval = pcall( get_next_action, tracking, host.ip ) if not status then - stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval ) + stdnse.print_debug( "%s %s pcall caught an exception in get_next_action: %s.", SCRIPT_NAME, host.ip, retval ) else tracking = retval end if tracking.this_db then @@ -207,7 +207,7 @@ action = function( host ) -- analyse data status, retval = pcall( analyse_response, tracking, host.ip, response, data ) if not status then - stdnse.print_debug( "%s %s pcall caught an exception in analyse_response: %s.", SCRIPT_NAME, host.ip, retval ) + stdnse.print_debug( "%s %s pcall caught an exception in analyse_response: %s.", SCRIPT_NAME, host.ip, retval ) else data = retval end -- get next action @@ -303,8 +303,8 @@ function get_next_action( tracking, ip ) end - -- get the next untried service from whoisdb_default_order - if not tracking.this_db and nmap.registry.whois.whoisdb_default_order then + -- get the next untried service from whoisdb_default_order + if not tracking.this_db and nmap.registry.whois.whoisdb_default_order then for i, db in ipairs( nmap.registry.whois.whoisdb_default_order ) do if not table.concat( tracking.completed, " " ):match( db ) then @@ -697,7 +697,7 @@ function analyse_response( tracking, ip, response, data ) -- check if any of the objects we like match this single object in response chunk for ob, t in pairs( meta.fieldreq ) do if ob ~= "ob_exist" and type( t.ob_start ) == "string" and response_chunk:match( t.ob_start ) then - data[this_db][ob] = extract_objects_from_response( response_chunk, this_db, ip, meta, ob ) + data[this_db][ob] = extract_objects_from_response( response_chunk, this_db, ip, meta, ob ) end end @@ -841,7 +841,7 @@ function extract_objects_from_response( response_string, db, ip, meta, specific_ end end end - end -- if ob_start and ob_end + end -- if ob_start and ob_end end -- if object_name end -- for object_name @@ -924,7 +924,7 @@ function redirection_rules( db, ip, data, meta ) end --redirection_validation - -- iterate over each table of redirect info for a specific field + -- iterate over each table of redirect info for a specific field for _, redirect_elems in ipairs( meta.redirects ) do local obj, fld, pattern = table.unpack( redirect_elems ) -- three redirect elements @@ -1139,7 +1139,7 @@ function smallest_range( range_1, range_2 ) local r2_first, r2_last = ipOps.get_ips_from_range( range_2.range ) if range_1.pointer and ipOps.compare_ip( r1_first, "eq", r2_first ) and ipOps.compare_ip( r1_last, "eq", r2_last ) - and range_1.pointer < range_2.pointer then + and range_1.pointer < range_2.pointer then sorted = false end @@ -1366,828 +1366,828 @@ function script_init( ) rpsl = { ob_exist = "\r?\n?%s*[Ii]net6?num:%s*.-\r?\n", ob_netnum = {ob_start = "\r?\n?%s*[Ii]net6?num:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", - netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:%s*(.-)\r?\n", - nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:%s*(.-)\r?\n", - descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", - status = "\r?\n%s*[Ss]tatus:%s*(.-)\r?\n", - source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", + netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:%s*(.-)\r?\n", + nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:%s*(.-)\r?\n", + descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", + country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", + status = "\r?\n%s*[Ss]tatus:%s*(.-)\r?\n", + source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, ob_org = { ob_start = "\r?\n%s*[Oo]rgani[sz]ation:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - organisation = "\r?\n%s*[Oo]rgani[sz]ation:%s*(.-)\r?\n", - orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:%s*(.-)\r?\n", - descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", - email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"}, + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + organisation = "\r?\n%s*[Oo]rgani[sz]ation:%s*(.-)\r?\n", + orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:%s*(.-)\r?\n", + descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n", + email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"}, ob_role = { ob_start = "\r?\n%s*[Rr]ole:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - role = "\r?\n%s*[Rr]ole:%s*(.-)\r?\n", - email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"}, + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + role = "\r?\n%s*[Rr]ole:%s*(.-)\r?\n", + email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"}, ob_persn = { ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n", - ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", - person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", - email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, - arin = { - ob_exist = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", - ob_netnum = {ob_start = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", - ob_end = "\r?\n\r?\n", - netrange = "\r?\n%s*[Nn]et[-]-[Rr]ange:(.-)\r?\n", - netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:(.-)\r?\n", - nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:(.-)\r?\n"}, - ob_org = {ob_start = "\r?\n%s*[Oo]rg[-]-[Nn]ame:.-\r?\n", - ob_end = "\r?\n\r?\n", - orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:(.-)\r?\n", - orgid = "\r?\n%s*[Oo]rg[-]-[Ii][Dd]:(.-)\r?\n", - stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, - ob_cust = {ob_start = "\r?\n%s*[Cc]ust[-]-[Nn]ame:.-\r?\n", - ob_end = "\r?\n\r?\n", - custname = "\r?\n%s*[Cc]ust[-]-[Nn]ame:(.-)\r?\n", - stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, - ob_persn = {ob_start = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:.-\r?\n", - ob_end = "\r?\n\r?\n", - orgtechname = - "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:(.-)\r?\n", - orgtechemail = - "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Ee][-]-[Mm]ail:(.-)\r?\n"} }, - lacnic = { - ob_exist = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", - ob_netnum = {ob_start = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", - ob_end = "\r?\n\r?\n", - inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", - owner = "\r?\n%s*[Oo]wner:%s*(.-)\r?\n", - ownerid = "\r?\n%s*[Oo]wner[-]-[Ii][Dd]:%s*(.-)\r?\n", - responsible = "\r?\n%s*[Rr]esponsible:%s*(.-)\r?\n", - country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", - source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, - ob_persn = {ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n", - ob_end = "\r?\n\r?\n", - person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", - email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, - jpnic = { - ob_exist = "\r?\n%s*[Nn]etwork%s-[Ii]nformation:%s*.-\r?\n", - ob_netnum = {ob_start = "[[Nn]etwork%s*[Nn]umber]%s*.-\r?\n", + ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n", + person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", + email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, + arin = { + ob_exist = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", + ob_netnum = {ob_start = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n", + ob_end = "\r?\n\r?\n", + netrange = "\r?\n%s*[Nn]et[-]-[Rr]ange:(.-)\r?\n", + netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:(.-)\r?\n", + nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:(.-)\r?\n"}, + ob_org = {ob_start = "\r?\n%s*[Oo]rg[-]-[Nn]ame:.-\r?\n", + ob_end = "\r?\n\r?\n", + orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:(.-)\r?\n", + orgid = "\r?\n%s*[Oo]rg[-]-[Ii][Dd]:(.-)\r?\n", + stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n", + country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, + ob_cust = {ob_start = "\r?\n%s*[Cc]ust[-]-[Nn]ame:.-\r?\n", + ob_end = "\r?\n\r?\n", + custname = "\r?\n%s*[Cc]ust[-]-[Nn]ame:(.-)\r?\n", + stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n", + country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"}, + ob_persn = {ob_start = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:.-\r?\n", + ob_end = "\r?\n\r?\n", + orgtechname = + "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:(.-)\r?\n", + orgtechemail = + "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Ee][-]-[Mm]ail:(.-)\r?\n"} }, + lacnic = { + ob_exist = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", + ob_netnum = {ob_start = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n", + ob_end = "\r?\n\r?\n", + inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n", + owner = "\r?\n%s*[Oo]wner:%s*(.-)\r?\n", + ownerid = "\r?\n%s*[Oo]wner[-]-[Ii][Dd]:%s*(.-)\r?\n", + responsible = "\r?\n%s*[Rr]esponsible:%s*(.-)\r?\n", + country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n", + source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"}, + ob_persn = {ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n", + ob_end = "\r?\n\r?\n", + person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n", + email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"} }, + jpnic = { + ob_exist = "\r?\n%s*[Nn]etwork%s-[Ii]nformation:%s*.-\r?\n", + ob_netnum = {ob_start = "[[Nn]etwork%s*[Nn]umber]%s*.-\r?\n", ob_end = "\r?\n\r?\n", inetnum = "[[Nn]etwork%s*[Nn]umber]%s*(.-)\r?\n", netname = "[[Nn]etwork%s*[Nn]ame]%s*(.-)\r?\n", orgname = "[[Oo]rganization]%s*(.-)\r?\n"} } - } - - --- - -- whoisdb defines the whois services this script is able to query and the script output produced for them. - -- Each entry is a key-value pair where the key is a short name for the service and value is a table of definitions for that service. - -- Note that there is defined here an entry for IANA which does not have a whois service. The entry is defined to allow us to redirect to ARIN when - -- IANA is referred to in a record. - -- - -- Each service defined should contain the following: - -- - -- id: String. Matches the key for the service and is a short name for the service. - -- hostname: String. Hostname of the service. - -- preflag: String. Prepended to the target IP address sent in the whois query. - -- postflag: String. Appended to the target IP address sent in the whois query. - -- longname: Table of strings. Each is a lowercase official (or semi-official) name of the service. - -- fieldreq: Linked table entry. The key identifying a table of a set of objects defined in fields_meta. - -- In its records each whois service displays a particular set of objects as defined here. - -- smallnet_rule: Linked table entry. The key of a pattern for the field defined in fields_meta which captures the Assignment Range. This is an - -- optional entry and is used to extract the smallest (i.e. Most Specific) range from a record when more than one range is detailed. - -- redirects: Table of tables, containing strings. Used to determine whether a record is referring to a different whois service by - -- searching for service specific information in certain fields of the record. - -- Each entry is a table thus: { "search_object", "search_field", "pattern" } - -- search_object: is the key name for a record object defined in fields_meta, in which to search. - -- search_field: is the key name for a field of the object, the data of which to search. - -- pattern: is typically the id or longname key names. - -- In the example: {"ob_org", "orgname", "longname"}, we cycle through each service defined in whoisdb and look for its longname in - -- the ob_org.orgname of the current record. - -- output_short: Table for each object to be displayed when Nmap verbosity is zero. The first element of each table is the object name and the - -- second element is a table of fields to display. The elements of the second may be field names, which are each output to a new - -- line, or tables containing field names which are output to the same line. - -- output_long: Table for each object to be displayed when Nmap verbosity is one or above. The structure is the same as output_short. - -- reg: String name for the field in ob_netnum which captures the Assignment Range (e.g. "netrange", "inetnum"), the data of which is - -- cached in the registry. - -- unordered: Boolean. Optional. True if the records from the service display an object other than ob_netnum as the first in the record (such - -- as at ARIN). This flag is used to decide whether we should extract an object immediately before the relevant ob_netnum object - -- from a record. - - nmap.registry.whois.whoisdb = { - arin = { - id = "arin", - hostname = "whois.arin.net", preflag = "+", postflag = "", - longname = {"american registry for internet numbers"}, - fieldreq = nmap.registry.whois.fields_meta.arin, - smallnet_rule = nmap.registry.whois.fields_meta.arin.ob_netnum.netrange, - redirects = { - {"ob_org", "orgname", "longname"}, - {"ob_org", "orgname", "id"}, - {"ob_org", "orgid", "id"} }, - output_short = { - {"ob_netnum", {"netrange", "netname"}}, - {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}} }, - output_long = { - {"ob_netnum", {"netrange", "netname"}}, - {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}}, - {"ob_cust", {"custname", {"country", "stateprov"}}}, - {"ob_persn", {"orgtechname", "orgtechemail"}} }, - reg = "netrange", - unordered = true - }, - ripe = { - id = "ripe", - hostname = "whois.ripe.net", preflag = "-B", postflag = "", - longname = {"ripe network coordination centre"}, - fieldreq = nmap.registry.whois.fields_meta.rpsl, - smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, - redirects = { - {"ob_role", "role", "longname"}, - {"ob_org", "orgname", "id"}, - {"ob_org", "orgname", "longname"} }, - output_short = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}} }, - output_long = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}}, - {"ob_role", {"role", "email"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - }, - apnic = { - id = "apnic", - hostname = "whois.apnic.net", preflag = "", postflag = "", - longname = {"asia pacific network information centre"}, - fieldreq = nmap.registry.whois.fields_meta.rpsl, - smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, - redirects = { - {"ob_netnum", "netname", "id"}, - {"ob_org", "orgname", "longname"}, - {"ob_role", "role", "longname"}, - {"ob_netnum", "source", "id"} }, - output_short = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}} }, - output_long = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}}, - {"ob_role", {"role", "email"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - }, - lacnic = { - id = "lacnic", - hostname = "whois.lacnic.net", preflag = "", postflag = "", - longname = - {"latin american and caribbean ip address regional registry"}, - fieldreq = nmap.registry.whois.fields_meta.lacnic, - smallnet_rule = nmap.registry.whois.fields_meta.lacnic.ob_netnum.inetnum, - redirects = { - {"ob_netnum", "ownerid", "id"}, - {"ob_netnum", "source", "id"} }, - output_short = { - {"ob_netnum", - {"inetnum", "owner", "ownerid", "responsible", "country"}} }, - output_long = { - {"ob_netnum", - {"inetnum", "owner", "ownerid", "responsible", "country"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - }, - afrinic = { - id = "afrinic", - hostname = "whois.afrinic.net", preflag = "-c", postflag = "", - longname = {"african internet numbers registry", - "african network information center"}, - fieldreq = nmap.registry.whois.fields_meta.rpsl, - smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, - redirects = { - {"ob_org", "orgname", "longname"} }, - output_short = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}} }, - output_long = { - {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, - {"ob_org", {"orgname", "organisation", "descr", "email"}}, - {"ob_role", {"role", "email"}}, - {"ob_persn", {"person", "email"}} }, - reg = "inetnum" - },--[[ - jpnic = { - id = "jpnic", - hostname = "whois.nic.ad.jp", preflag = "", postflag = "/e", - longname = {"japan network information center"}, - fieldreq = nmap.registry.whois.fields_meta.jpnic, - output_short = { - {"ob_netnum", {"inetnum", "netname", "orgname"}} }, - reg = "inetnum" },--]] - iana = { -- not actually a db but required here - id = "iana", longname = {"internet assigned numbers authority"} - } - } - - nmap.registry.whois.m_none = { - "\n%s*([Nn]o match found for[%s+]*$addr)", - "\n%s*([Uu]nallocated resource:%s*$addr)", - "\n%s*([Rr]eserved:%s*$addr)", - "\n[^\n]*([Nn]ot%s[Aa]ssigned[^\n]*$addr)", - "\n%s*(No match!!)%s*\n", - "(Invalid IP or CIDR block:%s*$addr)" - } - nmap.registry.whois.m_err = { - "\n%s*([Aa]n [Ee]rror [Oo]ccured)%s*\n", - "\n[^\n]*([Ee][Rr][Rr][Oo][Rr][^\n]*)\n" - } - - nmap.registry.whois.remote_assignments_files = {} - nmap.registry.whois.remote_assignments_files.ipv4 = { - { - remote_resource = "http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt", - local_resource = "ipv4-address-space", - match_assignment = "^%s*([%.%d]+/%d+)", - match_service = "whois%.(%w+)%.net" - } - } - nmap.registry.whois.remote_assignments_files.ipv6 = { - --[[{ - remote_resource = "http://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt", - local_resource = "ipv6-address-space", - match_assignment = "^([:%x]+/%d+)", - match_service = "^[:%x]+/%d+%s*(%w+)" - },--]] - { - remote_resource = "http://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.txt", - local_resource = "ipv6-unicast-address-assignments", - match_assignment = "^%s*([:%x]+/%d+)", - match_service = "whois%.(%w+)%.net" - } - } - - local err - - -- get and validate any --script-args - get_args() - - -- mutex for each service - nmap.registry.whois.mutex = {} - for id, v in pairs( nmap.registry.whois.whoisdb ) do - if id ~= "iana" then - nmap.registry.whois.mutex[id] = nmap.mutex(nmap.registry.whois.whoisdb[id]) - end - end - - -- get IANA assignments lists - if nmap.registry.whois.using_local_assignments_file then - nmap.registry.whois.local_assignments_data, err = get_local_assignments_data() - if err then nmap.registry.whois.using_local_assignments_file = false end - end - - nmap.registry.whois.init_done = true - -end - - - ---- --- Parses the command line arguments passed to the script with --script-args. --- Sets flags in the registry which threads read to determine certain behaviours. --- Permitted args are 'nofile' - Prevents use of a list of assignments to determine which service to query, --- 'nofollow' - Prevents following redirects found in records, --- 'arin', 'ripe', 'apnic', etc. - Service id's, as defined in the whoisdb table in the registry (see script_init). - -function get_args() - - if not nmap.registry.args then return end - - local args = stdnse.get_script_args('whois.whodb') - - if type( args ) ~= "string" or ( args == "" ) then return end - - local t = {} - -- match words in args which may be whois dbs or other arguments - for db in string.gmatch( args, "%w+" ) do - if not nmap.registry.whois.whoisdb[db] then - if ( db == "nofollow" ) then - nmap.registry.whois.nofollow = true - elseif ( db == "nocache" ) then - nmap.registry.whois.using_cache = false - elseif ( db == "nofile" ) then - nmap.registry.whois.using_local_assignments_file = false - stdnse.print_debug( 2, "%s: Not using local assignments data.", SCRIPT_NAME ) - end - elseif not ( string.match( table.concat( t, " " ), db ) ) then - -- we have a unique valid whois db - t[#t+1] = db - end - end - - if ( #t > 0 ) and nmap.registry.whois.using_local_assignments_file then - -- "nofile" was not explicitly supplied, but it is implied by supplying custom whoisdb_default_order - nmap.registry.whois.using_local_assignments_file = false - stdnse.print_debug(3, "%s: Not using local assignments data because custom whoisdb_default_order was supplied.", SCRIPT_NAME) - end - - if ( #t > 1 ) and nmap.registry.whois.nofollow then - -- using nofollow, we do not follow redirects and can only accept what we find as a record therefore we only accept the first db supplied - t = {t[1]} - stdnse.print_debug( 1, "%s: Too many args supplied with 'nofollow', only using %s.", SCRIPT_NAME, t[1] ) - end - - if ( #t > 0 ) then - nmap.registry.whois.whoisdb_default_order = t - stdnse.print_debug( 2, "%s: whoisdb_default_order: %s.", SCRIPT_NAME, table.concat( t, " " ) ) - end - -end - - - ---- --- Makes IANA hosted assignments data available for lookups against that data. In more detail it: --- Caches a local copy of remote assignments data if copies do not currently exist or are out-of-date. --- Checks whether the cached copies require updating and performs update as required. --- Parses the cached copies and populates a table of lookup data which is returned to the caller. --- Sets a flag in the registry to prevent use of the lookup data in the event of an error. --- @return Table of lookup data (or nil in case of an error). --- @return Nil or error message in case of an error. --- @see get_parentpath, file_exists, requires_updating, read_from_file, conditional_download, --- write_to_file, parse_assignments - -function get_local_assignments_data() - - if not next( nmap.registry.whois.remote_assignments_files ) then - nmap.registry.whois.using_local_assignments_file = false - return nil, "Error in get_local_assignments_data: Remote resources not defined in remote_assignments_files registry key" - end - - -- get the directory path where cached files will be stored. - local fetchfile = "nmap-services" - local directory_path, err = get_parentpath( fetchfile ) - if err then - stdnse.print_debug( 1, "%s: Nmap.fetchfile() failed to get a path to %s: %s.", SCRIPT_NAME, fetchfile, err ) - return nil, err - end - - local ret = {} - - -- cache or update and parse each remote file for each address family - for address_family, t in pairs( nmap.registry.whois.remote_assignments_files ) do - for i, assignment_data_spec in ipairs( t ) do - - local update_required, modified_date, entity_tag, err - - -- do we have a cached file and does it need updating? - local file, exists = directory_path .. assignment_data_spec.local_resource - exists, err = file_exists( file ) - if not exists and err then - stdnse.print_debug( 1, "%s: Error accessing %s: %s.", SCRIPT_NAME, file, err ) - elseif not exists then - update_required = true - stdnse.print_debug( 2, "%s: %s does not exist or is empty. Fetching it now...", SCRIPT_NAME, file ) - elseif exists then - update_required, modified_date, entity_tag = requires_updating( file ) - end - - local file_content - - -- read an existing and up-to-date file into file_content. - if exists and not update_required then - stdnse.print_debug( 2, "%s: %s was cached less than %s ago. Reading...", SCRIPT_NAME, file, nmap.registry.whois.local_assignments_file_expiry ) - file_content = read_from_file( file ) - end - - -- cache or update and then read into file_content - local http_response, write_success - if update_required then - http_response = ( conditional_download( assignment_data_spec.remote_resource, modified_date, entity_tag ) ) - if not http_response or type( http_response.status ) ~= "number" then - stdnse.print_debug( 1, "%s: Failed whilst requesting %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) - elseif http_response.status == 200 then - -- prepend our file header - stdnse.print_debug( 2, "%s: Retrieved %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) - file_content = stdnse.strsplit( "\r?\n", http_response.body ) - table.insert( file_content, 1, "** Do Not Alter This Line or The Following Line **" ) - local hline = {} - hline[#hline+1] = "<" .. os.time() .. ">" - hline[#hline+1] = "<" .. http_response.header["last-modified"] .. ">" - if http_response.header.etag then - hline[#hline+1] = "<" .. http_response.header.etag .. ">" - end - table.insert( file_content, 2, table.concat( hline ) ) - write_success, err = write_to_file( file, file_content ) - if err then - stdnse.print_debug( 1, "%s: Error writing %s to %s: %s.", SCRIPT_NAME, assignment_data_spec.remote_resource, file, err ) - end - elseif http_response.status == 304 then - -- update our file header with a new timestamp - stdnse.print_debug( 1, "%s: %s is up-to-date.", SCRIPT_NAME, file ) - file_content = read_from_file( file ) - file_content[2] = file_content[2]:gsub("^<[-+]?%d+>(.*)$", "<" .. os.time() .. ">%1") - write_success, err = write_to_file( file, file_content ) - if err then - stdnse.print_debug( 1, "%s: Error writing to %s: %s.", SCRIPT_NAME, file, err ) - end - else - stdnse.print_debug( 1, "%s: HTTP %s whilst requesting %s.", SCRIPT_NAME, http_response.status, assignment_data_spec.remote_resource ) - end - end - - - if file_content then - -- Create a table for this address family (if there isn't one already). - if not ret[address_family] then ret[address_family] = {} end - -- Parse data and add to the table for this address family. - local t - t, err = parse_assignments( assignment_data_spec, file_content ) - if #t == 0 or err then - -- good header, but bad file? Kill the file! - write_to_file( file, "" ) - stdnse.print_debug( 1, "%s: Problem with the data in %s.", SCRIPT_NAME, file ) - else - for i, v in pairs( t ) do - ret[address_family][#ret[address_family]+1] = v - end - end - end - - end -- file - end -- af - - -- If we decide to use more than one assignments file for ipv6 we may need to sort the resultant parsed list so that sub-assignments appear - -- before their parent. This is expensive, but it's worth doing to ensure the lookup process returns the correct service. - -- table.sort( ret.ipv6, sort_assignments ) - - -- final check for an empty table which we'll convert to nil - for af, t in pairs( ret ) do - if #t == 0 then - ret[af] = nil - stdnse.print_debug( 1, "%s: Cannot use local assignments file for address family %s.", SCRIPT_NAME, af ) - end - end - - return ret - -end - - - ---- --- Uses nmap.fetchfile to get the path of the parent directory of the supplied Nmap datafile SCRIPT_NAME. --- @param fname String - Filename of an Nmap datafile. --- @return String - The filepath of the directory containing the supplied SCRIPT_NAME including the trailing slash (or nil in case of an error). --- @return Nil or error message in case of an error. - -function get_parentpath( fname ) - - if type( fname ) ~= "string" or fname == "" then - return nil, "Error in get_parentpath: Expected fname as a string." - end - - local path = nmap.fetchfile( fname ) - if not path then - return nil, "Error in get_parentpath: Call to fetchfile() failed." - end - - path = path:sub( 1, path:len() - fname:len() ) - return path - -end - - - ---- --- Given a filepath, checks for the existence of that file. --- @param file Path to a file. --- @return Boolean True if file exists and can be read or false if file does not exist or is empty or cannot be otherwise read. --- @return Nil or error message. No error message if the file is empty or does not exist, only if the file cannot be read for some other reason. - -function file_exists( file ) - - local f, err, _ = io.open( file, "r" ) - if ( f and f:read() ) then - f:close() - return true, nil - elseif f then - f:close() - return false, nil - elseif not f and err:match("No such file or directory") then - return false, nil - elseif err then - return false, err - else - return false, ( "unforseen error while checking " .. file ) - end - -end - - - ---- --- Checks whether a cached file requires updating via HTTP. --- The cached file should contain the following string on the second line: "". --- where timestamp is number of seconds since epoch at the time the file was last cached and --- Last-Modified-Date is an HTTP compliant date sting returned by an HTTP server at the time the file was last cached and --- Entity-Tag is an HTTP Etag returned by an HTTP server at the time the file was last cached. --- @param file Filepath of the cached file. --- @return Boolean False if file does not require updating, true otherwise. --- @return nil or a valid modified-date (string). --- @return nil or a valid entity_tag (string). --- @see file_is_expired - -function requires_updating( file ) - - local last_cached, mod, etag, has_expired - - local f, err, _ = io.open( file, "r" ) - if not f then return true, nil end - - local _ = f:read("*line") - local stamp = f:read("*line") - f:close() - if not stamp then return true, nil end - - last_cached, mod, etag = stamp:match( "^<([^>]*)><([^>]*)>]*)>?$" ) - if (etag == "") then etag = nil end - if not ( last_cached or mod or etag ) then return true, nil end - if not ( - mod:match( "%a%a%a,%s%d%d%s%a%a%a%s%d%d%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) - or - mod:match( "%a*day,%d%d-%a%a%a-%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) - or - mod:match( "%a%a%a%s%a%a%a%s%d?%d%s%d%d:%d%d:%d%d%s%d%d%d%d" ) - ) then - mod = nil - end - if not etag and not mod then - return true, nil - end + } + + --- + -- whoisdb defines the whois services this script is able to query and the script output produced for them. + -- Each entry is a key-value pair where the key is a short name for the service and value is a table of definitions for that service. + -- Note that there is defined here an entry for IANA which does not have a whois service. The entry is defined to allow us to redirect to ARIN when + -- IANA is referred to in a record. + -- + -- Each service defined should contain the following: + -- + -- id: String. Matches the key for the service and is a short name for the service. + -- hostname: String. Hostname of the service. + -- preflag: String. Prepended to the target IP address sent in the whois query. + -- postflag: String. Appended to the target IP address sent in the whois query. + -- longname: Table of strings. Each is a lowercase official (or semi-official) name of the service. + -- fieldreq: Linked table entry. The key identifying a table of a set of objects defined in fields_meta. + -- In its records each whois service displays a particular set of objects as defined here. + -- smallnet_rule: Linked table entry. The key of a pattern for the field defined in fields_meta which captures the Assignment Range. This is an + -- optional entry and is used to extract the smallest (i.e. Most Specific) range from a record when more than one range is detailed. + -- redirects: Table of tables, containing strings. Used to determine whether a record is referring to a different whois service by + -- searching for service specific information in certain fields of the record. + -- Each entry is a table thus: { "search_object", "search_field", "pattern" } + -- search_object: is the key name for a record object defined in fields_meta, in which to search. + -- search_field: is the key name for a field of the object, the data of which to search. + -- pattern: is typically the id or longname key names. + -- In the example: {"ob_org", "orgname", "longname"}, we cycle through each service defined in whoisdb and look for its longname in + -- the ob_org.orgname of the current record. + -- output_short: Table for each object to be displayed when Nmap verbosity is zero. The first element of each table is the object name and the + -- second element is a table of fields to display. The elements of the second may be field names, which are each output to a new + -- line, or tables containing field names which are output to the same line. + -- output_long: Table for each object to be displayed when Nmap verbosity is one or above. The structure is the same as output_short. + -- reg: String name for the field in ob_netnum which captures the Assignment Range (e.g. "netrange", "inetnum"), the data of which is + -- cached in the registry. + -- unordered: Boolean. Optional. True if the records from the service display an object other than ob_netnum as the first in the record (such + -- as at ARIN). This flag is used to decide whether we should extract an object immediately before the relevant ob_netnum object + -- from a record. + + nmap.registry.whois.whoisdb = { + arin = { + id = "arin", + hostname = "whois.arin.net", preflag = "+", postflag = "", + longname = {"american registry for internet numbers"}, + fieldreq = nmap.registry.whois.fields_meta.arin, + smallnet_rule = nmap.registry.whois.fields_meta.arin.ob_netnum.netrange, + redirects = { + {"ob_org", "orgname", "longname"}, + {"ob_org", "orgname", "id"}, + {"ob_org", "orgid", "id"} }, + output_short = { + {"ob_netnum", {"netrange", "netname"}}, + {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}} }, + output_long = { + {"ob_netnum", {"netrange", "netname"}}, + {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}}, + {"ob_cust", {"custname", {"country", "stateprov"}}}, + {"ob_persn", {"orgtechname", "orgtechemail"}} }, + reg = "netrange", + unordered = true + }, + ripe = { + id = "ripe", + hostname = "whois.ripe.net", preflag = "-B", postflag = "", + longname = {"ripe network coordination centre"}, + fieldreq = nmap.registry.whois.fields_meta.rpsl, + smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, + redirects = { + {"ob_role", "role", "longname"}, + {"ob_org", "orgname", "id"}, + {"ob_org", "orgname", "longname"} }, + output_short = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}} }, + output_long = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}}, + {"ob_role", {"role", "email"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + }, + apnic = { + id = "apnic", + hostname = "whois.apnic.net", preflag = "", postflag = "", + longname = {"asia pacific network information centre"}, + fieldreq = nmap.registry.whois.fields_meta.rpsl, + smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, + redirects = { + {"ob_netnum", "netname", "id"}, + {"ob_org", "orgname", "longname"}, + {"ob_role", "role", "longname"}, + {"ob_netnum", "source", "id"} }, + output_short = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}} }, + output_long = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}}, + {"ob_role", {"role", "email"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + }, + lacnic = { + id = "lacnic", + hostname = "whois.lacnic.net", preflag = "", postflag = "", + longname = + {"latin american and caribbean ip address regional registry"}, + fieldreq = nmap.registry.whois.fields_meta.lacnic, + smallnet_rule = nmap.registry.whois.fields_meta.lacnic.ob_netnum.inetnum, + redirects = { + {"ob_netnum", "ownerid", "id"}, + {"ob_netnum", "source", "id"} }, + output_short = { + {"ob_netnum", + {"inetnum", "owner", "ownerid", "responsible", "country"}} }, + output_long = { + {"ob_netnum", + {"inetnum", "owner", "ownerid", "responsible", "country"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + }, + afrinic = { + id = "afrinic", + hostname = "whois.afrinic.net", preflag = "-c", postflag = "", + longname = {"african internet numbers registry", + "african network information center"}, + fieldreq = nmap.registry.whois.fields_meta.rpsl, + smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum, + redirects = { + {"ob_org", "orgname", "longname"} }, + output_short = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}} }, + output_long = { + {"ob_netnum", {"inetnum", "netname", "descr", "country"}}, + {"ob_org", {"orgname", "organisation", "descr", "email"}}, + {"ob_role", {"role", "email"}}, + {"ob_persn", {"person", "email"}} }, + reg = "inetnum" + },--[[ + jpnic = { + id = "jpnic", + hostname = "whois.nic.ad.jp", preflag = "", postflag = "/e", + longname = {"japan network information center"}, + fieldreq = nmap.registry.whois.fields_meta.jpnic, + output_short = { + {"ob_netnum", {"inetnum", "netname", "orgname"}} }, + reg = "inetnum" },--]] + iana = { -- not actually a db but required here + id = "iana", longname = {"internet assigned numbers authority"} + } + } + + nmap.registry.whois.m_none = { + "\n%s*([Nn]o match found for[%s+]*$addr)", + "\n%s*([Uu]nallocated resource:%s*$addr)", + "\n%s*([Rr]eserved:%s*$addr)", + "\n[^\n]*([Nn]ot%s[Aa]ssigned[^\n]*$addr)", + "\n%s*(No match!!)%s*\n", + "(Invalid IP or CIDR block:%s*$addr)" + } + nmap.registry.whois.m_err = { + "\n%s*([Aa]n [Ee]rror [Oo]ccured)%s*\n", + "\n[^\n]*([Ee][Rr][Rr][Oo][Rr][^\n]*)\n" + } + + nmap.registry.whois.remote_assignments_files = {} + nmap.registry.whois.remote_assignments_files.ipv4 = { + { + remote_resource = "http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt", + local_resource = "ipv4-address-space", + match_assignment = "^%s*([%.%d]+/%d+)", + match_service = "whois%.(%w+)%.net" + } + } + nmap.registry.whois.remote_assignments_files.ipv6 = { + --[[{ + remote_resource = "http://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt", + local_resource = "ipv6-address-space", + match_assignment = "^([:%x]+/%d+)", + match_service = "^[:%x]+/%d+%s*(%w+)" + },--]] + { + remote_resource = "http://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.txt", + local_resource = "ipv6-unicast-address-assignments", + match_assignment = "^%s*([:%x]+/%d+)", + match_service = "whois%.(%w+)%.net" + } + } + + local err + + -- get and validate any --script-args + get_args() + + -- mutex for each service + nmap.registry.whois.mutex = {} + for id, v in pairs( nmap.registry.whois.whoisdb ) do + if id ~= "iana" then + nmap.registry.whois.mutex[id] = nmap.mutex(nmap.registry.whois.whoisdb[id]) + end + end + + -- get IANA assignments lists + if nmap.registry.whois.using_local_assignments_file then + nmap.registry.whois.local_assignments_data, err = get_local_assignments_data() + if err then nmap.registry.whois.using_local_assignments_file = false end + end + + nmap.registry.whois.init_done = true + + end + + + + --- + -- Parses the command line arguments passed to the script with --script-args. + -- Sets flags in the registry which threads read to determine certain behaviours. + -- Permitted args are 'nofile' - Prevents use of a list of assignments to determine which service to query, + -- 'nofollow' - Prevents following redirects found in records, + -- 'arin', 'ripe', 'apnic', etc. - Service id's, as defined in the whoisdb table in the registry (see script_init). + + function get_args() + + if not nmap.registry.args then return end + + local args = stdnse.get_script_args('whois.whodb') + + if type( args ) ~= "string" or ( args == "" ) then return end + + local t = {} + -- match words in args which may be whois dbs or other arguments + for db in string.gmatch( args, "%w+" ) do + if not nmap.registry.whois.whoisdb[db] then + if ( db == "nofollow" ) then + nmap.registry.whois.nofollow = true + elseif ( db == "nocache" ) then + nmap.registry.whois.using_cache = false + elseif ( db == "nofile" ) then + nmap.registry.whois.using_local_assignments_file = false + stdnse.print_debug( 2, "%s: Not using local assignments data.", SCRIPT_NAME ) + end + elseif not ( string.match( table.concat( t, " " ), db ) ) then + -- we have a unique valid whois db + t[#t+1] = db + end + end + + if ( #t > 0 ) and nmap.registry.whois.using_local_assignments_file then + -- "nofile" was not explicitly supplied, but it is implied by supplying custom whoisdb_default_order + nmap.registry.whois.using_local_assignments_file = false + stdnse.print_debug(3, "%s: Not using local assignments data because custom whoisdb_default_order was supplied.", SCRIPT_NAME) + end + + if ( #t > 1 ) and nmap.registry.whois.nofollow then + -- using nofollow, we do not follow redirects and can only accept what we find as a record therefore we only accept the first db supplied + t = {t[1]} + stdnse.print_debug( 1, "%s: Too many args supplied with 'nofollow', only using %s.", SCRIPT_NAME, t[1] ) + end + + if ( #t > 0 ) then + nmap.registry.whois.whoisdb_default_order = t + stdnse.print_debug( 2, "%s: whoisdb_default_order: %s.", SCRIPT_NAME, table.concat( t, " " ) ) + end + + end + + + + --- + -- Makes IANA hosted assignments data available for lookups against that data. In more detail it: + -- Caches a local copy of remote assignments data if copies do not currently exist or are out-of-date. + -- Checks whether the cached copies require updating and performs update as required. + -- Parses the cached copies and populates a table of lookup data which is returned to the caller. + -- Sets a flag in the registry to prevent use of the lookup data in the event of an error. + -- @return Table of lookup data (or nil in case of an error). + -- @return Nil or error message in case of an error. + -- @see get_parentpath, file_exists, requires_updating, read_from_file, conditional_download, + -- write_to_file, parse_assignments + + function get_local_assignments_data() + + if not next( nmap.registry.whois.remote_assignments_files ) then + nmap.registry.whois.using_local_assignments_file = false + return nil, "Error in get_local_assignments_data: Remote resources not defined in remote_assignments_files registry key" + end + + -- get the directory path where cached files will be stored. + local fetchfile = "nmap-services" + local directory_path, err = get_parentpath( fetchfile ) + if err then + stdnse.print_debug( 1, "%s: Nmap.fetchfile() failed to get a path to %s: %s.", SCRIPT_NAME, fetchfile, err ) + return nil, err + end + + local ret = {} + + -- cache or update and parse each remote file for each address family + for address_family, t in pairs( nmap.registry.whois.remote_assignments_files ) do + for i, assignment_data_spec in ipairs( t ) do + + local update_required, modified_date, entity_tag, err + + -- do we have a cached file and does it need updating? + local file, exists = directory_path .. assignment_data_spec.local_resource + exists, err = file_exists( file ) + if not exists and err then + stdnse.print_debug( 1, "%s: Error accessing %s: %s.", SCRIPT_NAME, file, err ) + elseif not exists then + update_required = true + stdnse.print_debug( 2, "%s: %s does not exist or is empty. Fetching it now...", SCRIPT_NAME, file ) + elseif exists then + update_required, modified_date, entity_tag = requires_updating( file ) + end + + local file_content + + -- read an existing and up-to-date file into file_content. + if exists and not update_required then + stdnse.print_debug( 2, "%s: %s was cached less than %s ago. Reading...", SCRIPT_NAME, file, nmap.registry.whois.local_assignments_file_expiry ) + file_content = read_from_file( file ) + end + + -- cache or update and then read into file_content + local http_response, write_success + if update_required then + http_response = ( conditional_download( assignment_data_spec.remote_resource, modified_date, entity_tag ) ) + if not http_response or type( http_response.status ) ~= "number" then + stdnse.print_debug( 1, "%s: Failed whilst requesting %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) + elseif http_response.status == 200 then + -- prepend our file header + stdnse.print_debug( 2, "%s: Retrieved %s.", SCRIPT_NAME, assignment_data_spec.remote_resource ) + file_content = stdnse.strsplit( "\r?\n", http_response.body ) + table.insert( file_content, 1, "** Do Not Alter This Line or The Following Line **" ) + local hline = {} + hline[#hline+1] = "<" .. os.time() .. ">" + hline[#hline+1] = "<" .. http_response.header["last-modified"] .. ">" + if http_response.header.etag then + hline[#hline+1] = "<" .. http_response.header.etag .. ">" + end + table.insert( file_content, 2, table.concat( hline ) ) + write_success, err = write_to_file( file, file_content ) + if err then + stdnse.print_debug( 1, "%s: Error writing %s to %s: %s.", SCRIPT_NAME, assignment_data_spec.remote_resource, file, err ) + end + elseif http_response.status == 304 then + -- update our file header with a new timestamp + stdnse.print_debug( 1, "%s: %s is up-to-date.", SCRIPT_NAME, file ) + file_content = read_from_file( file ) + file_content[2] = file_content[2]:gsub("^<[-+]?%d+>(.*)$", "<" .. os.time() .. ">%1") + write_success, err = write_to_file( file, file_content ) + if err then + stdnse.print_debug( 1, "%s: Error writing to %s: %s.", SCRIPT_NAME, file, err ) + end + else + stdnse.print_debug( 1, "%s: HTTP %s whilst requesting %s.", SCRIPT_NAME, http_response.status, assignment_data_spec.remote_resource ) + end + end + + + if file_content then + -- Create a table for this address family (if there isn't one already). + if not ret[address_family] then ret[address_family] = {} end + -- Parse data and add to the table for this address family. + local t + t, err = parse_assignments( assignment_data_spec, file_content ) + if #t == 0 or err then + -- good header, but bad file? Kill the file! + write_to_file( file, "" ) + stdnse.print_debug( 1, "%s: Problem with the data in %s.", SCRIPT_NAME, file ) + else + for i, v in pairs( t ) do + ret[address_family][#ret[address_family]+1] = v + end + end + end + + end -- file + end -- af + + -- If we decide to use more than one assignments file for ipv6 we may need to sort the resultant parsed list so that sub-assignments appear + -- before their parent. This is expensive, but it's worth doing to ensure the lookup process returns the correct service. + -- table.sort( ret.ipv6, sort_assignments ) + + -- final check for an empty table which we'll convert to nil + for af, t in pairs( ret ) do + if #t == 0 then + ret[af] = nil + stdnse.print_debug( 1, "%s: Cannot use local assignments file for address family %s.", SCRIPT_NAME, af ) + end + end + + return ret + + end + + + + --- + -- Uses nmap.fetchfile to get the path of the parent directory of the supplied Nmap datafile SCRIPT_NAME. + -- @param fname String - Filename of an Nmap datafile. + -- @return String - The filepath of the directory containing the supplied SCRIPT_NAME including the trailing slash (or nil in case of an error). + -- @return Nil or error message in case of an error. + + function get_parentpath( fname ) + + if type( fname ) ~= "string" or fname == "" then + return nil, "Error in get_parentpath: Expected fname as a string." + end + + local path = nmap.fetchfile( fname ) + if not path then + return nil, "Error in get_parentpath: Call to fetchfile() failed." + end + + path = path:sub( 1, path:len() - fname:len() ) + return path + + end + + + + --- + -- Given a filepath, checks for the existence of that file. + -- @param file Path to a file. + -- @return Boolean True if file exists and can be read or false if file does not exist or is empty or cannot be otherwise read. + -- @return Nil or error message. No error message if the file is empty or does not exist, only if the file cannot be read for some other reason. + + function file_exists( file ) + + local f, err, _ = io.open( file, "r" ) + if ( f and f:read() ) then + f:close() + return true, nil + elseif f then + f:close() + return false, nil + elseif not f and err:match("No such file or directory") then + return false, nil + elseif err then + return false, err + else + return false, ( "unforseen error while checking " .. file ) + end + + end + + + + --- + -- Checks whether a cached file requires updating via HTTP. + -- The cached file should contain the following string on the second line: "". + -- where timestamp is number of seconds since epoch at the time the file was last cached and + -- Last-Modified-Date is an HTTP compliant date sting returned by an HTTP server at the time the file was last cached and + -- Entity-Tag is an HTTP Etag returned by an HTTP server at the time the file was last cached. + -- @param file Filepath of the cached file. + -- @return Boolean False if file does not require updating, true otherwise. + -- @return nil or a valid modified-date (string). + -- @return nil or a valid entity_tag (string). + -- @see file_is_expired + + function requires_updating( file ) + + local last_cached, mod, etag, has_expired + + local f, err, _ = io.open( file, "r" ) + if not f then return true, nil end + + local _ = f:read("*line") + local stamp = f:read("*line") + f:close() + if not stamp then return true, nil end + + last_cached, mod, etag = stamp:match( "^<([^>]*)><([^>]*)>]*)>?$" ) + if (etag == "") then etag = nil end + if not ( last_cached or mod or etag ) then return true, nil end + if not ( + mod:match( "%a%a%a,%s%d%d%s%a%a%a%s%d%d%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) + or + mod:match( "%a*day,%d%d-%a%a%a-%d%d%s%d%d:%d%d:%d%d%s%u%u%u" ) + or + mod:match( "%a%a%a%s%a%a%a%s%d?%d%s%d%d:%d%d:%d%d%s%d%d%d%d" ) + ) then + mod = nil + end + if not etag and not mod then + return true, nil + end - -- Check whether the file was cached within local_assignments_file_expiry (registry value) - has_expired = file_is_expired( last_cached ) + -- Check whether the file was cached within local_assignments_file_expiry (registry value) + has_expired = file_is_expired( last_cached ) - return has_expired, mod, etag + return has_expired, mod, etag -end + end ---- --- Reads a file, line by line, into a table. --- @param file String representing a filepath. --- @return Table (array-style) of lines read from the file (or nil in case of an error). --- @return Nil or error message in case of an error. + --- + -- Reads a file, line by line, into a table. + -- @param file String representing a filepath. + -- @return Table (array-style) of lines read from the file (or nil in case of an error). + -- @return Nil or error message in case of an error. -function read_from_file( file ) - - if type( file ) ~= "string" or file == "" then - return nil, "Error in read_from_file: Expected file as a string." - end + function read_from_file( file ) + + if type( file ) ~= "string" or file == "" then + return nil, "Error in read_from_file: Expected file as a string." + end - local f, err, _ = io.open( file, "r" ) - if not f then - stdnse.print_debug( 1, "%s: Error opening %s for reading: %s", SCRIPT_NAME, file, err ) - return nil, err - end + local f, err, _ = io.open( file, "r" ) + if not f then + stdnse.print_debug( 1, "%s: Error opening %s for reading: %s", SCRIPT_NAME, file, err ) + return nil, err + end - local line, ret = nil, {} - while true do - line = f:read() - if not line then break end - ret[#ret+1] = line - end - - f:close() + local line, ret = nil, {} + while true do + line = f:read() + if not line then break end + ret[#ret+1] = line + end + + f:close() - return ret + return ret -end + end ---- --- Performs either an HTTP Conditional GET request if mod_date or e_tag is passed, or a plain GET request otherwise. --- Will follow a single redirect for the remote resource. --- @param url String representing the full URL of the remote resource. --- @param mod_date String representing an HTTP date. --- @param e_tag String representing an HTTP entity tag. --- @return Table as per http.request or nil in case of a non-HTTP error. --- @return Nil or error message in case of an error. --- @see http.request - -function conditional_download( url, mod_date, e_tag ) + --- + -- Performs either an HTTP Conditional GET request if mod_date or e_tag is passed, or a plain GET request otherwise. + -- Will follow a single redirect for the remote resource. + -- @param url String representing the full URL of the remote resource. + -- @param mod_date String representing an HTTP date. + -- @param e_tag String representing an HTTP entity tag. + -- @return Table as per http.request or nil in case of a non-HTTP error. + -- @return Nil or error message in case of an error. + -- @see http.request + + function conditional_download( url, mod_date, e_tag ) - if type( url ) ~= "string" or url == "" then - return nil, "Error in conditional_download: Expected url as a string." - end + if type( url ) ~= "string" or url == "" then + return nil, "Error in conditional_download: Expected url as a string." + end - -- mod_date and e_tag allowed to be nil or a non-empty string - if mod_date and ( type( mod_date ) ~= "string" or mod_date == "" ) then - return nil, "Error in conditional_download: Expected mod_date as nil or as a non-empty string." - end - if e_tag and ( type( e_tag ) ~= "string" or e_tag == "" ) then - return nil, "Error in conditional_download: Expected e_tag as nil or as a non-empty string." - end + -- mod_date and e_tag allowed to be nil or a non-empty string + if mod_date and ( type( mod_date ) ~= "string" or mod_date == "" ) then + return nil, "Error in conditional_download: Expected mod_date as nil or as a non-empty string." + end + if e_tag and ( type( e_tag ) ~= "string" or e_tag == "" ) then + return nil, "Error in conditional_download: Expected e_tag as nil or as a non-empty string." + end - -- use e_tag in preference to mod_date - local request_options = {} - request_options.header = {} - if e_tag then - request_options.header["If-None-Match"] = e_tag - elseif mod_date then - request_options.header["If-Modified-Since"] = mod_date - end - if not next( request_options.header ) then request_options = nil end + -- use e_tag in preference to mod_date + local request_options = {} + request_options.header = {} + if e_tag then + request_options.header["If-None-Match"] = e_tag + elseif mod_date then + request_options.header["If-Modified-Since"] = mod_date + end + if not next( request_options.header ) then request_options = nil end - local request_response = http.get_url( url, request_options ) + local request_response = http.get_url( url, request_options ) - -- follow one redirection - if request_response.status ~= 304 and ( tostring( request_response.status ):match( "30%d" ) and - type( request_response.header.location ) == "string" and request_response.header.location ~= "" ) then - stdnse.print_debug( 2, "%s: HTTP Status:%d New Location: %s.", SCRIPT_NAME, request_response.status, request_response.header.location ) - request_response = http.get_url( request_response.header.location, request_options ) - end + -- follow one redirection + if request_response.status ~= 304 and ( tostring( request_response.status ):match( "30%d" ) and + type( request_response.header.location ) == "string" and request_response.header.location ~= "" ) then + stdnse.print_debug( 2, "%s: HTTP Status:%d New Location: %s.", SCRIPT_NAME, request_response.status, request_response.header.location ) + request_response = http.get_url( request_response.header.location, request_options ) + end - return request_response + return request_response -end + end ---- --- Writes the supplied content to file. --- @param file String representing a filepath (if it exists it will be overwritten). --- @param content String or table of data to write to file. Empty string or table is permitted. --- A table will be written to file with each element of the table on a new line. --- @return Boolean True on success or nil in case of an error. --- @return Nil or error message in case of an error. + --- + -- Writes the supplied content to file. + -- @param file String representing a filepath (if it exists it will be overwritten). + -- @param content String or table of data to write to file. Empty string or table is permitted. + -- A table will be written to file with each element of the table on a new line. + -- @return Boolean True on success or nil in case of an error. + -- @return Nil or error message in case of an error. -function write_to_file( file, content ) + function write_to_file( file, content ) - if type( file ) ~= "string" or file == "" then - return nil, "Error in write_to_file: Expected file as a string." - end - if type( content ) ~= "string" and type( content ) ~= "table" then - return nil, "Error in write_to_file: Expected content as a table or string." - end + if type( file ) ~= "string" or file == "" then + return nil, "Error in write_to_file: Expected file as a string." + end + if type( content ) ~= "string" and type( content ) ~= "table" then + return nil, "Error in write_to_file: Expected content as a table or string." + end - local f, err, _ = io.open( file, "w" ) - if not f then - stdnse.print_debug( 1, "%s: Error opening %s for writing: %s.", SCRIPT_NAME, file, err ) - return nil, err - end + local f, err, _ = io.open( file, "w" ) + if not f then + stdnse.print_debug( 1, "%s: Error opening %s for writing: %s.", SCRIPT_NAME, file, err ) + return nil, err + end - if ( type( content ) == "table" ) then - content = table.concat( content, "\n" ) or "" - end - f:write( content ) + if ( type( content ) == "table" ) then + content = table.concat( content, "\n" ) or "" + end + f:write( content ) - f:close() + f:close() - return true + return true -end + end ---- --- Converts raw data from an assignments file into a form optimised for lookups against that data. --- @param address_family_spec Table (assoc. array) containing patterns for extracting data. --- @param table_of_lines Table containing a line of data per table element. --- @return Table - each element of the form { range = { first = data, last = data }, service = data } (or nil in case of an error). --- @return Nil or error message in case of an error. + --- + -- Converts raw data from an assignments file into a form optimised for lookups against that data. + -- @param address_family_spec Table (assoc. array) containing patterns for extracting data. + -- @param table_of_lines Table containing a line of data per table element. + -- @return Table - each element of the form { range = { first = data, last = data }, service = data } (or nil in case of an error). + -- @return Nil or error message in case of an error. -function parse_assignments( address_family_spec, table_of_lines ) + function parse_assignments( address_family_spec, table_of_lines ) - if #table_of_lines < 1 then - return nil, "Error in parse_assignments: Expected table_of_lines as a non-empty table." - end + if #table_of_lines < 1 then + return nil, "Error in parse_assignments: Expected table_of_lines as a non-empty table." + end - local mnetwork = address_family_spec.match_assignment - local mservice = address_family_spec.match_service + local mnetwork = address_family_spec.match_assignment + local mservice = address_family_spec.match_service - local ret, net, svc = {} + local ret, net, svc = {} - for i, line in ipairs( table_of_lines ) do + for i, line in ipairs( table_of_lines ) do - net = line:match( mnetwork ) - if net then - svc = line:match( mservice ) - if svc then svc = string.lower( svc ) end - if not svc or ( svc == "iana" ) then - svc = "arin" - elseif not nmap.registry.whois.whoisdb[svc] then - svc = "arin" - end - -- optimise the data - local first_ip, last_ip, err = ipOps.get_ips_from_range( net ) - if not err then - local t = { first = first_ip, last = last_ip } - ret[#ret+1] = { range = t, service = svc } - end - end + net = line:match( mnetwork ) + if net then + svc = line:match( mservice ) + if svc then svc = string.lower( svc ) end + if not svc or ( svc == "iana" ) then + svc = "arin" + elseif not nmap.registry.whois.whoisdb[svc] then + svc = "arin" + end + -- optimise the data + local first_ip, last_ip, err = ipOps.get_ips_from_range( net ) + if not err then + local t = { first = first_ip, last = last_ip } + ret[#ret+1] = { range = t, service = svc } + end + end - end + end - return ret + return ret -end + end ---- --- Checks the age of the supplied timestamp and compares it to the value of local_assignments_file_expiry. --- @param time_string String representing a timestamp (seconds since epoch). --- @return Boolean True if the period elapsed since the timestamp is longer than the value of local_assignments_file_expiry --- also returns true if the parameter is not of the expected type, otherwise returns false. --- @see sane_expiry_period + --- + -- Checks the age of the supplied timestamp and compares it to the value of local_assignments_file_expiry. + -- @param time_string String representing a timestamp (seconds since epoch). + -- @return Boolean True if the period elapsed since the timestamp is longer than the value of local_assignments_file_expiry + -- also returns true if the parameter is not of the expected type, otherwise returns false. + -- @see sane_expiry_period -function file_is_expired( time_string ) + function file_is_expired( time_string ) - if type( time_string ) ~= "string" or time_string == "" then return true end - local allowed_age = nmap.registry.whois.local_assignments_file_expiry - if allowed_age == "" then return true end + if type( time_string ) ~= "string" or time_string == "" then return true end + local allowed_age = nmap.registry.whois.local_assignments_file_expiry + if allowed_age == "" then return true end - local cached_time = tonumber(time_string) - if not cached_time then return true end + local cached_time = tonumber(time_string) + if not cached_time then return true end - local now_time = os.time() - if now_time < cached_time then return true end - if now_time > ( cached_time + sane_expiry_period( allowed_age ) ) then return true end + local now_time = os.time() + if now_time < cached_time then return true end + if now_time > ( cached_time + sane_expiry_period( allowed_age ) ) then return true end - return false + return false -end + end ---- --- Checks that the supplied string represents a period of time between 0 and 7 days. --- @param period String representing a period. --- @return Number representing the supplied period or a failsafe period in whole seconds. --- @see get_period + --- + -- Checks that the supplied string represents a period of time between 0 and 7 days. + -- @param period String representing a period. + -- @return Number representing the supplied period or a failsafe period in whole seconds. + -- @see get_period -function sane_expiry_period( period ) + function sane_expiry_period( period ) - local sane_default_expiry = 57600 -- 16h - local max_expiry = 604800 -- 7d + local sane_default_expiry = 57600 -- 16h + local max_expiry = 604800 -- 7d - period = get_period( period ) - if not period or ( period == "" ) then return sane_default_expiry end + period = get_period( period ) + if not period or ( period == "" ) then return sane_default_expiry end - if period < max_expiry then return period end - return max_expiry + if period < max_expiry then return period end + return max_expiry -end + end ---- --- Converts a string representing a period of time made up of a quantity and a unit such as "24h" --- into whole seconds. --- @param period String combining a quantity and a unit of time. --- Acceptable units are days (D or d), hours (H or h), minutes (M or m) and seconds (S or s). --- If a unit is not supplied or not one of the above acceptable units, it is assumed to be seconds. --- Negative or fractional periods are permitted. --- @return Number representing the supplied period in whole seconds (or nil in case of an error). + --- + -- Converts a string representing a period of time made up of a quantity and a unit such as "24h" + -- into whole seconds. + -- @param period String combining a quantity and a unit of time. + -- Acceptable units are days (D or d), hours (H or h), minutes (M or m) and seconds (S or s). + -- If a unit is not supplied or not one of the above acceptable units, it is assumed to be seconds. + -- Negative or fractional periods are permitted. + -- @return Number representing the supplied period in whole seconds (or nil in case of an error). -function get_period( period ) + function get_period( period ) - if type( period ) ~= string or ( period == "" ) then return nil end - local quant, unit = period:match( "(-?+?%d*%.?%d*)([SsMmHhDd]?)" ) - if not ( tonumber( quant ) ) then return nil end + if type( period ) ~= string or ( period == "" ) then return nil end + local quant, unit = period:match( "(-?+?%d*%.?%d*)([SsMmHhDd]?)" ) + if not ( tonumber( quant ) ) then return nil end - if ( string.lower( unit ) == "m" ) then - unit = 60 - elseif ( string.lower( unit ) == "h" ) then - unit = 3600 - elseif ( string.lower( unit ) == "d" ) then - unit = 86400 - else - -- seconds and catch all - unit = 1 - end + if ( string.lower( unit ) == "m" ) then + unit = 60 + elseif ( string.lower( unit ) == "h" ) then + unit = 3600 + elseif ( string.lower( unit ) == "d" ) then + unit = 86400 + else + -- seconds and catch all + unit = 1 + end - return ( math.modf( quant * unit ) ) + return ( math.modf( quant * unit ) ) -end + end --- --- Passed to table.sort, will sort a table of IP assignments such that sub-assignments appear before their parent. --- This function is not in use at the moment (see get_local_assignments_data) and will not appear in nse documentation. --- @param first Table { range = { first = IP_addr, last = IP_addr } } --- @param second Table { range = { first = IP_addr, last = IP_addr } } --- @return Boolean True if the tables are already in the correct order, otherwise false. + -- + -- Passed to table.sort, will sort a table of IP assignments such that sub-assignments appear before their parent. + -- This function is not in use at the moment (see get_local_assignments_data) and will not appear in nse documentation. + -- @param first Table { range = { first = IP_addr, last = IP_addr } } + -- @param second Table { range = { first = IP_addr, last = IP_addr } } + -- @return Boolean True if the tables are already in the correct order, otherwise false. -function sort_assignments( first, second ) + function sort_assignments( first, second ) - local f_lo, f_hi = first.range.first, first.range.last - local s_lo, s_hi = second.range.first, second.range.last + local f_lo, f_hi = first.range.first, first.range.last + local s_lo, s_hi = second.range.first, second.range.last - if ipOps.compare_ip( f_lo, "gt", s_lo ) then return false end - if ipOps.compare_ip( f_lo, "le", s_lo ) and ipOps.compare_ip( f_hi, "ge", s_hi ) then - return false - end + if ipOps.compare_ip( f_lo, "gt", s_lo ) then return false end + if ipOps.compare_ip( f_lo, "le", s_lo ) and ipOps.compare_ip( f_hi, "ge", s_hi ) then + return false + end - return true + return true -end + end