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, + +} +