diff --git a/CHANGELOG b/CHANGELOG
index 908413db9..5444bc8dc 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*-
+o [NSE] Added a new brute library that provides a basic framework and logic
+ for password guessing scripts. [Patrik]
+
o [NSE] Renamed db2-info and db2-brute scripts to drda-*. Updated script
and library to reflect name change. Added support other DRDA based
databases such as IBM Informix Dynamic Server and Apache Derby.
diff --git a/nselib/brute.lua b/nselib/brute.lua
new file mode 100644
index 000000000..b39bc258c
--- /dev/null
+++ b/nselib/brute.lua
@@ -0,0 +1,716 @@
+---
+-- The brute library is an attempt to create a common framework for performing
+-- password guessing against remote services.
+--
+--
+-- Summary
+-- -------
+-- 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.
+--
+-- Overview
+-- --------
+-- The library contains the following classes:
+--
+-- o Account
+-- - Implmements a simple account class, that converts account "states"
+-- to common text representation.
+--
+-- o Engine
+-- - The actual engine doing the brute-forcing
+--
+-- o Error
+-- - Class used to return errors back to the engine
+--
+-- o 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 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 eg. 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.
+--
+-- 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.
+--
+-- Example
+-- -------
+-- 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.ip, self.port.number, "tcp" )
+-- 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
+--
+-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
+-- @author "Patrik Karlsson "
+--
+--
+-- @args brute.emptypass guess an empty password for each user (default: true)
+-- @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 or pass and determines if passwords are guessed
+-- against users (user) or users against passwords (pass).
+-- (default: pass)
+
+--
+-- Version 0.5
+-- 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.
+
+module(... or "brute", package.seeall)
+require 'unpwdb'
+
+-- Options that can be set through --script-args
+Options = {
+
+ mode = "password",
+
+ new = function(self)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.empty_password = self.checkBoolArg("brute.emptypass", true)
+ o.user_as_password = self.checkBoolArg("brute.useraspass", true)
+ o.check_unique = self.checkBoolArg("brute.unique", true)
+ 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
+ elseif ( val == "true" or val=="1" ) then
+ return true
+ else
+ return false
+ end
+ 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 )
+ if ( mode == "password" or mode == "user" ) then
+ self.mode = mode
+ else
+ stdnse.print_debug("ERROR: brute.options.setMode: mode %s not supported", mode)
+ return false, "Unsupported 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,
+
+}
+
+-- The account object which is to be reported back from each driver
+Account =
+{
+ new = function(self, username, password, state)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.username = username
+ o.password = password
+ o.state = state
+ return o
+ end,
+
+ --- Converts an account object to a printable script
+ --
+ -- @return string representation of object
+ toString = function( self )
+ local creds
+
+ if ( #self.username > 0 ) then
+ creds = ("%s:%s"):format( self.username, #self.password > 0 and self.password or "" )
+ else
+ creds = ("%s"):format( self.password )
+ end
+
+ -- An account have the following states
+ --
+ -- OPEN - Login was successful
+ -- LOCKED - The account was locked
+ -- DISABLED - The account was disabled
+ if ( self.state == "OPEN" ) then
+ return ("%s => Login correct"):format( creds )
+ elseif ( self.state == "LOCKED" ) then
+ return ("%s => Account locked"):format( creds )
+ elseif ( self.state == "DISABLED" ) then
+ return ("%s => Account disabled"):format( creds )
+ else
+ return ("%s => Account has unknown state (%s)"):format( creds, self.state )
+ 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 = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.msg = msg
+ o.done = false
+ return o
+ end,
+
+ --- Is the error recoverable?
+ isRetry = function( self )
+ return self.retry
+ end,
+
+ --- Set the error as recoverable
+ setRetry = function( self, r )
+ self.retry = r
+ end,
+
+ --- Set the error as abort all threads
+ setAbort = function( self, b )
+ self.abort = b
+ end,
+
+ --- Was the error abortable
+ isAbort = function( self )
+ return self.abort
+ end,
+
+ --- Get the error message reported
+ getMessage = function( self )
+ return self.msg
+ end,
+
+ isThreadDone = function( self )
+ return self.done
+ end,
+
+ setDone = function( self, b )
+ self.done = b
+ end,
+
+}
+
+-- The brute engine, doing all the nasty work
+Engine =
+{
+ STAT_INTERVAL = 20,
+ terminate_all = false,
+
+ --- 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 = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.driver = driver
+ o.driver_options = options
+ o.host = host
+ o.port = port
+ o.options = Options:new()
+ o.found_accounts = {}
+ o.threads = {}
+ o.counter = 0
+ o.max_threads = tonumber(nmap.registry.args["brute.threads"]) or 10
+ o.error = nil
+ o.tps = {}
+ return o
+ 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,
+
+ --- Does the actual authentication request
+ --
+ -- @return true on success, false on failure
+ -- @return response Account on success, Error on failure
+ doAuthenticate = function( self )
+
+ local driver, status, response, creds
+ local username, password
+ local retries = self.options.max_retries
+ local msg
+
+ repeat
+ driver = self.driver:new( self.host, self.port, self.driver_options )
+ status = driver:connect()
+
+ -- Did we succesfully connect?
+ if ( status ) then
+
+ if ( not(username) and not(password) ) then
+ username, password = self.iterator()
+ end
+
+ -- make sure that all threads locked in connect stat terminate quickly
+ if ( Engine.terminate_all ) then
+ driver:disconnect()
+ return false
+ end
+
+ -- We've reached the end of the iterator, signal the thread to terminate
+ if ( not(password) ) then
+ driver:disconnect()
+ self.threads[coroutine.running()].terminate = true
+ return false
+ end
+
+ -- The username was already tested
+ if ( self.found_accounts and self.found_accounts[username] ) then
+ driver:disconnect()
+ return false
+ end
+
+ -- Do we have a username or not?
+ if ( username and #username > 0 ) then
+ creds = ("%s/%s"):format(username, #password > 0 and password or "")
+ else
+ creds = ("%s"):format(#password > 0 and password or "")
+ end
+
+ -- Is this the first try?
+ if ( retries ~= self.options.max_retries ) then
+ msg = "Re-trying"
+ else
+ msg = "Trying"
+ end
+
+ stdnse.print_debug( "%s %s against %s:%d", msg, creds, 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)
+
+ -- 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, valid_accounts )
+ local username, password, creds
+ local status, response, driver
+ local interval_start, timediff = os.time(), nil
+ local condvar = nmap.condvar( valid_accounts )
+ local thread_data = self.threads[coroutine.running()]
+
+ while( true ) do
+
+ -- Should we terminate all threads?
+ if ( Engine.terminate_all or thread_data.terminate ) then
+ break
+ end
+
+ 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 ( response.username and #response.username > 0 ) then
+ stdnse.print_debug("Found valid password %s:%s on target %s", response.username, response.password, self.host.ip )
+ else
+ stdnse.print_debug("Found valid password %s on target %s", response.password, self.host.ip )
+ end
+ table.insert( valid_accounts, response:toString() )
+ self.found_accounts[response.username] = true
+ -- Check if firstonly option was set, if so abort all threads
+ if ( self.options.firstonly ) then
+ Engine.terminate_all = true
+ end
+ end
+ else
+ if ( response and response:isAbort() ) then
+ Engine.terminate_all = true
+ break
+ elseif( response and response:isThreadDone() ) then
+ break
+ end
+ end
+
+ -- Increase the amount of total guesses
+ self.counter = self.counter + 1
+ 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("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("broadcast")
+ 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 status, usernames, passwords, response
+ local result, valid_accounts, stats = {}, {}, {}
+ local condvar = nmap.condvar( valid_accounts )
+ local sum, tps, time_diff = 0, 0, 0
+
+ -- check if the driver is ready!
+ status, response = self.driver:new( self.host, self.port ):check()
+ if( not(status) ) then
+ return false, response
+ end
+
+ status, usernames = unpwdb.usernames()
+ if ( not(status) ) then
+ return false, "Failed to load usernames"
+ end
+
+ -- make sure we have a valid pw file
+ status, passwords = unpwdb.passwords()
+ if ( not(status) ) then
+ return false, "Failed to load passwords"
+ 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
+ self.iterator = Engine.usrpwd_iterator( self, single_user_iter(), passwords )
+ elseif ( nmap.registry.args['brute.mode'] and nmap.registry.args['brute.mode'] == 'user' ) then
+ self.iterator = Engine.usrpwd_iterator( self, usernames, passwords )
+ elseif( nmap.registry.args['brute.mode'] and nmap.registry.args['brute.mode'] == 'pass' ) then
+ self.iterator = Engine.pwdusr_iterator( self, usernames, passwords )
+ elseif ( nmap.registry.args['brute.mode'] ) then
+ return false, ("Unsupported mode: %s"):format(nmap.registry.args['brute.mode'])
+ else
+ self.iterator = Engine.pwdusr_iterator( self, 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, valid_accounts )
+ self.threads[co] = {}
+ self.threads[co].running = true
+ end
+
+ -- wait for all threads to finnish running
+ while self:threadCount()>0 do
+ condvar("wait")
+ end
+
+ -- Did we find any accounts, if so, do formatting
+ if ( #valid_accounts > 0 ) then
+ valid_accounts.name = "Accounts"
+ table.insert( result, valid_accounts )
+ else
+ table.insert( result, {"No valid accounts found", name="Accounts"} )
+ end
+
+ -- calculate the average tps
+ for _, v in ipairs( self.tps ) do
+ sum = sum + v
+ end
+ time_diff = ( os.time() - self.starttime )
+ if ( time_diff == 0 ) then time_diff = 1 end
+ if ( sum == 0 ) then
+ tps = self.counter / time_diff
+ else
+ tps = sum / #self.tps
+ end
+
+ -- Add the statistics to the result
+ table.insert(stats, ("Perfomed %d guesses in %d seconds, average tps: %d"):format( self.counter, time_diff, tps ) )
+ stats.name = "Statistics"
+ table.insert( result, stats )
+
+ if ( #result ) then
+ result = stdnse.format_output( true, result )
+ else
+ result = ""
+ end
+
+ -- Did any error occure? If so add this to the result.
+ if ( self.error ) then
+ result = result .. (" \n\n ERROR: %s"):format( self.error )
+ return false, result
+ end
+
+ return true, result
+ end,
+
+ --- Credential iterator, tries every user for each password
+ --
+ -- @param usernames iterator from unpwdb
+ -- @param passwords iterator from unpwdb
+ -- @return username string
+ -- @return password string
+ pwdusr_iterator = function(self, usernames, passwords)
+ local function next_password_username ()
+ local tested_creds = {}
+
+ -- should we check for empty passwords?
+ if ( self.options.empty_password ) then
+ for username in usernames do
+ if ( not(tested_creds[username]) ) then
+ tested_creds[username] = {}
+ end
+ tested_creds[username][""] = true
+ if ( not(self.found_accounts) or not(self.found_accounts[username]) ) then
+ coroutine.yield(username, "")
+ end
+ end
+ end
+ usernames("reset")
+
+ -- should we check for same password as username
+ if ( self.options.user_as_password ) then
+ for username in usernames do
+ if ( not( tested_creds[username] ) ) then
+ tested_creds[username] = {}
+ end
+
+ tested_creds[username][username] = true
+ if ( not(self.found_accounts) or not(self.found_accounts[username]) ) then
+ coroutine.yield(username, username)
+ end
+ end
+ end
+ usernames("reset")
+
+ for password in passwords do
+ for username in usernames do
+ if ( not(tested_creds[username]) ) then
+ tested_creds[username] = {}
+ end
+ if ( self.options.check_unique and not(tested_creds[username][password]) ) then
+ tested_creds[username][password] = true
+ if ( not(self.found_accounts) or not(self.found_accounts[username]) ) then
+ coroutine.yield(username, password)
+ end
+ end
+ end
+ usernames("reset")
+ end
+ while true do coroutine.yield(nil, nil) end
+ end
+ return coroutine.wrap(next_password_username)
+ end,
+
+ --- Credential iterator, tries every password for each user
+ --
+ -- @param usernames iterator from unpwdb
+ -- @param passwords iterator from unpwdb
+ -- @return username string
+ -- @return password string
+ usrpwd_iterator = function(self, usernames, passwords)
+ local function next_username_password ()
+ local tested_creds = {}
+
+ for username in usernames do
+ -- set's up a table to track tested credentials
+ tested_creds[username] = {}
+
+ -- should we check for empty passwords?
+ if ( self.options.empty_password ) then
+ tested_creds[username][""] = true
+ if ( not(self.found_accounts) or not(self.found_accounts[username]) ) then
+ coroutine.yield(username, "")
+ end
+ end
+
+ -- should we check for same password as username
+ if ( self.options.user_as_password and not(self.options.passonly) ) then
+ tested_creds[username][username:lower()] = true
+ if ( not(self.found_accounts) or not(self.found_accounts[username]) ) then
+ coroutine.yield(username, username:lower())
+ end
+ end
+
+ for password in passwords do
+ if ( self.options.check_unique and not(tested_creds[username][password]) ) then
+ tested_creds[username][password] = true
+ if ( not(self.found_accounts) or not(self.found_accounts[username]) ) then
+ coroutine.yield(username, password)
+ end
+ end
+ end
+ passwords("reset")
+ end
+ while true do coroutine.yield(nil, nil) end
+ end
+ return coroutine.wrap(next_username_password)
+ end,
+
+}
+