From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/brute.lua | 1551 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1551 insertions(+) create mode 100644 nselib/brute.lua (limited to 'nselib/brute.lua') diff --git a/nselib/brute.lua b/nselib/brute.lua new file mode 100644 index 0000000..e8b1549 --- /dev/null +++ b/nselib/brute.lua @@ -0,0 +1,1551 @@ +--- +-- The brute library is an attempt to create a common framework for performing +-- password guessing against remote services. +-- +-- The library currently attempts to parallelize the guessing by starting +-- a number of working threads and increasing that number gradually until +-- brute.threads limit is reached. The starting number of threads can be set +-- with brute.start argument, it defaults to 5. The brute.threads argument +-- defaults to 20. It is worth noticing that the number of working threads +-- will grow exponentially until any error occurs, after that the engine +-- will switch to linear growth. +-- +-- The library contains the following classes: +-- * Engine +-- ** The actual engine doing the brute-forcing . +-- * Error +-- ** Class used to return errors back to the engine. +-- * Options +-- ** Stores any options that should be used during brute-forcing. +-- +-- In order to make use of the framework a script needs to implement a Driver +-- class. The Driver class is then to be passed as a parameter to the Engine +-- constructor, which creates a new instance for each guess. The Driver class +-- SHOULD implement the following four methods: +-- +-- +-- Driver:login = function( self, username, password ) +-- Driver:check = function( self ) +-- Driver:connect = function( self ) +-- Driver:disconnect = function( self ) +-- +-- +-- The login method does not need a lot of explanation. The login +-- function should return two parameters. If the login was successful it should +-- return true and a creds.Account. If the login was a failure it +-- should return false and a brute.Error. The driver can signal +-- the Engine to retry a set of credentials by calling the Error objects +-- setRetry method. It may also signal the Engine to abort all +-- password guessing by calling the Error objects setAbort method. +-- Finally, the driver can notify the Engine about protocol related exception +-- (like the ftp code 421 "Too many connections") by calling +-- setReduce method. The latter will signal the Engine to reduce +-- the number of running worker threads. +-- +-- The following example code demonstrates how the Error object can be used. +-- +-- +-- -- After a number of incorrect attempts VNC blocks us, so we abort +-- if ( not(status) and x:match("Too many authentication failures") ) then +-- local err = brute.Error:new( data ) +-- -- signal the engine to abort +-- err:setAbort( true ) +-- return false, err +-- elseif ( not(status) ) then +-- local err = brute.Error:new( "VNC handshake failed" ) +-- -- This might be temporary, signal the engine to retry +-- err:setRetry( true ) +-- return false, err +-- end +-- . +-- . +-- . +-- -- Return a simple error, no retry needed +-- return false, brute.Error:new( "Incorrect password" ) +-- +-- +-- The purpose of the check method is to be able to determine +-- whether the script has all the information it needs, before starting the +-- brute force. It's the method where you should check, e.g., if the correct +-- database or repository URL was specified or not. On success, the +-- check method returns true, on failure it returns false and the +-- brute force engine aborts. +-- +-- NOTE: The check method is deprecated and will be removed from +-- all scripts in the future. Scripts should do this check in the action +-- function instead. +-- +-- The connect method provides the framework with the ability to +-- ensure that the thread can run once it has been dispatched a set of +-- credentials. As the sockets in NSE are limited we want to limit the risk of +-- a thread blocking, due to insufficient free sockets, after it has acquired a +-- username and password pair. +-- +-- The following sample code illustrates how to implement a sample +-- Driver that sends each username and password over a socket. +-- +-- +-- Driver = { +-- new = function(self, host, port, options) +-- local o = {} +-- setmetatable(o, self) +-- self.__index = self +-- o.host = host +-- o.port = port +-- o.options = options +-- return o +-- end, +-- connect = function( self ) +-- self.socket = nmap.new_socket() +-- return self.socket:connect( self.host, self.port ) +-- end, +-- disconnect = function( self ) +-- return self.socket:close() +-- end, +-- check = function( self ) +-- return true +-- end, +-- login = function( self, username, password ) +-- local status, err, data +-- status, err = self.socket:send( username .. ":" .. password) +-- status, data = self.socket:receive_bytes(1) +-- +-- if ( data:match("SUCCESS") ) then +-- return true, creds.Account:new(username, password, creds.State.VALID) +-- end +-- return false, brute.Error:new( "login failed" ) +-- end, +-- } +-- +-- +-- The following sample code illustrates how to pass the Driver +-- off to the brute engine. +-- +-- +-- action = function(host, port) +-- local options = { key1 = val1, key2 = val2 } +-- local status, accounts = brute.Engine:new(Driver, host, port, options):start() +-- if( not(status) ) then +-- return accounts +-- end +-- return stdnse.format_output( true, accounts ) +-- end +-- +-- +-- The Engine is written with performance and reasonable resource usage in mind +-- and requires minimum extra work from a script developer. A trivial approach +-- is to spawn as many working threads as possible regardless of network +-- conditions, other scripts' needs, and protocol response. This indeed works +-- well, but only in ideal conditions. In reality there might be several +-- scripts running or only limited number of threads are allowed to use sockets +-- at any given moment (as it is in Nmap). A more intelligent approach is to +-- automate the management of Engine's running threads, so that performance +-- of other scripts does not suffer because of exhaustive brute force work. +-- This can be done on three levels: protocol, network, and resource level. +-- +-- On the protocol level the developer should notify the Engine about connection +-- restrictions imposed by a server that can be learned during a protocol +-- communication. Like code 421 "To many connections" is used in FTP. Reasonably +-- in such cases we would like to reduce the number of connections to this +-- service, hence saving resources for other work and reducing the load on the +-- target server. This can be done by returning an Error object with called +-- setReduce method on it. The error will make the Engine reduce +-- the number of running threads. +-- +-- Following is an example how it can be done for FTP brute. +-- +-- +-- local line = +-- +-- if(string.match(line, "^230")) then +-- stdnse.debug1("Successful login: %s/%s", user, pass) +-- return true, creds.Account:new( user, pass, creds.State.VALID) +-- elseif(string.match(line, "^530")) then +-- return false, brute.Error:new( "Incorrect password" ) +-- elseif(string.match(line, "^421")) then +-- local err = brute.Error:new("Too many connections") +-- err:setReduce(true) +-- return false, err +-- elseif(string.match(line, "^220")) then +-- elseif(string.match(line, "^331")) then +-- else +-- stdnse.debug1("WARNING: Unhandled response: %s", line) +-- local err = brute.Error:new("Unhandled response") +-- err:setRetry(true) +-- return false, err +-- end +-- +-- +-- On the network level we want to catch errors that can occur because of +-- network congestion or target machine specifics, say firewalled. These +-- errors can be caught as return results of operations on sockets, like +-- local status, err = socket.receive(). Asking a developer to +-- relay such errors to the Engine is counterproductive, and it would lead to +-- bloated scripts with lots of repetitive code. The Engine takes care of that +-- with a little help from the developer. The only thing that needs to be +-- done is to use brute.new_socket() instead of +-- nmap.new_socket() when creating a socket in a script. +-- +-- NOTE: A socket created with brute.new_socket() will behave as +-- a regular socket when used without the brute library. The returned object +-- is a BruteSocket instance, which can be treated as a regular socket object. +-- +-- Example on creating "brute" socket. +-- +-- +-- connect = function( self ) +-- self.socket = brute.new_socket() +-- local status, err = self.socket:connect(self.host, self.port) +-- self.socket:set_timeout(arg_timeout) +-- if(not(status)) then +-- return false, brute.Error:new( "Couldn't connect to host: " .. err ) +-- end +-- return true +-- end +-- +-- +-- On the resource level the Engine can query the current status of the NSE. +-- As of the time of writing, the only parameter used is a number of threads +-- waiting for connection (as was said before the NSE has a constraint on the +-- number of concurrent connections due to performance reasons). With a +-- running brute script the limit can be hit pretty fast, which can affect +-- performance of other scripts. To mitigate this situation resource management +-- strategy is used, and the Engine will reduce the number of working threads +-- if there are any threads waiting for connection. As a result the preference +-- for connection will be given to non brute scripts and if there are many +-- brute scripts running simultaneously, then they will not exhaust resources +-- unnecessarily. +-- This feature is enabled by default and does not require any additional work +-- from the developer. +-- +-- Stagnation avoidance mechanism is implemented to alert users about services +-- that might have failed during bruteforcing. The Engine will abort if all working +-- threads have been experiencing connection errors during 100 consequentive +-- iterations of the main thread loop. If brute.killstagnated +-- is set to false the Engine will continue after issuing a +-- warning. +-- +-- For a complete example of a brute implementation consult the +-- svn-brute.nse or vnc-brute.nse scripts +-- +-- @args brute.useraspass guess the username as password for each user +-- (default: true) +-- @args brute.emptypass guess an empty password for each user +-- (default: false) +-- @args brute.unique make sure that each password is only guessed once +-- (default: true) +-- @args brute.firstonly stop guessing after first password is found +-- (default: false) +-- @args brute.passonly iterate over passwords only for services that provide +-- only a password for authentication. (default: false) +-- @args brute.retries the number of times to retry if recoverable failures +-- occur. (default: 2) +-- @args brute.delay the number of seconds to wait between guesses (default: 0) +-- @args brute.threads the number of initial worker threads, the number of +-- active threads will be automatically adjusted. +-- @args brute.mode can be user, pass or creds and determines what mode to run +-- the engine in. +-- * user - the unpwdb library is used to guess passwords, every password +-- password is tried for each user. (The user iterator is in the +-- outer loop) +-- * pass - the unpwdb library is used to guess passwords, each password +-- is tried for every user. (The password iterator is in the +-- outer loop) +-- * creds- a set of credentials (username and password pairs) are +-- guessed against the service. This allows for lists of known +-- or common username and password combinations to be tested. +-- If no mode is specified and the script has not added any custom +-- iterator the pass mode will be enabled. +-- @args brute.credfile a file containing username and password pairs delimited +-- by '/' +-- @args brute.guesses the number of guesses to perform against each account. +-- (default: 0 (unlimited)). The argument can be used to prevent account +-- lockouts. +-- @args brute.start the number of threads the engine will start with. +-- (default: 5). +-- +-- @author Patrik Karlsson +-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html + +-- +-- Version 0.73 +-- Created 06/12/2010 - v0.1 - created by Patrik Karlsson +-- Revised 07/13/2010 - v0.2 - added connect, disconnect methods to Driver +-- +-- Revised 07/21/2010 - v0.3 - documented missing argument brute.mode +-- Revised 07/23/2010 - v0.4 - fixed incorrect statistics and changed output to +-- include statistics, and to display "no accounts +-- found" message. +-- Revised 08/14/2010 - v0.5 - added some documentation and smaller changes per +-- David's request. +-- Revised 08/30/2010 - v0.6 - added support for custom iterators and did some +-- needed cleanup. +-- Revised 06/19/2011 - v0.7 - added support for creds library [Patrik] +-- Revised 07/07/2011 - v0.71- fixed some minor bugs, and changed credential +-- iterator to use a file handle instead of table +-- Revised 07/21/2011 - v0.72- added code to allow script reporting invalid +-- (non existing) accounts using setInvalidAccount +-- Revised 11/12/2011 - v0.73- added support for max guesses per account to +-- prevent account lockouts. +-- bugfix: added support for guessing the username +-- as password per default, as suggested by the +-- documentation. +-- Revised 07/11/2016 - v.8 - added smart resource management and error handling +-- mechanisms. Sergey Khegay + +local coroutine = require "coroutine" +local creds = require "creds" +local io = require "io" +local nmap = require "nmap" +local os = require "os" +local stdnse = require "stdnse" +local table = require "table" +local unpwdb = require "unpwdb" +local math = require "math" +_ENV = stdnse.module("brute", stdnse.seeall) + +-- Engine options that can be set by scripts +-- Supported options are: +-- * firstonly - stop after finding the first correct password +-- (can be set using script-arg brute.firstonly) +-- * passonly - guess passwords only, don't supply a username +-- (can be set using script-arg brute.passonly) +-- * max_retries - the amount of retries to do before aborting +-- (can be set using script-arg brute.retries) +-- * delay - sets the delay between attempts +-- (can be set using script-arg brute.delay) +-- * mode - can be set to either creds, user or pass and controls +-- whether the engine should iterate over users, passwords +-- or fetch a list of credentials from a single file. +-- (can be set using script-arg brute.mode) +-- * title - changes the title of the result table where the +-- passwords are returned. +-- * nostore - don't store the results in the credential library +-- * max_guesses - the maximum amount of guesses to perform for each +-- account. +-- * useraspass - guesses the username as password (default: true) +-- * emptypass - guesses an empty string as password (default: false) +-- * killstagnated - abort the Engine if bruteforcing has stagnated +-- getting too many connections errors. (default: true) +-- +Options = { + + new = function (self) + local o = {} + setmetatable(o, self) + self.__index = self + + o.emptypass = self.checkBoolArg("brute.emptypass", false) + o.useraspass = self.checkBoolArg("brute.useraspass", true) + o.firstonly = self.checkBoolArg("brute.firstonly", false) + o.passonly = self.checkBoolArg("brute.passonly", false) + o.killstagnated = self.checkBoolArg("brute.killstagnated", true) + o.max_retries = tonumber(stdnse.get_script_args("brute.retries")) or 2 + o.delay = tonumber(stdnse.get_script_args("brute.delay")) or 0 + o.max_guesses = tonumber(stdnse.get_script_args("brute.guesses")) or 0 + + return o + end, + + --- Checks if a script argument is boolean true or false + -- + -- @param arg string containing the name of the argument to check + -- @param default boolean containing the default value + -- @return boolean, true if argument evaluates to 1 or true, else false + checkBoolArg = function (arg, default) + local val = stdnse.get_script_args(arg) or default + return (val == "true" or val == true or tonumber(val) == 1) + end, + + --- Sets the brute mode to either iterate over users or passwords + -- @see description for more information. + -- + -- @param mode string containing either "user" or "password" + -- @return status true on success else false + -- @return err string containing the error message on failure + setMode = function (self, mode) + local modes = { + "password", + "user", + "creds", + } + local supported = false + + for _, m in ipairs(modes) do + if mode == m then + supported = true + end + end + + if not supported then + stdnse.debug1("ERROR: brute.options.setMode: mode %s not supported", mode) + return false, "Unsupported mode" + else + self.mode = mode + end + return true + end, + + --- Sets an option parameter + -- + -- @param param string containing the parameter name + -- @param value string containing the parameter value + setOption = function (self, param, value) + self[param] = value + end, + + --- Set an alternate title for the result output (default: Accounts) + -- + -- @param title string containing the title value + setTitle = function (self, title) + self.title = title + end, + +} + +-- The account object which is to be reported back from each driver +-- The Error class, is currently only used to flag for retries +-- It also contains the error message, if one was returned from the driver. +Error = { + retry = false, + + new = function (self, msg) + local o = { + msg = msg, + done = false, + reduce = nil, + } + setmetatable(o, self) + self.__index = self + return o + end, + + --- Is the error recoverable? + -- + -- @return status true if the error is recoverable, false if not + isRetry = function (self) + return self.retry + end, + + --- Set the error as recoverable + -- + -- @param r boolean true if the engine should attempt to retry the + -- credentials, unset or false if not + setRetry = function (self, r) + self.retry = r + end, + + --- Set the error as abort all threads + -- + -- @param b boolean true if the engine should abort guessing on all threads + setAbort = function (self, b) + self.abort = b + end, + + --- Was the error abortable + -- + -- @return status true if the driver flagged the engine to abort + isAbort = function (self) + return self.abort + end, + + --- Get the error message reported + -- + -- @return msg string containing the error message + getMessage = function (self) + return self.msg + end, + + --- Is the thread done? + -- + -- @return status true if done, false if not + isDone = function (self) + return self.done + end, + + --- Signals the engine that the thread is done and should be terminated + -- + -- @param b boolean true if done, unset or false if not + setDone = function (self, b) + self.done = b + end, + + -- Marks the username as invalid, aborting further guessing. + -- @param username + setInvalidAccount = function (self, username) + self.invalid_account = username + end, + + -- Checks if the error reported the account as invalid. + -- @return username string containing the invalid account + isInvalidAccount = function (self) + return self.invalid_account + end, + + --- Set the error as reduce the number of running threads + -- + -- @param r boolean true if should reduce, unset or false if not + setReduce = function (self, r) + self.reduce = r + end, + + --- Checks if the error signals to reduce the number of running threads + -- + -- @return status true if reduce, false otherwise + isReduce = function (self) + if self.reduce then + return true + end + return false + end, +} + +-- Auxillary data structure +Batch = { + new = function (self, lim, stime) + local o = { + limit = lim or 3, -- maximum number of items + full = false, + data = {}, -- storage + size = 0, -- current number of items + start_time = stime or 0, + } + setmetatable(o, self) + self.__index = self + return o + end, + + --- Adds new item to the vault (if possible) + -- + -- @param obj, new object + -- @return true if insert is successful, false if the vault is full + add = function (self, obj) + if self.size < self.limit then + self.data[self.size + 1] = obj + self.size = self.size + 1 + return true + end + + return false + end, + + isFull = function (self) + if self.size >= self.limit then + return true + end + + return false + end, + + getData = function (self) + return self.data + end, + + getSize = function (self) + return self.size + end, + + getStartTime = function (self) + return self.start_time + end, + + getLimit = function (self) + return self.limit + end, + + setLimit = function (self, lim) + self.limit = lim + end, +} + + +-- The brute engine, doing all the nasty work +Engine = { + STAT_INTERVAL = 20, + THREAD_TO_ENGINE = {}, + + --- Creates a new Engine instance + -- + -- @param driver, the driver class that should be instantiated + -- @param host table as passed to the action method of the script + -- @param port table as passed to the action method of the script + -- @param options table containing any script specific options + -- @return o new Engine instance + new = function (self, driver, host, port, options) + + -- we want Engine.THREAD_TO_ENGINE to contain weak keys + -- for effective garbage collection + if getmetatable(Engine.THREAD_TO_ENGINE) == nil then + setmetatable(Engine.THREAD_TO_ENGINE, { + __mode = "k", + }) + end + + local o = { + driver = driver, + host = host, + port = port, + driver_options = options, + terminate_all = false, + error = nil, + counter = 0, + threads = {}, + tps = {}, + iterator = nil, + usernames = usernames_iterator(), + passwords = passwords_iterator(), + found_accounts = {}, + account_guesses = {}, + options = Options:new(), + + retry_accounts = {}, + initial_accounts_exhausted = false, + batch = nil, + tick = 0, + } + setmetatable(o, self) + self.__index = self + + o.max_threads = tonumber(stdnse.get_script_args "brute.threads") or 20 + o.start_threads = tonumber(stdnse.get_script_args "brute.start") or 5 + + return o + end, + + --- Sets the username iterator + -- + -- @param usernameIterator function to set as a username iterator + setUsernameIterator = function (self, usernameIterator) + self.usernames = usernameIterator + end, + + --- Sets the password iterator + -- + -- @param passwordIterator function to set as a password iterator + setPasswordIterator = function (self, passwordIterator) + self.passwords = passwordIterator + end, + + --- Limit the number of worker threads + -- + -- @param max number containing the maximum number of allowed threads + setMaxThreads = function (self, max) + self.max_threads = max + end, + + --- Returns the number of non-dead threads + -- + -- @return count number of non-dead threads + threadCount = function (self) + local count = 0 + + for thread in pairs(self.threads) do + if coroutine.status(thread) == "dead" then + self.threads[thread] = nil + else + count = count + 1 + end + end + return count + end, + + --- Calculates the number of threads that are actually doing any work + -- + -- @return count number of threads performing activity + activeThreads = function (self) + local count = 0 + for thread, v in pairs(self.threads) do + if v.guesses ~= nil then + count = count + 1 + end + end + return count + end, + + --- Iterator wrapper used to iterate over all registered iterators + -- + -- @return iterator function + get_next_credential = function (self) + local function next_credential () + for user, pass in self.iterator do + -- makes sure the credentials have not been tested before + self.used_creds = self.used_creds or {} + pass = pass or "nil" + if not self.used_creds[user .. pass] then + self.used_creds[user .. pass] = true + coroutine.yield(user, pass) + end + end + while true do + coroutine.yield(nil, nil) + end + end + return coroutine.wrap(next_credential) + end, + + --- Does the actual authentication request + -- + -- @return true on success, false on failure + -- @return response Account on success, Error on failure + doAuthenticate = function (self) + + local status, response + local next_credential = self:get_next_credential() + local tries = self.options.max_retries + 1 + local username, password + local thread_data = Engine.getThreadData(coroutine.running()) + assert(thread_data, "Unknown coroutine is running") + + repeat + local driver = self.driver:new(self.host, self.port, self.driver_options) + + status, response = driver:connect() + + -- Temporary workaround. Did not connect successfully + -- due to stressed server + if not status then + -- We have to first check whether the response is a brute.Error + -- since many times the connect method returns a string error instead, + -- which could crash this thread in several places + if response and not response.isReduce then + -- Create a new Error + response = Error:new("Connect error: " .. response) + response:setRetry(true) + end + if response and response:isReduce() then + local ret_creds = {} + ret_creds.connect_phase = true + return false, response, ret_creds + end + else + -- Did we successfully connect? + if not username and not password then + repeat + if #self.retry_accounts > 0 then + -- stdnse.debug1("Using retry credentials") + username = self.retry_accounts[#self.retry_accounts].username + password = self.retry_accounts[#self.retry_accounts].password + table.remove(self.retry_accounts, #self.retry_accounts) + else + username, password = next_credential() + end + + thread_data.username = username + thread_data.password = password + + + if not username and not password then + driver:disconnect() + self.initial_accounts_exhausted = true + return false + end + until (not self.found_accounts or not self.found_accounts[username]) + and (self.options.max_guesses == 0 or not self.account_guesses[username] + or self.options.max_guesses > self.account_guesses[username]) + + -- increases the number of guesses for an account + self.account_guesses[username] = self.account_guesses[username] + and self.account_guesses[username] + 1 or 1 + end + + -- make sure that all threads locked in connect stat terminate quickly + if Engine.terminate_all then + driver:disconnect() + driver = nil + return false + end + + local c + -- Do we have a username or not? + if username and #username > 0 then + c = ("%s/%s"):format(username, #password > 0 and password or "") + else + c = ("%s"):format(#password > 0 and password or "") + end + + local msg = (tries <= self.options.max_retries) and "Re-trying" or "Trying" + stdnse.debug2("%s %s against %s:%d", msg, c, self.host.ip, self.port.number) + status, response = driver:login(username, password) + + driver:disconnect() + driver = nil + + if not status and response and response:isReduce() then + local ret_creds = {} + ret_creds.username = username + ret_creds.password = password + return false, response, ret_creds + end + + end + + tries = tries - 1 + + -- End if: + -- * The guess was successful + -- * The response was not set to retry + -- * We've reached the maximum retry attempts + until status or (response and not (response:isRetry())) or tries <= 0 + + -- Increase the amount of total guesses + self.counter = self.counter + 1 + + return status, response + end, + + + login = function (self, cvar) + local condvar = nmap.condvar(cvar) + local thread_data = self.threads[coroutine.running()] + local interval_start = os.time() + + + while true do + -- Should we terminate all threads or this particular thread? + if (self.terminate_all or thread_data.terminate) + or (self.initial_accounts_exhausted and #self.retry_accounts == 0) then + break + end + + -- Update tick and add this thread to the batch + self.tick = self.tick + 1 + + if not (self.batch:isFull()) and not thread_data.in_batch then + self.batch:add(coroutine.running()) + + thread_data.in_batch = true + thread_data.ready = false + end + + -- We expect doAuthenticate to pass the report variable received from the script + local status, response, ret_creds = self:doAuthenticate() + + if thread_data.in_batch then + thread_data.ready = true + end + + if status then + -- Prevent locked accounts from appearing several times + if not self.found_accounts or self.found_accounts[response.username] == nil then + if not self.options.nostore then + local c = creds.Credentials:new(self.options.script_name, self.host, self.port) + c:add(response.username, response.password, response.state) + else + self.credstore = self.credstore or {} + table.insert(self.credstore, tostring(response)) + end + + stdnse.debug1("Discovered account: %s", tostring(response)) + + -- if we're running in passonly mode, and want to continue guessing + -- we will have a problem as the username is always the same. + -- in this case we don't log the account as found. + if not self.options.passonly then + self.found_accounts[response.username] = true + end + + -- Check if firstonly option was set, if so abort all threads + if self.options.firstonly then + self.terminate_all = true + end + end + elseif ret_creds then + if not ret_creds.connect_phase then + -- add credentials to a vault + self.retry_accounts[#self.retry_accounts + 1] = { + username = ret_creds.username, + password = ret_creds.password, + } + end + -- notify the main thread that there were an error on this coroutine + thread_data.protocol_error = true + + condvar "signal" + condvar "wait" + else + if response and response:isAbort() then + self.terminate_all = true + self.error = response:getMessage() + break + elseif response and response:isDone() then + break + elseif response and response:isInvalidAccount() then + self.found_accounts[response:isInvalidAccount()] = true + end + end + + local timediff = (os.time() - interval_start) + + -- This thread made another guess + thread_data.guesses = (thread_data.guesses and thread_data.guesses + 1 or 1) + + -- Dump statistics at regular intervals + if timediff > Engine.STAT_INTERVAL then + interval_start = os.time() + local tps = self.counter / (os.time() - self.starttime) + table.insert(self.tps, tps) + stdnse.debug2("threads=%d,tps=%.1f", self:activeThreads(), tps) + end + + -- if delay was specified, do sleep + if self.options.delay > 0 then + stdnse.sleep(self.options.delay) + end + + condvar "signal" + end + + condvar "signal" + end, + + --- Adds new worker thread using start function + -- + -- @return new thread object + addWorker = function (self, cvar) + local co = stdnse.new_thread(self.login, self, cvar) + + Engine.THREAD_TO_ENGINE[co] = self + + self.threads[co] = { + running = true, + protocol_error = nil, + attempt = 0, + in_batch = false, + ready = false, + + connection_error = nil, + con_error_reason = nil, + username = nil, + password = nil, + } + + return co + end, + + addWorkerN = function (self, cvar, n) + assert(n >= 0) + for i = 1, n do + self:addWorker(cvar) + end + end, + + renewBatch = function (self) + if self.batch then + local size = self.batch:getSize() + local data = self.batch:getData() + + for i = 1, size do + if self.threads[data[i]] then + self.threads[data[i]].in_batch = false + self.threads[data[i]].ready = false + end + end + end + + self.batch = Batch:new(math.min(self:threadCount(), 3), self.tick) + end, + + readyBatch = function (self) + if not self.batch then + return false + end + + local n = self.batch:getSize() + local data = self.batch:getData() + + if n == 0 then + return false + end + + for i = 1, n do + if self.threads[data[i]] and coroutine.status(data[i]) ~= "dead" and self.threads[data[i]].in_batch then + if not self.threads[data[i]].ready then + return false + end + end + end + + return true + end, + + --- Starts the brute-force + -- + -- @return status true on success, false on failure + -- @return err string containing error message on failure + start = function (self) + + local cvar = {} + local condvar = nmap.condvar(cvar) + + assert(self.options.script_name, "SCRIPT_NAME was not set in options.script_name") + assert(self.port.number and self.port.protocol, "Invalid port table detected") + self.port.service = self.port.service or "unknown" + + -- Only run the check method if it exist. We should phase this out + -- in favor of a check in the action function of the script + if self.driver:new(self.host, self.port, self.driver_options).check then + -- check if the driver is ready! + local status, response = self.driver:new(self.host, self.port, self.driver_options):check() + if not status then + return false, response + end + end + + local usernames = self.usernames + local passwords = self.passwords + + if "function" ~= type(usernames) then + return false, ("Invalid usernames iterator: %s"):format(usernames) + end + if "function" ~= type(passwords) then + return false, ("Invalid passwords iterator: %s"):format(passwords) + end + + local mode = self.options.mode or stdnse.get_script_args "brute.mode" + + -- if no mode was given, but a credfile is present, assume creds mode + if not mode and stdnse.get_script_args "brute.credfile" then + if stdnse.get_script_args "userdb" or stdnse.get_script_args "passdb" then + return false, "\n ERROR: brute.credfile can't be used in combination with userdb/passdb" + end + mode = 'creds' + end + + -- Are we guessing against a service that has no username (eg. VNC) + if self.options.passonly then + local function single_user_iter (next) + local function next_user () + coroutine.yield "" + end + return coroutine.wrap(next_user) + end + -- only add this iterator if no other iterator was specified + if self.iterator == nil then + self.iterator = Iterators.user_pw_iterator(single_user_iter(), passwords) + end + elseif mode == 'creds' then + local credfile = stdnse.get_script_args "brute.credfile" + if not credfile then + return false, "No credential file specified (see brute.credfile)" + end + + local f = io.open(credfile, "r") + if not f then + return false, ("Failed to open credfile (%s)"):format(credfile) + end + + self.iterator = Iterators.credential_iterator(f) + elseif mode and mode == 'user' then + self.iterator = self.iterator or Iterators.user_pw_iterator(usernames, passwords) + elseif mode and mode == 'pass' then + self.iterator = self.iterator or Iterators.pw_user_iterator(usernames, passwords) + elseif mode then + return false, ("Unsupported mode: %s"):format(mode) + -- Default to the pw_user_iterator in case no iterator was specified + elseif self.iterator == nil then + self.iterator = Iterators.pw_user_iterator(usernames, passwords) + end + + if (not mode or mode == 'user' or mode == 'pass') and self.options.useraspass then + -- if we're only guessing passwords, this doesn't make sense + if not self.options.passonly then + self.iterator = unpwdb.concat_iterators( + Iterators.pw_same_as_user_iterator(usernames, "lower"), + self.iterator + ) + end + end + + if (not mode or mode == 'user' or mode == 'pass') and self.options.emptypass then + local function empty_pass_iter () + local function next_pass () + coroutine.yield "" + end + return coroutine.wrap(next_pass) + end + self.iterator = Iterators.account_iterator(usernames, empty_pass_iter(), mode or "pass") + end + + self.starttime = os.time() + + + -- How many threads should start? + local start_threads = self.start_threads + -- If there are already too many threads waiting for connection, + -- then start humbly with one thread + if nmap.socket.get_stats().connect_waiting > 0 then + start_threads = 1 + end + + -- Start `start_threads` number of threads + self:addWorkerN(cvar, start_threads) + self:renewBatch() + + local revive = false + local killed_one = false + local error_since_batch_start = false + local stagnation_count = 0 -- number of times when all threads are stopped because of exceptions + local quick_start = true + local stagnated = true + + -- Main logic loop + while true do + local thread_count = self:threadCount() + + -- should we stop + if thread_count <= 0 then + if (self.initial_accounts_exhausted and #self.retry_accounts == 0) or self.terminate_all then + break + else + -- there are some accounts yet to be checked, so revive the engine + revive = true + end + end + + -- Reset flags + killed_one = false + error_since_batch_start = false + + -- Are all the threads have any kind of mistake? + -- if not, then this variable will change to false after next loop + stagnated = true + + -- Run through all coroutines and check their statuses + -- if any mistake has happened kill one coroutine. + -- We do not actually kill a coroutine right-away, we just + -- signal it to finish work until some point an then die. + for co, v in pairs(self.threads) do + if not v.connection_error then + stagnated = false + end + + if v.protocol_error or v.connection_error then + if v.attempt >= self.batch:getStartTime() then + error_since_batch_start = true + end + + if not killed_one then + v.terminate = true + killed_one = true + + if v.protocol_error then + stdnse.debug2("Killed one thread because of PROTOCOL exception") + else + stdnse.debug2("Killed one thread because of CONNECTION exception") + end + end + + -- Remove error flags of the thread to let it continue to run + v.protocol_error = nil + v.connection_error = nil + else + -- If we got here, then at least one thread is running fine + -- and there is no connection stagnation + --stagnated = false + end + end + + if stagnated == true then + stagnation_count = stagnation_count + 1 + + -- If we get inside `if` below, then we are not making any + -- guesses for too long. In this case it is reasonable to stop + -- bruteforce. + if stagnation_count == 100 then + stdnse.debug1("WARNING: The service seems to have failed or is heavily firewalled... Consider aborting.") + if self.options.killstagnated then + self.error = "The service seems to have failed or is heavily firewalled..." + self.terminate_all = true + end + stagnation_count = 0 + end + else + stagnation_count = 0 + end + + -- `quick_start` changes to false only once since Engine starts + -- `quick_start` remains false till the end of the bruteforce. + if killed_one then + quick_start = false + end + + -- Check if we possibly exhaust resources. + if not killed_one then + local waiting = nmap.socket.get_stats().connect_waiting + + if waiting ~= 0 then + local kill_count = 1 + if waiting > 5 then + kill_count = math.max(math.floor(thread_count / 2), 1) + end + + for co, v in pairs(self.threads) do + if coroutine.status(co) ~= "dead" then + stdnse.debug2("Killed one because of RESOURCE management") + v.terminate = true + killed_one = true + + kill_count = kill_count - 1 + if kill_count == 0 then + break + end + end + end + end + + end + + -- Renew the batch if there was an error since we started to assemble the batch + -- or the batch's limit is unreachable with current number of threads + -- or when some thread does not change state to ready for too long + if error_since_batch_start + or not killed_one and thread_count < self.batch:getLimit() + or (thread_count > 0 and self.tick - self.batch:getStartTime() > 10) then + self:renewBatch() + end + + if (not killed_one and self.batch:isFull() and thread_count < self.max_threads) + or revive then + + local num_to_add = 1 + if quick_start then + num_to_add = math.min(self.max_threads - thread_count, thread_count) + end + + self:addWorkerN(cvar, num_to_add) + self:renewBatch() + revive = false + end + + + local threads = self:threadCount() + stdnse.debug2("Status: #threads = %d, #retry_accounts = %d, initial_accounts_exhausted = %s, waiting = %d", + threads, #self.retry_accounts, tostring(self.initial_accounts_exhausted), + nmap.socket.get_stats().connect_waiting) + + if threads > 0 then + -- wake up other threads + -- wait for all threads to finish running + condvar "broadcast" + condvar "wait" + end + end + + + local valid_accounts + + if not self.options.nostore then + valid_accounts = creds.Credentials:new(self.options.script_name, self.host, self.port):getTable() + else + valid_accounts = self.credstore + end + + local result = stdnse.output_table() + -- Did we find any accounts, if so, do formatting + if valid_accounts and #valid_accounts > 0 then + result[self.options.title or "Accounts"] = valid_accounts + else + result.Accounts = "No valid accounts found" + end + + -- calculate the average tps + local sum = 0 + for _, v in ipairs(self.tps) do + sum = sum + v + end + local time_diff = (os.time() - self.starttime) + time_diff = (time_diff == 0) and 1 or time_diff + local tps = (sum == 0) and (self.counter / time_diff) or (sum / #self.tps) + + -- Add the statistics to the result + result.Statistics = ("Performed %d guesses in %d seconds, average tps: %.1f"):format( self.counter, time_diff, tps ) + + if self.options.max_guesses > 0 then + -- we only display a warning if the guesses are equal to max_guesses + for user, guesses in pairs(self.account_guesses) do + if guesses == self.options.max_guesses then + result.Information = ("Guesses restricted to %d tries per account to avoid lockout"):format(self.options.max_guesses) + break + end + end + end + + -- Did any error occur? If so add this to the result. + if self.error then + result.ERROR = self.error + return false, result + end + return true, result + end, + + getEngine = function (co) + local engine = Engine.THREAD_TO_ENGINE[co] + if not engine then + stdnse.debug1("WARNING: No engine associated with %s", coroutine.running()) + end + return engine + end, + + getThreadData = function (co) + local engine = Engine.getEngine(co) + if not engine then + return nil + end + return engine.threads[co] + end, +} + +--- Default username iterator that uses unpwdb +-- +function usernames_iterator () + local status, usernames = unpwdb.usernames() + if not status then + return usernames or "Failed to load usernames" + end + return usernames +end + +--- Default password iterator that uses unpwdb +-- +function passwords_iterator () + local status, passwords = unpwdb.passwords() + if not status then + return passwords or "Failed to load passwords" + end + return passwords +end + +Iterators = { + + --- Iterates over each user and password + -- + -- @param users table/function containing list of users + -- @param pass table/function containing list of passwords + -- @param mode string, should be either 'user' or 'pass' and controls + -- whether the users or passwords are in the 'outer' loop + -- @return function iterator + account_iterator = function (users, pass, mode) + local function next_credential () + local outer, inner + if "table" == type(users) then + users = unpwdb.table_iterator(users) + end + if "table" == type(pass) then + pass = unpwdb.table_iterator(pass) + end + + if mode == 'pass' then + outer, inner = pass, users + elseif mode == 'user' then + outer, inner = users, pass + else + return + end + + for o in outer do + for i in inner do + if mode == 'pass' then + coroutine.yield(i, o) + else + coroutine.yield(o, i) + end + end + inner "reset" + end + while true do + coroutine.yield(nil, nil) + end + end + return coroutine.wrap(next_credential) + end, + + + --- Try each password for each user (user in outer loop) + -- + -- @param users table/function containing list of users + -- @param pass table/function containing list of passwords + -- @return function iterator + user_pw_iterator = function (users, pass) + return Iterators.account_iterator(users, pass, "user") + end, + + --- Try each user for each password (password in outer loop) + -- + -- @param users table/function containing list of users + -- @param pass table/function containing list of passwords + -- @return function iterator + pw_user_iterator = function (users, pass) + return Iterators.account_iterator(users, pass, "pass") + end, + + --- An iterator that returns the username as password + -- + -- @param users function returning the next user + -- @param case string [optional] 'upper' or 'lower', specifies if user + -- and password pairs should be case converted. + -- @return function iterator + pw_same_as_user_iterator = function (users, case) + local function next_credential () + for user in users do + if case == 'upper' then + coroutine.yield(user, user:upper()) + elseif case == 'lower' then + coroutine.yield(user, user:lower()) + else + coroutine.yield(user, user) + end + end + users "reset" + while true do + coroutine.yield(nil, nil) + end + end + return coroutine.wrap(next_credential) + end, + + --- An iterator that returns the username and uppercase password + -- + -- @param users table containing list of users + -- @param pass table containing list of passwords + -- @param mode string, should be either 'user' or 'pass' and controls + -- whether the users or passwords are in the 'outer' loop + -- @return function iterator + pw_ucase_iterator = function (users, passwords, mode) + local function next_credential () + for user, pass in Iterators.account_iterator(users, passwords, mode) do + coroutine.yield(user, pass:upper()) + end + while true do + coroutine.yield(nil, nil) + end + end + return coroutine.wrap(next_credential) + end, + + --- Credential iterator (for default or known user/pass combinations) + -- + -- @param f file handle to file containing credentials separated by '/' + -- @return function iterator + credential_iterator = function (f) + local function next_credential () + local c = {} + for line in f:lines() do + if not (line:match "^#!comment:") then + local trim = function (s) + return s:match '^()%s*$' and '' or s:match '^%s*(.*%S)' + end + line = trim(line) + local user, pass = line:match "^([^%/]*)%/(.*)$" + coroutine.yield(user, pass) + end + end + f:close() + while true do + coroutine.yield(nil, nil) + end + end + return coroutine.wrap(next_credential) + end, + + unpwdb_iterator = function (mode) + local status, users, passwords + + status, users = unpwdb.usernames() + if not status then + return + end + + status, passwords = unpwdb.passwords() + if not status then + return + end + + return Iterators.account_iterator(users, passwords, mode) + end, + +} + +-- These functions all return a boolean and an error (or result) +-- and should all be wrapped in order to check status of the engine. +checkwrap = { + connect = true, + send = true, + receive = true, + receive_lines = true, + receive_buf = true, + receive_bytes = true, +} + +-- A socket wrapper class. +-- Instances of this class can be treated as regular sockets. +-- This wrapper is used to relay connection errors to the corresponding Engine +-- instance. +BruteSocket = { + new = function (self) + local o = { + socket = nil, + } + setmetatable(o, self) + + self.__index = function (instance, key) + local f = rawget(self, key) + if f then + -- BruteSocket function + return f + else + -- something provided by NSE socket + f = instance.socket[key] + end + -- Check if it should be wrapped with a checkStatus call + if checkwrap[key] then + return function(s, ...) + local status, err = f(instance.socket, ...) + instance:checkStatus(status, err) + return status, err + end + elseif type(f) == "function" then + -- not wrapped? call the function on the underlying socket + return function (s, ...) + return f(instance.socket, ...) + end + end + return f + end + + o.socket = nmap.new_socket() + + return o + end, + + getSocket = function (self) + return self.socket + end, + + checkStatus = function (self, status, err) + if not status and (err == "ERROR" or err == "TIMEOUT") then + local engine = Engine.getEngine(coroutine.running()) + + if not engine then + stdnse.debug2("WARNING: No associated engine detected for %s", coroutine.running()) + return -- behave like a usual socket + end + + local thread_data = Engine.getThreadData(coroutine.running()) + + engine.retry_accounts[#engine.retry_accounts + 1] = { + username = thread_data.username, + password = thread_data.password, + } + + thread_data.connection_error = true + thread_data.con_error_reason = err + end + end, +} + +function new_socket () + return BruteSocket:new() +end + + +return _ENV -- cgit v1.2.3