1552 lines
51 KiB
Lua
1552 lines
51 KiB
Lua
---
|
|
-- 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:
|
|
-- * <code>Engine</code>
|
|
-- ** The actual engine doing the brute-forcing .
|
|
-- * <code>Error</code>
|
|
-- ** Class used to return errors back to the engine.
|
|
-- * <code>Options</code>
|
|
-- ** 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:
|
|
--
|
|
-- <code>
|
|
-- Driver:login = function( self, username, password )
|
|
-- Driver:check = function( self )
|
|
-- Driver:connect = function( self )
|
|
-- Driver:disconnect = function( self )
|
|
-- </code>
|
|
--
|
|
-- The <code>login</code> 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 <code>creds.Account</code>. If the login was a failure it
|
|
-- should return false and a <code>brute.Error</code>. The driver can signal
|
|
-- the Engine to retry a set of credentials by calling the Error objects
|
|
-- <code>setRetry</code> method. It may also signal the Engine to abort all
|
|
-- password guessing by calling the Error objects <code>setAbort</code> method.
|
|
-- Finally, the driver can notify the Engine about protocol related exception
|
|
-- (like the ftp code 421 "Too many connections") by calling
|
|
-- <code>setReduce</code> 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.
|
|
--
|
|
-- <code>
|
|
-- -- 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" )
|
|
-- </code>
|
|
--
|
|
-- The purpose of the <code>check</code> 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
|
|
-- <code>check</code> method returns true, on failure it returns false and the
|
|
-- brute force engine aborts.
|
|
--
|
|
-- NOTE: The <code>check</code> method is deprecated and will be removed from
|
|
-- all scripts in the future. Scripts should do this check in the action
|
|
-- function instead.
|
|
--
|
|
-- The <code>connect</code> 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
|
|
-- <code>Driver</code> that sends each username and password over a socket.
|
|
--
|
|
-- <code>
|
|
-- 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,
|
|
-- }
|
|
-- </code>
|
|
--
|
|
-- The following sample code illustrates how to pass the <code>Driver</code>
|
|
-- off to the brute engine.
|
|
--
|
|
-- <code>
|
|
-- 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
|
|
-- </code>
|
|
--
|
|
-- 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
|
|
-- <code>setReduce</code> 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.
|
|
--
|
|
-- <code>
|
|
-- local line = <response from the server>
|
|
--
|
|
-- 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
|
|
-- </code>
|
|
--
|
|
-- 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
|
|
-- <code>local status, err = socket.receive()</code>. 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 <code>brute.new_socket()</code> instead of
|
|
-- <code>nmap.new_socket()</code> when creating a socket in a script.
|
|
--
|
|
-- NOTE: A socket created with <code>brute.new_socket()</code> 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.
|
|
--
|
|
-- <code>
|
|
-- 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
|
|
-- </code>
|
|
--
|
|
-- 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 <code>brute.killstagnated</code>
|
|
-- is set to <code>false</code> the Engine will continue after issuing a
|
|
-- warning.
|
|
--
|
|
-- For a complete example of a brute implementation consult the
|
|
-- <code>svn-brute.nse</code> or <code>vnc-brute.nse</code> 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 <patrik@cqure.net>
|
|
-- @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 <patrik@cqure.net>
|
|
-- Revised 07/13/2010 - v0.2 - added connect, disconnect methods to Driver
|
|
-- <patrik@cqure.net>
|
|
-- 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 <g.sergeykhegay@gmail.com>
|
|
|
|
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 "<empty>")
|
|
else
|
|
c = ("%s"):format(#password > 0 and password or "<empty>")
|
|
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
|