Index: nselib/brute.lua =================================================================== --- nselib/brute.lua (revision 20030) +++ nselib/brute.lua (arbetskopia) @@ -64,8 +64,12 @@ -- 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. +-- 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 @@ -143,12 +147,24 @@ -- @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) +-- @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 '/' -- --- Version 0.5 +-- Version 0.6 -- Created 06/12/2010 - v0.1 - created by Patrik Karlsson -- Revised 07/13/2010 - v0.2 - added connect, disconnect methods to Driver -- @@ -158,9 +174,12 @@ -- 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. module(... or "brute", package.seeall) require 'unpwdb' +require 'datafiles' -- Options that can be set through --script-args Options = { @@ -350,11 +369,21 @@ o.threads = {} o.counter = 0 o.max_threads = tonumber(nmap.registry.args["brute.threads"]) or 10 + o.iterators = {} o.error = nil o.tps = {} return o end, + addIterator = function( self, iterator ) + table.insert( self.iterators, iterator ) + end, + + setMode = function( self, mode ) + mode = ( mode == "user" or mode == "creds" or mode == "pass" ) and mode or nil + assert(mode, ("Unsupported mode: (%s)"):format(mode)) + end, + --- Limit the number of worker threads -- -- @param max number containing the maximum number of allowed threads @@ -392,6 +421,27 @@ return count end, + --- Iterator wrapper used to iterate over all registered iterators + -- + -- @return iterator function + get_next_credential = function( self ) + local function next_credential () + local used_creds = {} + -- 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 + if ( not(used_creds[user..pass]) ) then + 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 @@ -403,6 +453,8 @@ local retries = self.options.max_retries local msg + local next_credential = self:get_next_credential() + repeat driver = self.driver:new( self.host, self.port, self.driver_options ) status = driver:connect() @@ -411,7 +463,7 @@ if ( status ) then if ( not(username) and not(password) ) then - username, password = self.iterator() + username, password = next_credential() end -- make sure that all threads locked in connect stat terminate quickly @@ -541,45 +593,62 @@ -- @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 + -- 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 ).check ) then + -- check if the driver is ready! + local status, response = self.driver:new( self.host, self.port ):check() + if( not(status) ) then return false, response end end - status, usernames = unpwdb.usernames() - if ( not(status) ) then - return false, "Failed to load usernames" - 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 - status, passwords = unpwdb.passwords() - if ( not(status) ) then - return false, "Failed to load passwords" - end + local status, passwords = unpwdb.passwords() + if ( not(status) ) then return false, "Failed to load passwords" end + local mode = 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 + 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 + table.insert( self.iterators, Iterators.user_pw_iterator( single_user_iter(), passwords ) ) + elseif ( mode == 'creds' ) then + local credfile = stdnse.get_script_args("brute.credfile") + if ( not(credfile) ) then + return false, "No credential file specified" + end + + local f = io.open( credfile, "r" ) + if ( not(f) ) then + return false, ("Failed to open credfile (%s)"):format(credfile) + end + local creds = {} + for line in f:lines() do + local trim = function(s) return s:match('^()%s*$') and '' or s:match('^%s*(.*%S)') end + line = trim(line) + local user, pass = line:match("^([^%/]*)%/(.*)$") + table.insert(creds, { [user]=pass } ) + end + + table.insert( self.iterators, Iterators.credential_iterator( creds ) ) + 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(nmap.registry.args['brute.mode']) - else - self.iterator = Engine.pwdusr_iterator( self, usernames, passwords ) + -- 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() @@ -605,16 +674,11 @@ 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 + 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 table.insert(stats, ("Perfomed %d guesses in %d seconds, average tps: %d"):format( self.counter, time_diff, tps ) ) @@ -636,86 +700,140 @@ return true, result end, - --- Credential iterator, tries every user for each password +} + +Iterators = { + + --- Iterates over each user and 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 = {} + -- @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 = pass; inner = users + elseif ( mode == 'user' ) then + outer = users; inner = pass + else + return + end - -- 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] = {} + 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 - - 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) + 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 - usernames("reset") end while true do coroutine.yield(nil, nil) end end - return coroutine.wrap(next_password_username) + return coroutine.wrap( next_credential ) end, - - --- Credential iterator, tries every password for each user + + --- Try each password for each user (user in outer loop) -- - -- @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 = {} + -- @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, - for username in usernames do - -- set's up a table to track tested credentials - tested_creds[username] = {} - - -- 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") + --- 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_username_password) + 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 creds table containing username/pass combinations + -- the table should be of the following format + -- { ["user"] = "pass", ["user2"] = "pass2" } + -- @return function iterator + credential_iterator = function( creds ) + local function next_credential () + for _, item in ipairs(creds) do + for user, pass in pairs(item) do + coroutine.yield( user, pass ) + end + end + 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, + } -