---
-- The brute library is an attempt to create a common framework for performing
-- password guessing against remote services.
--
-- The library currently attempts to parallellize the guessing by starting
-- a number of working threads. The number of threads can be defined using
-- the brute.threads argument, it defaults to 10.
--
-- The library contains the following classes:
-- * Account
-- ** Implements a simple account class, that converts account "states" to common text representation.
-- ** The state can be either of the following: OPEN, LOCKED or DISABLED
-- * 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 an Account
. If the login was a failure it
-- should return false and an 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.
--
-- 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 aquired 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, brute.Account:new(username, password, "OPEN")
-- 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
--
--
-- 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.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
-- occure. (default: 3)
-- @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 '/'
-- @author "Patrik Karlsson "
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
--
-- Version 0.7
-- 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
module(... or "brute", package.seeall)
require 'unpwdb'
require 'datafiles'
require 'creds'
-- 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 cred, 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 brut.mode)
-- * title - changes the title of the result table where the
-- passwords are returned.
-- * nostore - don't store the results in the credential library
--
Options = {
new = function(self)
local o = {}
setmetatable(o, self)
self.__index = self
o.firstonly = self.checkBoolArg("brute.firstonly", false)
o.passonly = self.checkBoolArg("brute.passonly", false)
o.max_retries = tonumber( nmap.registry.args["brute.retries"] ) or 3
o.delay = tonumber( nmap.registry.args["brute.delay"] ) 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 = nmap.registry.args[arg]
if ( not(val) ) then return default end
return ( val == "true" or val=="1" ) and true or false
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.print_debug("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
Account =
{
--- Creates a new instance of the Account class
--
-- @param username containing the user's name
-- @param password containing the user's password
-- @param state containing the account state and should be one of the
-- following OPEN
, LOCKED
,
-- DISABLED
.
new = function(self, username, password, state)
local o = { username = username, password = password, state = state }
setmetatable(o, self)
self.__index = self
return o
end,
--- Converts an account object to a printable script
--
-- @return string representation of object
toString = function( self )
local c
if ( #self.username > 0 ) then
c = ("%s:%s"):format( self.username, #self.password > 0 and self.password or "" )
else
c = ("%s"):format( ( self.password and #self.password > 0 ) and self.password or "" )
end
if ( creds.StateMsg[self.state] ) then
return ( "%s - %s"):format(c, creds.StateMsg[self.state] )
else
return ("%s"):format(c)
end
end,
}
-- 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 }
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,
}
-- The brute engine, doing all the nasty work
Engine =
{
STAT_INTERVAL = 20,
--- 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)
local o = {
driver = driver,
host = host,
port = port,
driver_options = options,
terminate_all = false,
error = nil,
counter = 0,
threads = {},
tps = {},
iterators = {},
found_accounts = {},
options = Options:new(),
}
setmetatable(o, self)
self.__index = self
o.max_threads = stdnse.get_script_args("brute.threads") or 10
return o
end,
--- Adds an iterator to the list
--
-- @param iterator function to add to the list
addIterator = function( self, iterator )
table.insert( self.iterators, iterator )
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 ()
-- iterate over all credential iterators
for _, iter in ipairs( self.iterators ) do
for user, pass in iter do
-- makes sure the credentials have not been tested before
self.used_creds = self.used_creds or {}
if ( not(self.used_creds[user..pass]) ) then
self.used_creds[user..pass] = true
coroutine.yield( user, pass )
end
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 retries = self.options.max_retries
repeat
local driver = self.driver:new( self.host, self.port, self.driver_options )
status = driver:connect()
-- Did we succesfully connect?
if ( status ) then
local username, password
if ( not(username) and not(password) ) then
repeat
username, password = next_credential()
if ( not(username) and not(password) ) then
driver:disconnect()
self.threads[coroutine.running()].terminate = true
return false
end
until ( not(self.found_accounts) or not(self.found_accounts[username]) )
end
-- make sure that all threads locked in connect stat terminate quickly
if ( Engine.terminate_all ) then
driver:disconnect()
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 = ( retries ~= self.options.max_retries ) and "Re-trying" or "Trying"
stdnse.print_debug(2, "%s %s against %s:%d", msg, c, self.host.ip, self.port.number )
status, response = driver:login( username, password )
driver:disconnect()
driver = nil
end
retries = retries - 1
-- End if:
-- * The guess was successfull
-- * The response was not set to retry
-- * We've reached the maximum retry attempts
until( status or ( response and not( response:isRetry() ) ) or retries == 0)
-- Increase the amount of total guesses
self.counter = self.counter + 1
-- did we exhaust all retries, terminate and report?
if ( retries == 0 ) then
Engine.terminate_all = true
self.error = "Too many retries, aborted ..."
end
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?
if ( self.terminate_all or thread_data.terminate ) then break end
local status, response = self:doAuthenticate()
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
creds.Credentials:new( self.options.script_name, self.host, self.port ):add(response.username, response.password, response.state )
else
self.credstore = self.credstore or {}
table.insert(self.credstore, response:toString() )
end
stdnse.print_debug("Discovered account: %s", response:toString())
-- 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
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.print_debug(2, "threads=%d,tps=%d", self:activeThreads(), tps )
end
-- if delay was speciefied, do sleep
if ( self.options.delay > 0 ) then stdnse.sleep( self.options.delay ) end
end
condvar "signal"
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 and self.port.service, "Invalid port table detected")
-- 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 status, usernames = unpwdb.usernames()
if ( not(status) ) then return false, "Failed to load usernames" end
-- make sure we have a valid pw file
local status, passwords = unpwdb.passwords()
if ( not(status) ) then return false, "Failed to load passwords" end
local mode = self.options.mode or stdnse.get_script_args("brute.mode")
-- 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.iterators == 0 ) then
table.insert( self.iterators, 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
table.insert( self.iterators, Iterators.credential_iterator( f ) )
elseif ( mode and mode == 'user' ) then
table.insert( self.iterators, Iterators.user_pw_iterator( usernames, passwords ) )
elseif( mode and mode == 'pass' ) then
table.insert( self.iterators, 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 ( 0 == #self.iterators ) then
table.insert( self.iterators, Iterators.pw_user_iterator( usernames, passwords ) )
end
self.starttime = os.time()
-- Startup all worker threads
for i=1, self.max_threads do
local co = stdnse.new_thread( self.login, self, cvar )
self.threads[co] = {}
self.threads[co].running = true
end
-- wait for all threads to finnish running
while self:threadCount()>0 do condvar "wait" 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 = {}
-- Did we find any accounts, if so, do formatting
if ( valid_accounts and #valid_accounts > 0 ) then
valid_accounts.name = self.options.title or "Accounts"
table.insert( result, valid_accounts )
else
table.insert( result, {"No valid accounts found", name="Accounts"} )
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
local stats = {}
table.insert(stats, ("Performed %d guesses in %d seconds, average tps: %d"):format( self.counter, time_diff, tps ) )
stats.name = "Statistics"
table.insert( result, stats )
result = ( #result ) and stdnse.format_output( true, result ) or ""
-- Did any error occure? If so add this to the result.
if ( self.error ) then
result = result .. (" \n ERROR: %s"):format( self.error )
return false, result
end
return true, result
end,
}
Iterators = {
--- Iterates over each user and 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
account_iterator = function(users, pass, mode)
local function next_credential ()
local outer, inner
if ( mode == 'pass' ) then
outer, inner = pass, users
elseif ( mode == 'user' ) then
outer, inner = users, pass
else
return
end
if ( 'table' == type(users) and 'table' == type(pass) ) then
for _, o in ipairs(outer) do
for _, i in ipairs(inner) do
if ( mode == 'pass' ) then
coroutine.yield( i, o )
else
coroutine.yield( o, i )
end
end
end
elseif ( 'function' == type(users) and 'function' == type(pass) ) then
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
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 containing list of users
-- @param pass table 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 containing list of users
-- @param pass table 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 table containing list of users
-- @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 ipairs(users) do
if ( case == 'upper' ) then
coroutine.yield( user:upper(), user:upper() )
elseif( case == 'lower' ) then
coroutine.yield( user:lower(), user:lower() )
else
coroutine.yield( user, user )
end
end
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,
}