diff options
Diffstat (limited to 'nse_main.lua')
-rw-r--r-- | nse_main.lua | 1494 |
1 files changed, 1494 insertions, 0 deletions
diff --git a/nse_main.lua b/nse_main.lua new file mode 100644 index 0000000..596aba9 --- /dev/null +++ b/nse_main.lua @@ -0,0 +1,1494 @@ +-- Arguments when this file (function) is called, accessible via ... +-- [1] The NSE C library. This is saved in the local variable cnse for +-- access throughout the file. +-- [2] The list of categories/files/directories passed via --script. +-- The actual arguments passed to the anonymous main function: +-- [1] The list of hosts we run against. +-- +-- When making changes to this code, please ensure you do not add any +-- code relying global indexing. Instead, create a local below for the +-- global you need access to. This protects the engine from possible +-- replacements made to the global environment, speeds up access, and +-- documents dependencies. +-- +-- A few notes about the safety of the engine, that is, the ability for +-- a script developer to crash or otherwise stall NSE. The purpose of noting +-- these attack vectors is more to show the difficulty in accidentally +-- breaking the system than to indicate a user may wish to break the +-- system through these means. +-- - A script writer can use the undocumented Lua function newproxy +-- to inject __gc code that could run (and error) at any location. +-- - A script writer can use the debug library to break out of +-- the "sandbox" we give it. This is made a little more difficult by +-- our use of locals to all Lua functions we use and the exclusion +-- of the main thread and subsequent user threads. +-- - A simple while true do end loop can stall the system. This can be +-- avoided by debug hooks to yield the thread at periodic intervals +-- (and perhaps kill the thread) but a C function like string.find and +-- a malicious pattern can stall the system from C just as easily. +-- - The garbage collector function is available to users and they may +-- cause the system to stall through improper use. +-- - Of course the os and io library can cause the system to also break. + +local _VERSION = _VERSION; +local MAJOR, MINOR = assert(_VERSION:match "^Lua (%d+).(%d+)$"); +if tonumber(MAJOR.."."..MINOR) < 5.4 then + error "NSE requires Lua 5.4 or newer. It looks like you're using an older version of nmap." +end + +local NAME = "NSE"; + +-- Script Scan phases. +local NSE_PRE_SCAN = "NSE_PRE_SCAN"; +local NSE_SCAN = "NSE_SCAN"; +local NSE_POST_SCAN = "NSE_POST_SCAN"; + +-- String keys into the registry (_R), for data shared with nse_main.cc. +local YIELD = "NSE_YIELD"; +local BASE = "NSE_BASE"; +local WAITING_TO_RUNNING = "NSE_WAITING_TO_RUNNING"; +local DESTRUCTOR = "NSE_DESTRUCTOR"; +local SELECTED_BY_NAME = "NSE_SELECTED_BY_NAME"; +local FORMAT_TABLE = "NSE_FORMAT_TABLE"; +local FORMAT_XML = "NSE_FORMAT_XML"; +local PARALLELISM = "NSE_PARALLELISM"; + +-- Unique value indicating the action function is going to run. +local ACTION_STARTING = {}; + +-- This is a limit on the number of script instance threads running at once. It +-- exists only to limit memory use when there are many open ports. It doesn't +-- count worker threads started by scripts. +local CONCURRENCY_LIMIT = 1000; + +-- Table of different supported rules. +local NSE_SCRIPT_RULES = { + prerule = "prerule", + hostrule = "hostrule", + portrule = "portrule", + postrule = "postrule", +}; + +local cnse, rules = ...; -- The NSE C library and Script Rules + +local _G = _G; + +local assert = assert; +local collectgarbage = collectgarbage; +local error = error; +local ipairs = ipairs; +local load = load; +local loadfile = loadfile; +local next = next; +local pairs = pairs; +local pcall = pcall; +local rawget = rawget; +local rawset = rawset; +local require = require; +local select = select; +local setmetatable = setmetatable; +local tonumber = tonumber; +local tostring = tostring; +local type = type; + +local coroutine = require "coroutine"; +local create = coroutine.create; +local resume = coroutine.resume; +local status = coroutine.status; +local yield = coroutine.yield; +local wrap = coroutine.wrap; + +local debug = require "debug"; +local traceback = debug.traceback; +local _R = debug.getregistry(); + +local io = require "io"; +local lines = io.lines; +local open = io.open; + +local math = require "math"; +local max = math.max; + +local package = require "package"; + +local string = require "string"; +local byte = string.byte; +local find = string.find; +local format = string.format; +local gsub = string.gsub; +local lower = string.lower; +local match = string.match; +local sub = string.sub; +local upper = string.upper; + +local table = require "table"; +local concat = table.concat; +local insert = table.insert; +local pack = table.pack; +local remove = table.remove; +local sort = table.sort; +local unpack = table.unpack; + +local os = require "os" +local time = os.time +local difftime = os.difftime + +do -- Add loader to look in nselib/?.lua (nselib/ can be in multiple places) + local function loader (lib) + lib = lib:gsub("%.", "/"); -- change Lua "module separator" to directory separator + local name = "nselib/"..lib..".lua"; + local type, path = cnse.fetchfile_absolute(name); + if type == "file" then + return assert(loadfile(path)); + else + return "\n\tNSE failed to find "..name.." in search paths."; + end + end + insert(package.searchers, 1, loader); +end + +local lpeg = require "lpeg"; +local U = require "lpeg-utility" +local locale = lpeg.locale; +local P = lpeg.P; +local R = lpeg.R; +local S = lpeg.S; +local V = lpeg.V; +local C = lpeg.C; +local Cb = lpeg.Cb; +local Cc = lpeg.Cc; +local Cf = lpeg.Cf; +local Cg = lpeg.Cg; +local Ct = lpeg.Ct; + +local nmap = require "nmap"; +local lfs = require "lfs"; + +local socket = require "nmap.socket"; +local loop = socket.loop; + +local stdnse = require "stdnse"; + +local strict = require "strict"; +assert(_ENV == _G); +strict(_ENV); + +local script_database_type, script_database_path = + cnse.fetchfile_absolute(cnse.script_dbpath); +local script_database_update = cnse.scriptupdatedb; +local script_database = {Entry = nil,chunk = nil} + +local script_help = cnse.scripthelp; + +-- NSE_YIELD_VALUE +-- This is the table C uses to yield a thread with a unique value to +-- differentiate between yields initiated by NSE or regular coroutine yields. +local NSE_YIELD_VALUE = {}; + +do + -- This is the method by which we allow a script to have nested + -- coroutines. If a sub-thread yields in an NSE function such as + -- nsock.connect, then we propagate the yield up. These replacements + -- to the coroutine library are used only by Script Threads, not the engine. + + local function handle (co, status, ...) + if status and NSE_YIELD_VALUE == ... then -- NSE has yielded the thread + return handle(co, resume(co, yield(NSE_YIELD_VALUE))); + else + return status, ...; + end + end + + function coroutine.resume (co, ...) + return handle(co, resume(co, ...)); + end + + local resume = coroutine.resume; -- local reference to new coroutine.resume + local function aux_wrap (status, ...) + if not status then + return error(..., 2); + else + return ...; + end + end + function coroutine.wrap (f) + local co = create(f); + return function (...) + return aux_wrap(resume(co, ...)); + end + end +end + +-- Some local helper functions -- + +local log_write, verbosity, debugging = + nmap.log_write, nmap.verbosity, nmap.debugging; +local log_write_raw = cnse.log_write; + +local function print_verbose (level, fmt, ...) + if verbosity() >= assert(tonumber(level)) or debugging() > 0 then + log_write("stdout", format(fmt, ...)); + end +end + +local function print_debug (level, fmt, ...) + if debugging() >= assert(tonumber(level)) then + log_write("stdout", format(fmt, ...)); + end +end + +local function log_error (fmt, ...) + log_write("stderr", format(fmt, ...)); +end + +-- Check for and warn about some known bad behaviors +if ("test"):gsub(".*$", "x") == "xx" then + log_error("Known bug in string.gsub in Lua 5.3 before 5.3.3 will cause bugs in NSE scripts.") +end + +local function table_size (t) + local n = 0; for _ in pairs(t) do n = n + 1; end return n; +end + +local function loadscript (filename) + local source = "@"..filename; + local function ld () + -- header for scripts to allow setting the environment + yield [[return function (_ENV) return function (...)]]; + -- actual script + for line in lines(filename, 2^15) do + yield(line); + end + -- footer... + yield [[ end end]]; + return nil; + end + return assert(load(wrap(ld), source, "t"))(); +end + +-- recursively copy a table, for host/port tables +-- not very rigorous, but it doesn't need to be +local tcopy = require "tableaux".tcopy + +-- copies the host table while preserving the registry +local function host_copy(t) + local h = tcopy(t) + h.registry = t.registry + return h +end + +-- Return a pattern which matches a "keyword" literal, case insensitive. +local memo_K = {} +local function K (a) + local kw = memo_K[a] + if not kw then + kw = U.caseless(a) * #(V "space" + S"()," + P(-1)) + memo_K[a] = kw + end + return kw +end + +local REQUIRE_ERROR = {}; +rawset(stdnse, "silent_require", function (...) + local status, mod = pcall(require, ...); + if not status then + print_debug(2, "%s", traceback(mod)); + error(REQUIRE_ERROR) + else + return mod; + end +end); + +-- Gets a string containing as much of a host's name, IP, and port as are +-- available. +local function against_name(host, port) + local targetname, ip, portno, ipport, against; + if host then + targetname = host.targetname; + ip = host.ip; + end + if port then + portno = port.number; + end + if ip and portno then + ipport = ip..":"..portno; + elseif ip then + ipport = ip; + end + if targetname and ipport then + against = targetname.." ("..ipport..")"; + elseif targetname then + against = targetname; + elseif ipport then + against = ipport; + end + if against then + return " against "..against + else + return "" + end +end + +-- The Script Class, its constructor is Script.new. +local Script = {}; +-- The Thread Class, its constructor is Script:new_thread. +local Thread = {}; +-- The Worker Class, it's a subclass of Thread. Its constructor is +-- Thread:new_worker. It (currently) has no methods. +local Worker = {}; +do + -- Workers reference data from parent thread. + function Worker:__index (key) + return Worker[key] or self.parent[key] + end + + local function replace(fmt, pattern, repl) + -- Escape each % twice: once for gsub, and once for print_debug. + local r = gsub(repl, "%%", "%%%%%%%%") + return gsub(fmt, pattern, r); + end + -- Thread:d() + -- Outputs debug information at level 1 or higher. + -- Changes "%THREAD" with an appropriate identifier for the debug level + function Thread:d (fmt, ...) + local against = against_name(self.host, self.port); + local dbg = debugging() + if dbg > 1 then + fmt = replace(fmt, "%%THREAD_AGAINST", self.info..against); + fmt = replace(fmt, "%%THREAD", self.info); + elseif dbg == 1 then + fmt = replace(fmt, "%%THREAD_AGAINST", self.short_basename..against); + fmt = replace(fmt, "%%THREAD", self.short_basename); + else + return + end + -- debugging() >= 1 + log_write("stdout", format(fmt, ...)); + end + + -- Sets script output. r1 and r2 are the (as many as two) return values. + function Thread:set_output(r1, r2) + if not self.worker then + -- Structure table and unstructured string outputs. + local tab, str + + if r2 then + tab, str = r1, tostring(r2); + elseif type(r1) == "string" then + tab, str = nil, r1; + elseif r1 == nil then + return + else + tab, str = r1, nil; + end + + if self.type == "prerule" or self.type == "postrule" then + cnse.script_set_output(self.id, tab, str); + elseif self.type == "hostrule" then + cnse.host_set_output(self.host, self.id, tab, str); + elseif self.type == "portrule" then + cnse.port_set_output(self.host, self.port, self.id, tab, str); + end + end + end + + -- prerule/postrule scripts may be timed out in the future + -- based on start time and script lifetime? + function Thread:timed_out () + -- checking whether user gave --script-timeout option or not + if cnse.script_timeout and cnse.script_timeout > 0 and + -- comparing script's timeout with time elapsed + cnse.script_timeout < difftime(time(), self.start_time) then + return true + end + if self.host then + return cnse.timedOut(self.host) + end + return false + end + + function Thread:start_time_out_clock () + if self.type == "hostrule" or self.type == "portrule" then + cnse.startTimeOutClock(self.host); + end + end + + function Thread:stop_time_out_clock () + if self.type == "hostrule" or self.type == "portrule" then + cnse.stopTimeOutClock(self.host); + end + end + + -- Register scripts in the timeouts list to track their timeouts. + function Thread:start (timeouts) + if self.host then + timeouts[self.host] = timeouts[self.host] or {}; + timeouts[self.host][self.co] = true; + end + -- storing script's start time so as to account for script's timeout later + if self.worker then + self.start_time = self.parent.start_time + else + self.start_time = time() + end + end + + -- Remove scripts from the timeouts list and call their + -- destructor handles. + function Thread:close (timeouts, result) + self.error = result; + if self.host then + timeouts[self.host][self.co] = nil; + -- Any more threads running for this script/host? + if not next(timeouts[self.host]) then + self:stop_time_out_clock(); + timeouts[self.host] = nil; + end + end + local ch = self.close_handlers; + for key, destructor_t in pairs(ch) do + destructor_t.destructor(destructor_t.thread, key); + ch[key] = nil; + end + end + + -- thread = Script:new_thread(rule, ...) + -- Creates a new thread for the script Script. + -- Arguments: + -- rule The rule argument the rule, hostrule or portrule, tested. + -- ... The arguments passed to the rule function (host[, port]). + -- Returns: + -- thread The thread (class) is returned, or nil. + function Script:new_thread (rule, ...) + local script_type = assert(NSE_SCRIPT_RULES[rule]); + if not self[rule] then return nil end -- No rule for this script? + + -- Rebuild the environment for the running thread. + local env = { + SCRIPT_PATH = self.filename, + SCRIPT_NAME = self.short_basename, + SCRIPT_TYPE = script_type, + }; + setmetatable(env, {__index = _G}); + local forced = self.forced_to_run; + local script_closure_generator = self.script_closure_generator; + local function main (...) + local _ENV = env; -- change the environment + -- Load the script's globals in the same Lua thread the action and rule + -- functions will execute in. + script_closure_generator(_ENV)(); + if forced or _ENV[rule](...) then + yield(ACTION_STARTING) + return action(...) + end + end + + local co = create(main); + local thread = { + action_started = false, + args = pack(...), + close_handlers = {}, + co = co, + env = env, + identifier = tostring(co), + info = format("%s M:%s", self.id, match(tostring(co), "^thread: 0?[xX]?(.*)")); + parent = nil, -- placeholder + script = self, + type = script_type, + worker = false, + start_time = 0, --for script timeout + }; + thread.parent = thread; + setmetatable(thread, Thread) + return thread; + end + + function Thread:new_worker (main, ...) + local co = create(main); + print_debug(2, "%s spawning new thread (%s).", self.parent.info, tostring(co)); + local thread = { + args = pack(...), + close_handlers = {}, + co = co, + info = format("%s W:%s", self.id, match(tostring(co), "^thread: 0?[xX]?(.*)")); + parent = self, + worker = true, + start_time = 0, + }; + setmetatable(thread, Worker) + local function info () + return status(co), rawget(thread, "error"); + end + return thread, info; + end + + function Thread:resume (timeouts) + local ok, r1, r2 = resume(self.co, unpack(self.args, 1, self.args.n)); + local status = status(self.co); + if ok and r1 == ACTION_STARTING then + self:d("Starting %THREAD_AGAINST."); + self.action_started = true + return self:resume(timeouts); + elseif not ok then + -- Extend this to create new types of errors with custom handling. + -- nmap.new_try does equivalent of: error({errtype="nmap.new_try", message="TIMEOUT"}) + if type(r1) == "table" and r1.errtype == "nmap.new_try" then + -- nmap.new_try "exception" is closing the script + if debugging() > 0 then + self:d("Finished %THREAD_AGAINST. Reason: %s\n", r1.message); + end + r1 = r1.message + elseif debugging() > 0 then + self:d("%THREAD_AGAINST threw an error!\n%s\n", traceback(self.co, tostring(r1))); + else + self:set_output("ERROR: Script execution failed (use -d to debug)"); + end + self:close(timeouts, r1); + return false + elseif status == "suspended" then + if r1 == NSE_YIELD_VALUE then + return true + else + self:d("%THREAD yielded unexpectedly and cannot be resumed."); + self:close(timeouts, "yielded unexpectedly and cannot be resumed"); + return false + end + elseif status == "dead" then + if self.action_started then + self:set_output(r1, r2); + -- -d1 = report finished scripts. -d2 = report finished threads + if not self.worker or debugging() > 1 then + self:d("Finished %THREAD_AGAINST."); + end + end + self:close(timeouts); + end + end + + function Thread:__index (key) + return Thread[key] or self.script[key] + end + + -- Script.new provides defaults for some of these. + local required_fields = { + action = "function", + categories = "table", + dependencies = "table", + }; + local quiet_errors = { + [REQUIRE_ERROR] = true, + } + + -- script = Script.new(filename) + -- Creates a new Script Class for the script. + -- Arguments: + -- filename The filename (path) of the script to load. + -- script_params The script selection parameters table. + -- Possible key/value pairs: + -- selection: A string to indicate the script selection type. + -- "name": Selected by name or pattern. + -- "category" Selected by category. + -- "file path" Selected by file path. + -- "directory" Selected by directory. + -- verbosity: A boolean, if set to true the script will get a + -- verbosity boost. Scripts selected by name or + -- file paths must set this to true. + -- forced: A boolean to indicate if the script will be + -- forced to run regardless to its rule results. + -- (e.g. "+script"). + -- Returns: + -- script The script (class) created. + function Script.new (filename, script_params) + local script_params = script_params or {}; + assert(type(filename) == "string", "string expected"); + if not find(filename, "%.nse$") then + log_error( + "Warning: Loading '%s' -- the recommended file extension is '.nse'.", + filename); + end + + local basename = match(filename, "([^/\\]+)$") or filename; + local short_basename = match(filename, "([^/\\]+)%.nse$") or + match(filename, "([^/\\]+)%.[^.]*$") or filename; + + print_debug(2, "Script %s was selected by %s%s.", + basename, + script_params.selection or "(unknown)", + script_params.forced and " and forced to run" or ""); + local script_closure_generator = loadscript(filename); + -- Give the closure its own environment, with global access + local env = { + SCRIPT_PATH = filename, + SCRIPT_NAME = short_basename, + categories = {}, + dependencies = {}, + }; + setmetatable(env, {__index = _G}); + local script_closure = script_closure_generator(env); + local co = create(script_closure); -- Create a garbage thread + local status, e = resume(co); -- Get the globals it loads in env + if not status then + if quiet_errors[e] then + print_verbose(1, "Failed to load '%s'.", filename); + return nil; + else + log_error("Failed to load %s:\n%s", filename, traceback(co, e)); + error("could not load script"); + end + end + -- Check that all the required fields were set + for f, t in pairs(required_fields) do + local field = rawget(env, f); + if field == nil then + error(filename.." is missing required field: '"..f.."'"); + elseif type(field) ~= t then + error(filename.." field '"..f.."' is of improper type '".. + type(field).."', expected type '"..t.."'"); + end + end + -- Check the required rule functions + local rules = {} + for rule in pairs(NSE_SCRIPT_RULES) do + local rulef = rawget(env, rule); + assert(type(rulef) == "function" or rulef == nil, + rule.." must be a function!"); + rules[rule] = rulef; + end + assert(next(rules), filename.." is missing required function: 'rule'"); + local prerule = rules.prerule; + local hostrule = rules.hostrule; + local portrule = rules.portrule; + local postrule = rules.postrule; + -- Assert that categories is an array of strings + for i, category in ipairs(rawget(env, "categories")) do + assert(type(category) == "string", + filename.." has non-string entries in the 'categories' array"); + end + -- Assert that dependencies is an array of strings + for i, dependency in ipairs(rawget(env, "dependencies")) do + assert(type(dependency) == "string", + filename.." has non-string entries in the 'dependencies' array"); + end + -- Return the script + local script = { + filename = filename, + basename = basename, + short_basename = short_basename, + id = match(filename, "^.-[/\\]([^\\/]-)%.nse$") or short_basename, + script_closure_generator = script_closure_generator, + prerule = prerule, + hostrule = hostrule, + portrule = portrule, + postrule = postrule, + args = {n = 0}; + description = rawget(env, "description"), + categories = rawget(env, "categories"), + author = rawget(env, "author"), + license = rawget(env, "license"), + dependencies = rawget(env, "dependencies"), + threads = {}, + -- Make sure that the following are boolean types. + selected_by_name = not not script_params.verbosity, + forced_to_run = not not script_params.forced, + }; + return setmetatable(script, Script) + end + + Script.__index = Script; +end + +-- check_rules(rules) +-- Adds the "default" category if no rules were specified. +-- Adds other implicitly specified rules (e.g. "version") +-- +-- Arguments: +-- rules The array of rules to check. +local function check_rules (rules) + if cnse.default and #rules == 0 then rules[1] = "default" end + if cnse.scriptversion then rules[#rules+1] = "version" end +end + +-- chosen_scripts = get_chosen_scripts(rules) +-- Loads all the scripts for the given rules using the Script Database. +-- Arguments: +-- rules The array of rules to use for loading scripts. +-- Returns: +-- chosen_scripts The array of scripts loaded for the given rules. +local function get_chosen_scripts (rules) + check_rules(rules); + assert(script_database.chunk, "Script database not loaded") + + local chosen_scripts, files_loaded = {}, {}; + local used_rules, forced_rules = {}, {}; + + for i, rule in ipairs(rules) do + -- A rule (usually filename) is forced if it starts with "+" + local forced, rule = match(rule, "^%s*(%+?)%s*(.-)%s*$"); -- strip surrounding whitespace + if rule and rule ~= "" then + used_rules[rule] = false; -- has not been used yet + forced_rules[rule] = (forced == "+"); + rules[i] = rule; + end + end + + local pre_T = locale { + V "space"^0 * V "expression" * V "space"^0 * P(-1); + + expression = V "disjunct" + V "conjunct" + V "value"; + disjunct = (V "conjunct" + V "value") * V "space"^0 * K "or" * V "space"^0 * V "expression" / function (a, b) return a or b end; + conjunct = V "value" * V "space"^0 * K "and" * V "space"^0 * V "expression" / function (a, b) return a and b end; + value = K "not" * V "space"^0 * V "value" / function (a) return not a end + + P "(" * V "space"^0 * V "expression" * V "space"^0 * P ")" + + K "true" * Cc(true) + + K "false" * Cc(false) + + V "category" + + V "path"; + } + -- cache/memoize result of "glob-izing" a word in a rule. + local globs = {} + setmetatable(globs, { + __index = function(t, path) + local glob = gsub(path, "%.nse$", ""); -- remove optional extension + glob = gsub(glob, "[%^%$%(%)%%%.%[%]%+%-%?]", "%%%1"); -- esc magic + glob = gsub(glob, "%*", ".*"); -- change to Lua wildcard + glob = "^"..glob.."$"; -- anchor to beginning and end + t[path] = glob + return glob + end, + }) + -- Checks if a given script, script_entry, should be loaded. A script_entry + -- should be in the form: { filename = "name.nse", categories = { ... } } + script_database.Entry = function (script_entry) + local categories = rawget(script_entry, "categories"); + local filename = rawget(script_entry, "filename"); + assert(type(categories) == "table" and type(filename) == "string", "script database appears corrupt, try `nmap --script-updatedb`"); + local escaped_basename = match(filename, "([^/\\]-)%.nse$") or match(filename, "([^/\\]-)$"); + local selected_by_name = false; + -- The script selection parameters table. + local script_params = {}; + + -- Test if path is a glob pattern that matches script_entry.filename. + local function match_script (path) + local found = not not find(escaped_basename, globs[path]); + selected_by_name = selected_by_name or found; + return found; + end + + local my_cats = K "all" * Cc(true) -- pseudo-category "all" matches everything + for i, category in ipairs(categories) do + assert(type(category) == "string", "bad entry in script database"); + my_cats = my_cats + K(category) * Cc(true); + end + + pre_T.path = R("\033\039", "\042\126")^1 / match_script; -- all graphical characters not '(', ')' + pre_T.category = my_cats + + local T = P(pre_T) + + for i, rule in ipairs(rules) do + selected_by_name = false; + if T:match(rule) then + used_rules[rule] = true; + script_params.forced = not not forced_rules[rule]; + if selected_by_name then + script_params.selection = "name" + script_params.verbosity = true + else + script_params.selection = "category" + end + local t, path = cnse.fetchscript(filename); + if t == "file" then + if not files_loaded[path] then + local script = Script.new(path, script_params) + chosen_scripts[#chosen_scripts+1] = script; + files_loaded[path] = true; + -- do not break so other rules can be marked as used + end + else + log_error("Warning: Could not load '%s': %s", filename, path); + break; + end + end + end + end + + script_database.chunk() -- Load the scripts + + -- Now load any scripts listed by name rather than by category. + for rule, loaded in pairs(used_rules) do + if not loaded then -- attempt to load the file/directory + local script_params = {}; + script_params.forced = not not forced_rules[rule]; + local t, path = cnse.fetchscript(rule); + if t == nil then -- perhaps omitted the extension? + t, path = cnse.fetchscript(rule..".nse"); + end + if t == nil then + -- Avoid erroring if -sV but no scripts are present + if not (cnse.scriptversion and rule == "version") then + error("'"..rule.."' did not match a category, filename, or directory"); + end + elseif t == "bare_directory" then + error("directory '"..path.."' found, but will not match without '/'") + elseif t == "file" and not files_loaded[path] then + script_params.selection = "file path"; + script_params.verbosity = true; + local script = Script.new(path, script_params); + chosen_scripts[#chosen_scripts+1] = script; + files_loaded[path] = true; + elseif t == "directory" then + for f in lfs.dir(path) do + local file = path .."/".. f + if find(file, "%.nse$") and not files_loaded[file] then + script_params.selection = "directory"; + local script = Script.new(file, script_params); + chosen_scripts[#chosen_scripts+1] = script; + files_loaded[file] = true; + end + end + end + end + end + + -- calculate runlevels + local name_script = {}; + for i, script in ipairs(chosen_scripts) do + assert(name_script[script.short_basename] == nil, + ("duplicate script ID: '%s'"):format(script.short_basename)); + name_script[script.short_basename] = script; + end + local chain = {}; -- chain of script names + local function calculate_runlevel (script) + chain[#chain+1] = script.short_basename; + if script.runlevel == false then -- circular dependency + error("circular dependency in chain `"..concat(chain, "->").."`"); + else + script.runlevel = false; -- placeholder + end + local runlevel = 1; + for i, dependency in ipairs(script.dependencies) do + -- yes, use rawget in case we add strong dependencies again + local s = rawget(name_script, dependency); + if s then + local r = tonumber(s.runlevel) or calculate_runlevel(s); + runlevel = max(runlevel, r+1); + end + end + chain[#chain] = nil; + script.runlevel = runlevel; + return runlevel; + end + for i, script in ipairs(chosen_scripts) do + local _ = script.runlevel or calculate_runlevel(script); + end + + return chosen_scripts; +end + +-- run(threads) +-- The main loop function for NSE. It handles running all the script threads. +-- Arguments: +-- threads An array of threads (a runlevel) to run. +local function run (threads_iter) + -- running scripts may be resumed at any time. waiting scripts are + -- yielded until Nsock wakes them. After being awakened with + -- nse_restore, waiting threads become pending and later are moved all + -- at once back to running. pending is used because we cannot modify + -- running during traversal. + local running, waiting, pending = {}, {}, {}; + local all = setmetatable({}, {__mode = "kv"}); -- base coroutine to Thread + local current; -- The currently running Thread. + local total = 0; -- Number of threads, for record keeping. + local timeouts = {}; -- A list to save and to track scripts timeout. + local num_threads = 0; -- Number of script instances currently running. + + -- Map of yielded threads to the base Thread + local yielded_base = setmetatable({}, {__mode = "kv"}); + -- _R[YIELD] is called by nse_yield in nse_main.cc + _R[YIELD] = function (co) + yielded_base[co] = current; -- set base + return NSE_YIELD_VALUE; -- return NSE_YIELD_VALUE + end + _R[BASE] = function () + return current and current.co; + end + -- _R[WAITING_TO_RUNNING] is called by nse_restore in nse_main.cc + _R[WAITING_TO_RUNNING] = function (co, ...) + local base = yielded_base[co] or all[co]; -- translate to base thread + if base then + co = base.co; + if waiting[co] then -- ignore a thread not waiting + pending[co], waiting[co] = waiting[co], nil; + pending[co].args = pack(...); + end + end + end + -- _R[DESTRUCTOR] is called by nse_destructor in nse_main.cc + _R[DESTRUCTOR] = function (what, co, key, destructor) + local thread = yielded_base[co] or all[co] or current; + if thread then + local ch = thread.close_handlers; + if what == "add" then + ch[key] = { + thread = co, + destructor = destructor + }; + elseif what == "remove" then + ch[key] = nil; + end + end + end + _R[SELECTED_BY_NAME] = function() + return current and current.selected_by_name; + end + rawset(stdnse, "new_thread", function (main, ...) + assert(type(main) == "function", "function expected"); + if current == nil then + error "stdnse.new_thread can only be run from an active script" + end + local worker, info = current:new_worker(main, ...); + total, all[worker.co], pending[worker.co], num_threads = total+1, worker, worker, num_threads+1; + worker:start(timeouts); + return worker.co, info; + end); + + rawset(stdnse, "base", function () + return current and current.co; + end); + rawset(stdnse, "gettid", function () + return current and current.identifier; + end); + rawset(stdnse, "getid", function () + return current and current.id; + end); + rawset(stdnse, "getinfo", function () + return current and current.info; + end); + rawset(stdnse, "gethostport", function () + if current then + return current.host, current.port; + end + end); + rawset(stdnse, "isworker", function () + return current and current.worker; + end); + + local progress = cnse.scan_progress_meter(NAME); + + -- Loop while any thread is running or waiting. + while next(running) or next(waiting) or threads_iter do + -- Start as many new threads as possible. + while threads_iter and num_threads < CONCURRENCY_LIMIT do + local thread = threads_iter() + if not thread then + threads_iter = nil; + break; + end + all[thread.co], running[thread.co], total = thread, thread, total+1; + num_threads = num_threads + 1; + thread:start(timeouts); + end + + local nr, nw = table_size(running), table_size(waiting); + -- total may be 0 if no scripts are running in this phase + if total > 0 and cnse.key_was_pressed() then + print_verbose(1, "Active NSE Script Threads: %d (%d waiting)", + nr+nw, nw); + progress("printStats", 1-(nr+nw)/total); + if debugging() >= 2 then + for co, thread in pairs(running) do + thread:d("Running: %THREAD_AGAINST\n\t%s", + (gsub(traceback(co), "\n", "\n\t"))); + end + for co, thread in pairs(waiting) do + thread:d("Waiting: %THREAD_AGAINST\n\t%s", + (gsub(traceback(co), "\n", "\n\t"))); + end + elseif debugging() >= 1 then + local display = {} + local limit = 0 + for co, thread in pairs(running) do + local this = display[thread.short_basename] + if not this then + this = {} + limit = limit + 1 + if limit > 5 then + -- Only print stats if 5 or fewer scripts remaining + break + end + end + this[1] = (this[1] or 0) + 1 + display[thread.short_basename] = this + end + for co, thread in pairs(waiting) do + local this = display[thread.short_basename] + if not this then + this = {} + limit = limit + 1 + if limit > 5 then + -- Only print stats if 5 or fewer scripts remaining + break + end + end + this[2] = (this[2] or 0) + 1 + display[thread.short_basename] = this + end + if limit <= 5 then + for name, stats in pairs(display) do + print_debug(1, "Script %s: %d threads running, %d threads waiting", + name, stats[1] or 0, stats[2] or 0) + end + end + end + elseif total > 0 and progress "mayBePrinted" then + if verbosity() > 1 or debugging() > 0 then + progress("printStats", 1-(nr+nw)/total); + else + progress("printStatsIfNecessary", 1-(nr+nw)/total); + end + end + + local orphans = true + -- Checked for timed-out scripts and hosts. + for co, thread in pairs(waiting) do + if thread:timed_out() then + waiting[co], all[co], num_threads = nil, nil, num_threads-1; + thread:d("%THREAD_AGAINST timed out") + thread:close(timeouts, "timed out"); + elseif not thread.worker then + orphans = false + end + end + + for co, thread in pairs(running) do + current, running[co] = thread, nil; + thread:start_time_out_clock(); + + if thread:resume(timeouts) then + waiting[co] = thread; + if not thread.worker then + orphans = false + end + else + all[co], num_threads = nil, num_threads-1; + end + current = nil; + end + + loop(50); -- Allow nsock to perform any pending callbacks + -- Move pending threads back to running. + for co, thread in pairs(pending) do + pending[co], running[co] = nil, thread; + if not thread.worker then + orphans = false + end + end + + collectgarbage "step"; + -- If we didn't see at least one non-worker thread, then any remaining are orphaned. + if num_threads > 0 and orphans then + print_debug(1, "%d orphans left!", total) + break + end + end + + progress "endTask"; +end + +-- This function does the automatic formatting of Lua objects into strings, for +-- normal output and for the XML @output attribute. Each nested table is +-- indented by two spaces. Tables having a __tostring metamethod are converted +-- using tostring. Otherwise, integer keys are listed first and only their +-- value is shown; then string keys are shown prefixed by the key and a colon. +-- Any other kinds of keys. Anything that is not a table is converted to a +-- string with tostring. +local function format_table(obj, indent) + indent = indent or " "; + if type(obj) == "table" then + local mt = getmetatable(obj) + if mt and mt["__tostring"] then + -- Table obeys tostring, so use that. + return tostring(obj) + end + + local lines = {}; + -- Do integer keys. + for _, v in ipairs(obj) do + lines[#lines + 1] = "\n" + lines[#lines + 1] = indent + lines[#lines + 1] = format_table(v, indent .. " ") + end + -- Do string keys. + for k, v in pairs(obj) do + if type(k) == "string" then + lines[#lines + 1] = "\n" + lines[#lines + 1] = indent + lines[#lines + 1] = k + lines[#lines + 1] = ": " + lines[#lines + 1] = format_table(v, indent .. " ") + end + end + return concat(lines); + else + return tostring(obj); + end +end +_R[FORMAT_TABLE] = format_table + +local format_xml +local function format_xml_elem(obj, key) + if key then + key = cnse.protect_xml(tostring(key)); + end + if type(obj) == "table" then + cnse.xml_start_tag("table", {key=key}); + cnse.xml_newline(); + else + cnse.xml_start_tag("elem", {key=key}); + end + format_xml(obj); + cnse.xml_end_tag(); + cnse.xml_newline(); +end + +-- This function writes an XML representation of a Lua object to the XML stream. +function format_xml(obj, key) + if type(obj) == "table" then + -- Do integer keys. + for _, v in ipairs(obj) do + format_xml_elem(v); + end + -- Do string keys. + for k, v in pairs(obj) do + if type(k) == "string" then + format_xml_elem(v, k); + end + end + else + cnse.xml_write_escaped(cnse.protect_xml(tostring(obj))); + end +end +_R[FORMAT_XML] = format_xml + +-- Format NSEDoc markup (e.g., including bullet lists and <code> sections) into +-- a display string at the given indentation level. Currently this only indents +-- the string and doesn't interpret any other markup. +local function format_nsedoc(nsedoc, indent) + indent = indent or "" + + return gsub(nsedoc, "([^\n]+)", indent .. "%1") +end + +-- Return the NSEDoc URL for the script with the given id. +local function nsedoc_url(id) + return format("%s/nsedoc/scripts/%s.html", cnse.NMAP_URL, id) +end + +local function script_help_normal(chosen_scripts) + for i, script in ipairs(chosen_scripts) do + log_write_raw("stdout", "\n"); + log_write_raw("stdout", format("%s\n", script.id)); + log_write_raw("stdout", format("Categories: %s\n", concat(script.categories, " "))); + log_write_raw("stdout", format("%s\n", nsedoc_url(script.id))); + if script.description then + log_write_raw("stdout", format_nsedoc(script.description, " ")); + end + end +end + +local function script_help_xml(chosen_scripts) + cnse.xml_start_tag("nse-scripts"); + cnse.xml_newline(); + + local t, scripts_dir, nselib_dir + t, scripts_dir = cnse.fetchfile_absolute("scripts/") + assert(t == 'directory', 'could not locate scripts directory'); + t, nselib_dir = cnse.fetchfile_absolute("nselib/") + assert(t == 'directory', 'could not locate nselib directory'); + cnse.xml_start_tag("directory", { name = "scripts", path = scripts_dir }); + cnse.xml_end_tag(); + cnse.xml_newline(); + cnse.xml_start_tag("directory", { name = "nselib", path = nselib_dir }); + cnse.xml_end_tag(); + cnse.xml_newline(); + + for i, script in ipairs(chosen_scripts) do + cnse.xml_start_tag("script", { filename = script.filename }); + cnse.xml_newline(); + + cnse.xml_start_tag("categories"); + for _, category in ipairs(script.categories) do + cnse.xml_start_tag("category"); + cnse.xml_write_escaped(category); + cnse.xml_end_tag(); + end + cnse.xml_end_tag(); + cnse.xml_newline(); + + if script.description then + cnse.xml_start_tag("description"); + cnse.xml_write_escaped(script.description); + cnse.xml_end_tag(); + cnse.xml_newline(); + end + + -- script + cnse.xml_end_tag(); + cnse.xml_newline(); + end + + -- nse-scripts + cnse.xml_end_tag(); + cnse.xml_newline(); +end + +nmap.registry.args = {}; +do + local args = {}; + + if cnse.scriptargsfile then + local t, path = cnse.fetchfile_absolute(cnse.scriptargsfile) + assert(t == 'file', format("%s is not a file", path)) + print_debug(1, "Loading script-args from file `%s'", cnse.scriptargsfile); + args[#args+1] = assert(assert(open(path, 'r')):read "*a"):gsub(",*$", ""); + end + + if cnse.scriptargs then -- Load script arguments (--script-args) + print_debug(1, "Arguments from CLI: %s", cnse.scriptargs); + args[#args+1] = cnse.scriptargs; + end + + if cnse.script_timeout and cnse.script_timeout > 0 then + print_debug(1, "Set script-timeout as: %d seconds", cnse.script_timeout); + end + + args = concat(args, ","); + if #args > 0 then + print_debug(1, "Arguments parsed: %s", args); + local function set (t, a, b) + if b == nil then + insert(t, a); + return t; + else + return rawset(t, a, b); + end + end + local parser = locale { + V "space"^0 * V "table" * V "space"^0, + table = Cf(Ct "" * P "{" * V "space"^0 * (V "fieldlst")^-1 * V "space"^0 * P "}", set); + hws = V "space" - P "\n", -- horizontal whitespace + fieldlst = V "field" * (V "hws"^0 * S "\n," * V "space"^0 * V "field")^0; + field = V "kv" + V "av"; + kv = Cg(V "string" * V "hws"^0 * P "=" * V "hws"^0 * V "value"); + av = Cg(V "value"); + value = V "table" + V "string"; + string = V "qstring" + V "uqstring"; + qstring = U.escaped_quote('"') + U.escaped_quote("'"); + uqstring = V "hws"^0 * C((P(1) - V "hws"^0 * S "\n,{}=")^0) * V "hws"^0; -- everything but '\n,{}=', do not capture final space + }; + --U.debug(parser,function(...)return print_debug(1,...)end) + parser = assert(P(parser)); + nmap.registry.args = parser:match("{"..args.."}"); + if not nmap.registry.args then + log_write("stdout", "args = "..args); + error "arguments did not parse!" + end + if debugging() >= 2 then + local out = {} + rawget(stdnse, "pretty_printer")(nmap.registry.args, function (s) out[#out+1] = s end) + print_debug(2, "%s", concat(out)) + end + end +end + +-- Update Missing Script Database? +if script_database_type ~= "file" then + print_verbose(1, "Script Database missing, will create new one."); + script_database_update = true; -- force update +else + local err + script_database.chunk, err = loadfile(script_database_path, "t", script_database) + if not script_database.chunk then + log_write("stdout", + "NSE script database appears to be corrupt or out of date;\n".. + "\tplease update using: nmap --script-updatedb") + print_debug(1, "loadfile error: %s", err) + script_database_update = true + end +end + +if script_database_update then + log_write("stdout", "Updating rule database."); + local t, path = cnse.fetchfile_absolute('scripts/'); -- fetch script directory + assert(t == 'directory', 'could not locate scripts directory'); + script_database_path = path .. "script.db" + local scripts = {}; + for f in lfs.dir(path) do + if match(f, '%.nse$') then + scripts[#scripts+1] = path.."/"..f; + end + end + sort(scripts); + local db_text = {} + local db_params = {selection = "script.db update"} + for i, script in ipairs(scripts) do + script = Script.new(script, db_params); + if ( script ) then + sort(script.categories); + db_text[#db_text+1] = format('Entry { filename = "%s", categories = {', script.basename) + for j, category in ipairs(script.categories) do + db_text[#db_text+1] = format(' "%s",', lower(category)) + end + db_text[#db_text+1] = ' } }\n' + end + end + db_text = concat(db_text) + local db, status, err + script_database.chunk, err = load(db_text, "script.db", "t", script_database) + if not script_database.chunk then + error("Script database corrupt: " .. err) + end + db, err = open(script_database_path, 'w') + if db then + status, err = db:write(db_text) + db:close(); + end + if status then + log_write("stdout", "Script Database updated successfully."); + else + (cnse.scriptupdatedb and error or log_error)("Could not save script.db: " .. err) + end +end + +-- Load all user chosen scripts +local chosen_scripts = get_chosen_scripts(rules); +print_verbose(1, "Loaded %d scripts for scanning.", #chosen_scripts); +for i, script in ipairs(chosen_scripts) do + print_debug(2, "Loaded '%s'.", script.filename); +end + +if script_help then + script_help_normal(chosen_scripts); + script_help_xml(chosen_scripts); +end + +-- This iterator is passed to the run function. It returns one new script +-- thread on demand until exhausted. +local threads_iters = { + NSE_PRE_SCAN = function (hosts, scripts) + return function () -- threads_iter + for _, script in ipairs(scripts) do + local thread = script:new_thread("prerule"); + if thread then + yield(thread) + end + end + end + end, + NSE_SCAN = function (hosts, scripts) + return function () -- threads_iter + -- Check hostrules for this host. + for j, host in ipairs(hosts) do + for _, script in ipairs(scripts) do + local thread = script:new_thread("hostrule", host_copy(host)); + if thread then + thread.host = host; + yield(thread); + end + end + -- Check portrules for this host. + for port in cnse.ports(host) do + for _, script in ipairs(scripts) do + local thread = script:new_thread("portrule", host_copy(host), tcopy(port)); + if thread then + thread.host, thread.port = host, port; + yield(thread); + end + end + end + end + end + end, + NSE_POST_SCAN = function (hosts, scripts) + return function () -- threads_iter + for _, script in ipairs(scripts) do + local thread = script:new_thread("postrule"); + if thread then + yield(thread); + end + end + end + end, +} + +-- main(hosts) +-- This is the main function we return to NSE (on the C side), nse_main.cc +-- gets this function by loading and executing nse_main.lua. This +-- function runs a script scan phase according to its arguments. +-- Arguments: +-- hosts An array of hosts to scan. +-- scantype A string that indicates the current script scan phase. +-- Possible string values are: +-- "SCRIPT_PRE_SCAN" +-- "SCRIPT_SCAN" +-- "SCRIPT_POST_SCAN" +local function main (hosts, scantype) + -- Used to set up the runlevels. + local threads, runlevels = {}, {}; + + -- Every script thread has a table that is used in the run function + -- (the main loop of NSE). + -- This is the list of the thread table key/value pairs: + -- Key Value + -- type A string that indicates the rule type of the script. + -- co A thread object to identify the coroutine. + -- parent A table that contains the parent thread table (it self). + -- close_handlers + -- A table that contains the thread destructor handlers. + -- info A string that contains the script name and the thread + -- debug information. + -- args A table that contains the arguments passed to scripts, + -- arguments can be host and port tables. + -- env A table that contains the global script environment: + -- categories, description, author, license, nmap table, + -- action function, rule functions, SCRIPT_PATH, + -- SCRIPT_NAME, SCRIPT_TYPE (pre|host|port|post rule). + -- identifier + -- A string to identify the thread address. + -- host A table that contains the target host information. This + -- will be nil for Pre-scanning and Post-scanning scripts. + -- port A table that contains the target port information. This + -- will be nil for Pre-scanning and Post-scanning scripts. + + local runlevels = {}; + for i, script in ipairs(chosen_scripts) do + runlevels[script.runlevel] = runlevels[script.runlevel] or {}; + insert(runlevels[script.runlevel], script); + end + + if _R[PARALLELISM] > CONCURRENCY_LIMIT then + CONCURRENCY_LIMIT = _R[PARALLELISM]; + end + + if scantype == NSE_PRE_SCAN then + print_verbose(1, "Script Pre-scanning."); + elseif scantype == NSE_SCAN then + if #hosts > 1 then + print_verbose(1, "Script scanning %d hosts.", #hosts); + elseif #hosts == 1 then + print_verbose(1, "Script scanning %s.", hosts[1].ip); + end + elseif scantype == NSE_POST_SCAN then + print_verbose(1, "Script Post-scanning."); + end + + for runlevel, scripts in ipairs(runlevels) do + local threads_iter = assert(threads_iters[scantype](hosts, scripts)) + print_verbose(2, "Starting runlevel %u (of %u) scan.", runlevel, #runlevels); + run(wrap(threads_iter)) + end + + collectgarbage "collect"; +end + +return main; |