
Nmap Development mailing list archives
[NSE] ipOps reloaded
From: jah <jah () zadkiel plus com>
Date: Thu, 17 Jul 2008 02:41:59 +0100
Greetings, Attached is ipOps.lua which a) updates the presently available functions to work with IPv6 addresses where possible b) adds new functions (some of which are based on Perl's Net::IP) Some changes to functionality: The todword() function has not been updated to work with IPv6 due to the limitation on numbers in NSE (and Lua without BigNum support) - it will, however, reject IPv6 addresses and return an error. get_parts_as_number() previously returned four variables - one for each part of an IPv4 address. It now returns a table of numbers instead. I decided on the table because it makes it easier to return an error, regardless of whether the address is IPv4 or 6, rather than returning nil, nil, nil, nil, err or nil, nil, nil, nil, nil, nil, nil, nil, err depending on the address family. No scripts currently distributed with Nmap make use of this function. isPrivate() previously checked whether the supplied IP address was in the 10/8, 172.15/12 or 192.168/16 ranges. I've added: 127/8 and 169.254/16 as well as IPv6 ::/127, FC00::/7 and FE80::/10. These are all, I believe, not internet-routable. Do say, if there are any disagreements to any of the above. Here's a summary: ipOps.isPrivate( ip ) Is 192.168.1.1 private... true Is 45.67.89.0 private... false Is FE80:: private... true ipOps.todword( ip ) 0 => 0 255.255.255.255 => 4294967295 ipOps.get_parts_as_number( ip ) 127 => 127 0 0 0 DEAD:BEEF::FFFF => 57005 48879 0 0 0 0 0 65535 ipOps.compare_ip( left, op, right ) - Compares two IP addresses (from the same address family). 187 > 186.255.255.255 true FE80::1:0 <= FE80::1 false 2000::139.104.75.40 == 2000::8b68:4b28 true ipOps.ip_in_range( ip, range ) - Checks whether the supplied IP address is within the supplied Range of IP addresses if they belong to the same address family. Is 192.168.1.1 in the range 193/8 ? false Is 2000:: in the range ::/0 ? true ipOps.expand_ip( ip ) - Expands an IP address supplied in shortened notation. 212 => 212.0.0.0 :: => 0:0:0:0:0:0:0:0 1::212.0.0.0 => 1:0:0:0:0:0:d400:0 1::212. => 1:0:0:0:0:0:d400:0 1::212 => 1:0:0:0:0:0:0:212 ipOps.get_ips_from_range( range ) - Returns the first and last IP addresses in the supplied range of addresses. 0/0 => 0.0.0.0 255.255.255.255 ::/0 => 0:0:0:0:0:0:0:0 ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 192.168.0.0 - 192.168.255.255 => 192.168.0.0 192.168.255.255 ipOps.get_last_ip( ip, prefix ) - Calculates the last IP address of a range of addresses given: an IP address in; and prefix length for; that range. 192.168.1.0/24 => 192.168.1.255 2002:0000::/16 => 2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff ipOps.ip_to_bin( ip ) - Converts an IP address into a string representing the address as binary digits. 1.3.7.15 => 00000001000000110000011100001111 ipOps.bin_to_ip( binstring ) - Converts a string representing binary digits into an IP address. 11111110111111001111100011110000 => 254.252.248.240 ipOps.hex_to_bin( hex ) - Converts a string representing a hexadecimal number into a string representing that number as binary digits. 0123456789abcdef => 0000000100100011010001010110011110001001101010111100110111101111 There's lots of checking for sane parameters so this stuff should be fine against untrusted input, but I'd love to hear if anyone manages to break something. I've added NSEdoc tags to the functions in the library, but I haven't been able to get nsedoc working on Windows as yet and can't vouch for their well formedness. Regards, jah
-- See Nmap's COPYING for licence module(...,package.seeall) local stdnse = require "stdnse" --- 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. --@return true or false -- In case of an error returns nil, err. isPrivate = function( ip ) ip, err = expand_ip( ip ) if err then return nil, err end 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 for _, range in ipairs( t ) do is_private, err = ip_in_range( ip, range ) -- break 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 --- Converts the supplied IPv4 address into a DWORD value -- (i.e. the address <a.b.c.d> becomes (((a*256+b)*256+c)*256+d) ). -- Note: Currently, numbers in NSE are limited to (10 ^ 14) therefore not all IPv6 addresses can be represented in base 10. --@param ip: String representing an IPv4 address. Shortened notation is permitted. --@return DWORD value corresponding to the supplied IP address. -- In case of an error returns nil, err. todword = function( ip ) if type( ip ) ~= "string" or ip:match( ":" ) then return nil, "Error in ipOps.todword: Expected IPv4 address." end local n, ret = {} n, err = get_parts_as_number( ip ) if err then return nil, err end ret = (((n[1]*256+n[2]))*256+n[3])*256+n[4] return ret end --- Separates the supplied IP address into its constituent parts and returns them as a table of decimal numbers -- (e.g. the address 139.104.32.123 becomes { 139, 104, 32, 123 } ) --@usage local a, b, c, d -- local t, err = get_parts_as_number( ip_address_string ) -- if t then a, b, c, d = t[1], t[2], t[3], t[4] end --@param ip: String representing an IPv4 or IPv6 address. Shortened notation is permitted. --@return Array-style Table containing decimal numbers for each part of the supplied IP address (i.e. 4 or 8 parts). -- In case of an error returns nil, err. get_parts_as_number = function( ip ) ip, err = expand_ip( ip ) if err then return nil, err end local pattern, base if ip:match( ":" ) then pattern = "%x+" base = 16 else pattern = "%d+" base = 10 end local t = {} for part in string.gmatch(ip, pattern) do t[#t+1] = tonumber( part, base ) end return t end --- 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. --@return true or false -- In case of an error returns nil, err. compare_ip = function( left, op, right ) if type( left ) ~= "string" or type( right ) ~= "string" then return nil, "Error in ipOps.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 ipOps.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 ipOps.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 ipOps.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 --- 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"). --@return true or false -- In case of an error returns nil, err. 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 ipOps.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 --- Expands an IP address supplied in shortened notation. -- Serves also to check the well-formedness of an IP address. -- Note: IPv4to6 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. --@return String representing a fully expanded IPv4 or IPv6 address. -- In case of an error returns nil, err. expand_ip = function( ip ) if type( ip ) ~= "string" then return nil, "Error in ipOps.expand_ip: Expected IP address as a string." end local err4 = "Error in ipOps.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 ipv4to6 local t = {} if hexadectets[#hexadectets]:match( "[\.]+" ) then hexadectets[#hexadectets], err = expand_ip( hexadectets[#hexadectets] ) if err then return nil, ( err:gsub( "IPv4", "IPv4to6" ) ) 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, ":" ) ) 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. --@return Two strings representing the first and last addresses in the supplied range. -- In case of an error returns nil, nil, err. get_ips_from_range = function( range ) if type( range ) ~= "string" then return nil, nil, "Error in ipOps.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 ipOps.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 ipOps.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; 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. --@return String representing the last IP address of the range denoted by the supplied parameters. -- In case of an error returns nil, err. 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 ipOps.get_last_ip: Invalid prefix length." end local hostbits = string.sub( first, prefix + 1 ) hostbits = string.gsub( hostbits, "0", "1" ) 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. --@return String representing the supplied IP address as 32 or 128 binary digits. -- In case of an error returns nil, err. 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. --@return String representing an IP address. -- In case of an error returns nil, err. bin_to_ip = function( binstring ) if type( binstring ) ~= "string" or binstring:match( "[^01]+" ) then return nil, "Error in ipOps.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 ipOps.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. --@return String representing the supplied number in binary digits. -- In case of an error, returns nil, err. hex_to_bin = function( hex ) if type( hex ) ~= "string" or hex:match( "[^%x]+" ) then return nil, "Error in ipOps.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
_______________________________________________ Sent through the nmap-dev mailing list http://cgi.insecure.org/mailman/listinfo/nmap-dev Archived at http://SecLists.Org
Current thread:
- [NSE] ipOps reloaded jah (Jul 16)
- Re: [NSE] ipOps reloaded jah (Aug 03)