1468 lines
48 KiB
Lua
1468 lines
48 KiB
Lua
|
-- 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.3 then
|
||
|
error "NSE requires Lua 5.3 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_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 function K (a)
|
||
|
local insensitize = Cf((P(1) / function (a) return S(lower(a)..upper(a)) end)^1, function (a, b) return a * b end);
|
||
|
return assert(insensitize:match(a)) * #(V "space" + S"()," + P(-1)); -- "keyword" token
|
||
|
end
|
||
|
|
||
|
local REQUIRE_ERROR = {};
|
||
|
rawset(stdnse, "silent_require", function (...)
|
||
|
local status, mod = pcall(require, ...);
|
||
|
if not status then
|
||
|
print_debug(1, "%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);
|
||
|
|
||
|
local db_env = {Entry = nil};
|
||
|
local db_closure = assert(loadfile(script_database_path, "t", db_env),
|
||
|
"database appears to be corrupt or out of date;\n"..
|
||
|
"\tplease update using: nmap --script-updatedb");
|
||
|
|
||
|
local chosen_scripts, files_loaded = {}, {};
|
||
|
local used_rules, forced_rules = {}, {};
|
||
|
|
||
|
-- Was this category selection forced to run (e.g. "+script").
|
||
|
-- Return:
|
||
|
-- Boolean: True if it's forced otherwise false.
|
||
|
-- String: The new cleaned string.
|
||
|
local function is_forced_set (str)
|
||
|
local specification = match(str, "^%+(.*)$");
|
||
|
if specification then
|
||
|
return true, specification;
|
||
|
else
|
||
|
return false, str;
|
||
|
end
|
||
|
end
|
||
|
|
||
|
for i, rule in ipairs(rules) do
|
||
|
rule = match(rule, "^%s*(.-)%s*$"); -- strip surrounding whitespace
|
||
|
local forced, rule = is_forced_set(rule);
|
||
|
if rule and rule ~= "" then
|
||
|
used_rules[rule] = false; -- has not been used yet
|
||
|
forced_rules[rule] = forced;
|
||
|
-- Here we escape backslashes which might appear in Windows filenames.
|
||
|
rule = gsub(rule, "\\([^\\])", "\\\\%1");
|
||
|
rules[i] = rule;
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Checks if a given script, script_entry, should be loaded. A script_entry
|
||
|
-- should be in the form: { filename = "name.nse", categories = { ... } }
|
||
|
function db_env.Entry (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)
|
||
|
path = gsub(path, "%.nse$", ""); -- remove optional extension
|
||
|
path = gsub(path, "[%^%$%(%)%%%.%[%]%+%-%?]", "%%%1"); -- esc magic
|
||
|
path = gsub(path, "%*", ".*"); -- change to Lua wildcard
|
||
|
path = "^"..path.."$"; -- anchor to beginning and end
|
||
|
local found = not not find(escaped_basename, path);
|
||
|
selected_by_name = selected_by_name or found;
|
||
|
return found;
|
||
|
end
|
||
|
|
||
|
local 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";
|
||
|
|
||
|
category = K "all" * Cc(true); -- pseudo-category "all" matches everything
|
||
|
path = R("\033\039", "\042\126")^1 / match_script; -- all graphical characters not '(', ')'
|
||
|
};
|
||
|
|
||
|
for i, category in ipairs(categories) do
|
||
|
assert(type(category) == "string", "bad entry in script database");
|
||
|
T.category = T.category + K(category) * Cc(true);
|
||
|
end
|
||
|
|
||
|
T = P(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
|
||
|
|
||
|
db_closure(); -- 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, hosts)
|
||
|
-- 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
|
||
|
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 db = assert(open(script_database_path, 'w'));
|
||
|
local scripts = {};
|
||
|
for f in lfs.dir(path) do
|
||
|
if match(f, '%.nse$') then
|
||
|
scripts[#scripts+1] = path.."/"..f;
|
||
|
end
|
||
|
end
|
||
|
sort(scripts);
|
||
|
for i, script in ipairs(scripts) do
|
||
|
script = Script.new(script);
|
||
|
if ( script ) then
|
||
|
sort(script.categories);
|
||
|
db:write('Entry { filename = "', script.basename, '", ');
|
||
|
db:write('categories = {');
|
||
|
for j, category in ipairs(script.categories) do
|
||
|
db:write(' "', lower(category), '",');
|
||
|
end
|
||
|
db:write(' } }\n');
|
||
|
end
|
||
|
end
|
||
|
db:close();
|
||
|
log_write("stdout", "Script Database updated successfully.");
|
||
|
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
|
||
|
|
||
|
-- 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
|
||
|
-- This iterator is passed to the run function. It returns one new script
|
||
|
-- thread on demand until exhausted.
|
||
|
local function threads_iter ()
|
||
|
-- activate prerule scripts
|
||
|
if scantype == NSE_PRE_SCAN then
|
||
|
for _, script in ipairs(scripts) do
|
||
|
local thread = script:new_thread("prerule");
|
||
|
if thread then
|
||
|
yield(thread)
|
||
|
end
|
||
|
end
|
||
|
-- activate hostrule and portrule scripts
|
||
|
elseif scantype == NSE_SCAN then
|
||
|
-- 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
|
||
|
-- activate postrule scripts
|
||
|
elseif scantype == NSE_POST_SCAN then
|
||
|
for _, script in ipairs(scripts) do
|
||
|
local thread = script:new_thread("postrule");
|
||
|
if thread then
|
||
|
yield(thread);
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
print_verbose(2, "Starting runlevel %u (of %u) scan.", runlevel, #runlevels);
|
||
|
run(wrap(threads_iter), hosts)
|
||
|
end
|
||
|
|
||
|
collectgarbage "collect";
|
||
|
end
|
||
|
|
||
|
return main;
|