Index: target.lua =================================================================== --- target.lua (revision 29646) +++ target.lua (working copy) @@ -12,18 +12,26 @@ -- @args newtargets If specified, lets NSE scripts add new targets. -- @args max-newtargets Sets the number of the maximum allowed -- new targets. If set to 0 or less then there --- is no limit. The default value is 0. +-- is no limit. The default value is 0. Unlike +-- previous versions, max-newtargets will count +-- IP ranges as multiple targets. -local nmap = require "nmap" -local stdnse = require "stdnse" -local table = require "table" local type = type local select = select +local string = string +local table = table +local ipairs = ipairs +local pairs = pairs +local unpack = unpack local tonumber = tonumber +local tostring = tostring +local stdnse = require "stdnse" +local nmap = require "nmap" +local ipOps = require "ipOps" + _ENV = stdnse.module("target", stdnse.seeall) - -- This is a special variable and it is a global one, so -- scripts can check it to see if adding targets is allowed, -- before calling target.add() function. @@ -31,8 +39,10 @@ -- 'newtargets' was specified. ALLOW_NEW_TARGETS = false -local newtargets, max_newtargets = stdnse.get_script_args("newtargets", - "max-newtargets") +-- Global database of new targets +TARGET_DB = {} + +local newtargets, max_newtargets = stdnse.get_script_args("newtargets", "max-newtargets") if newtargets then ALLOW_NEW_TARGETS = true end @@ -43,19 +53,99 @@ max_newtargets = 0 end +local pushed_targets = 0 + +--- Local function to clean up IP strings for TARGET_DB storage/comparisons +local transform_ip = function( ip ) + ip = ip:upper() -- forgo case-sensitivity + + if ip:match('^[%d%.:/%-]+$') then + if ip:match('/') or ip:match('-') then + local first, last = ipOps.get_ips_from_range(ip) + + if first == nil then + return ip, -1 -- match only + else + if first == last then return first, 1 end -- single IP + return ip, 2 -- range + end + + else + local eIP = ipOps.expand_ip(ip) + if eIP == nil then return ip, -1 end -- match only + return ip, 1 -- single IP + end + end + + return ip, -1 -- match only +end + --- Local function to calculate max allowed new targets local calc_max_targets = function(targets) + targets.count = ipOps.count_ips(targets) + if max_newtargets > 0 then - local pushed_targets = nmap.new_targets_num() if pushed_targets >= max_newtargets then - return 0 - elseif (targets + pushed_targets) > max_newtargets then - return (max_newtargets - pushed_targets) + return { count = 0 } + elseif (targets.count + pushed_targets) > max_newtargets then + -- need to do some individual checks + local can_add = (max_newtargets - pushed_targets) + local new_targets = {} + new_targets.count = 0 + + for i,ip in ipairs(targets) do + local new = ipOps.count_ips(ip) + if (new_targets.count + new) >= can_add then + new_targets.count = new_targets.count + new + new_targets:insert(ip) + end + if new_targets.count == can_add then break end + end end end + return targets end +--- Local function to find any dupes within the TARGET_DB +local check_for_dupes = function(targets) + for i,sIP in ipairs(targets) do + local sIP, sType = transform_ip(sIP) + targets[i] = sIP + + -- Try a direct match first + if TARGET_DB[sIP] then + table.remove(targets, i) + i = i-1 + elseif sType ~= -1 then -- (don't waste time on search with domain names or non-IPs) + for dIP,dType in pairs(TARGET_DB) do + + if not (sType == 1 and dType == 1) and dType ~= -1 then -- bypass IP->IP and domains + local ranges = ipOps.subtract_range_from_range(sIP, dIP) + + -- destination swallowed up source + if ranges == nil or #ranges == 0 then + table.remove(targets, i) + i = i-1 + break + -- if source isn't in destination, don't work on splitting + elseif not (#ranges == 1 and ranges[1] == sIP) then + -- some sort of split range; remove this one and add the new split + -- at the end, so that each split still goes through checks + for _,item in pairs(ranges) do table.insert(targets, item) end + table.remove(targets, i) + i = i-1 + break + end + end + + end + end + end + + return targets +end + --- Adds the passed arguments to the Nmap scan queue. -- -- Only prerule, portrule and hostrule scripts can add new targets. @@ -87,29 +177,124 @@ return false, "to add targets run with --script-args 'newtargets'" end - local new_targets = {count = select("#", ...), ...} + local new_targets = {...} -- function called without arguments - if new_targets.count == 0 then + if #new_targets == 0 then return true, nmap.add_targets() end + + -- check for duplicate targets + new_targets = check_for_dupes(new_targets) + if #new_targets == 0 then + stdnse.print_debug(3, + "Warning: All targets were already added previously.") + return false, "All targets were already added previously." + end - new_targets.count = calc_max_targets(new_targets.count) - + new_targets = calc_max_targets(new_targets) if new_targets.count == 0 then stdnse.print_debug(3, "Warning: Maximum new targets reached, no more new targets.") return false, "Maximum new targets reached, no more new targets." end + + local hosts, err = nmap.add_targets(table.unpack(new_targets)) - local hosts, err = nmap.add_targets(table.unpack(new_targets,1,new_targets.count)) - if hosts == 0 then stdnse.print_debug(3, "%s", err) return false, err end + + -- add to TARGET_DB + add_to_db(new_targets) + pushed_targets = pushed_targets + new_targets.count + if #new_targets >= 50 then clean_db() end -- force a cleanup for large batches return true, hosts end +--- Adds the passed arguments to the target DB, which effectively +-- puts it on an exclusion list for future targets. +-- +-- @param targets A variable number of targets. Target is a +-- string that represents an IP or a Hostname. Tables will unpack. +-- @usage +-- local status, err = target.add_to_db("192.168.1.1") +-- local status, err = target.add_to_db(array_of_targets) +add_to_db = function (...) + local ips = {...} + + for i,ip in ipairs(ips) do + if type(ip) == "string" then + local newIP, IPtype = transform_ip(ip) + TARGET_DB[newIP] = IPtype + elseif type(ip) == "table" then + for _,item in pairs(ip) do table.insert(ips, item) end + end + end +end + +--- Cleans up the target DB, merging ranges that overlap. Requires +-- no parameters and returns nothing. This is automatically done if +-- 50 or more entries are added at the same time. +-- +-- @usage +-- local status, err = target.add_to_db("192.168.1.1") +-- local status, err = target.add_to_db(unpack(array_of_targets)) +clean_db = function() + + -- loop simplification (can't do si+1 with a hash...) + local arrayDB = {} + for ip,ipType in pairs(TARGET_DB) do + if ipType ~= -1 then table.insert(arrayDB, { ip, ipType } ) end -- no domains + end + local origCount = #arrayDB -- also can't get a count from a hash... + + for si,sTbl in ipairs(TARGET_DB) do + local sIP, sType = unpack(sTbl) + + -- don't care about IP->IP + if sType == 2 then + for di = si+1, #arrayDB, 1 do + + local dTbl = arrayDB[di] + local dIP, dType = unpack(dTbl) + + local ranges = add_range_to_range(sIP, dIP) + + if type(range) == 'table' and #ranges == 1 then + -- source wins; remove dest + if ranges[1] == sIP then + TARGET_DB[dIP] = nil + table.remove(arrayDB, di) + -- dest wins; remove source (and break this loop) + elseif ranges[1] == dIP then + TARGET_DB[sIP] = nil + table.remove(arrayDB, si) + si = si-1 + break + -- everybody loses; remove both and add new range + else + local rIP, rType = ranges[1], 1 + if rIP:match('/') then rType = 2 end + + TARGET_DB[sIP] = nil + TARGET_DB[dIP] = nil + TARGET_DB[rIP] = rType + table.remove(arrayDB, si) + table.remove(arrayDB, di) + table.insert(arrayDB, { rIP, rType } ) + si = si-1 + break + end + end + + end + end + end + + stdnse.print_debug("TARGET_DB: Reduced %d targets down to %d.", tostring(origCount), tostring(#arrayDB)) +end + return _ENV;