diff --git a/scripts/http-form-brute.nse b/scripts/http-form-brute.nse index 32c4929d9..78aae374a 100644 --- a/scripts/http-form-brute.nse +++ b/scripts/http-form-brute.nse @@ -10,6 +10,25 @@ Performs brute force password auditing against http form-based authentication. -- guessing. Any successful guesses are stored in the nmap registry, under -- the nmap.registry.credentials.http key for other scripts to use. -- +-- The script automatically attempts to discover the form field names to +-- use in order to perform password guessing. If it fails doing so the form +-- parameters can be supplied using the uservar and passvar arguments. +-- +-- After attempting to authenticate using a HTTP POST request the script +-- analyzes the response and attempt to determine whether authentication was +-- successful or not. The script analyzes this by checking the response using +-- the following rules: +-- 1. If the response was empty the authentication was successful +-- 2. If the response contains the message passed in the onsuccess +-- argument the authentication was successful +-- 3. If no onsuccess argument was passed, and if the response +-- does not contain the message passed in the onfailure argument the +-- authentication was successful +-- 4. If neither the onsuccess or onfailure argument was passed and the +-- response does not contain a password form field authentication +-- was successful +-- 5. Authentication failed +-- -- @output -- PORT STATE SERVICE REASON -- 80/tcp open http syn-ack @@ -27,17 +46,23 @@ Performs brute force password auditing against http form-based authentication. -- @args http-form-brute.path points to the path protected by authentication -- @args http-form-brute.hostname sets the host header in case of virtual -- hosting --- @args http-form-brute.uservar sets the http-variable name that holds the --- username used to authenticate. A simple autodetection of this variable --- is attempted. +-- @args http-form-brute.uservar (optional) sets the http-variable name that +-- holds the username used to authenticate. A simple autodetection of +-- this variable is attempted. -- @args http-form-brute.passvar sets the http-variable name that holds the -- password used to authenticate. A simple autodetection of this variable -- is attempted. - +-- @args http-form-brute.onsuccess (optional) sets the message to expect on +-- successful authentication +-- @args http-form-brute.onfailure (optional) sets the message to expect on +-- unsuccessful authentication -- --- Version 0.1 +-- Version 0.3 -- Created 07/30/2010 - v0.1 - created by Patrik Karlsson +-- Revised 05/23/2011 - v0.2 - changed so that uservar is optional +-- Revised 06/05/2011 - v0.3 - major re-write, added onsucces, onfailure and +-- support for redirects -- author = "Patrik Karlsson" @@ -47,6 +72,7 @@ categories = {"intrusive", "auth"} require 'shortport' require 'http' require 'brute' +require 'url' portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open") @@ -60,7 +86,6 @@ Driver = { self.__index = self o.host = nmap.registry.args['http-form-brute.hostname'] or host o.port = port - o.path = nmap.registry.args['http-form-brute.path'] o.options = options return o end, @@ -75,18 +100,40 @@ Driver = { login = function( self, username, password ) -- we need to supply the no_cache directive, or else the http library -- incorrectly tells us that the authentication was successfull - local response = http.post( self.host, self.port, self.path, { no_cache = true }, nil, { [self.options.uservar] = username, [self.options.passvar] = password } ) + local postparams = { [self.options.passvar] = password } + if ( self.options.uservar ) then postparams[self.options.uservar] = username end + + local response = Driver.postRequest(self.host, self.port, self.options.path, postparams) + local success = false + + -- if we have no response, we were successful + if ( not(response.body) ) then + success = true + -- if we have a response and it matches our onsuccess match, login was successful + elseif ( response.body and + self.options.onsuccess and + response.body:match(self.options.onsuccess) ) then + success = true + -- if we have a response and it does not match our onfailure, login was successful + elseif ( response.body and + not(self.options.onsuccess) and + self.options.onfailure and + not(response.body:match(self.options.onfailure))) then + success = true + -- if we have a response and no onfailure or onsuccess match defined + -- and can't find a password field, login was successful + elseif ( response.body and + not(self.options.onfailure) and + not(self.options.onsuccess) and + not(response.body:match("input.-type=[\"]*[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd][\"]*")) + ) then + success = true + end -- We check whether the body was empty or that we have a body without our user- and pass-var - if ( not( response.body ) or - ( response.body and not( response.body:match("name=\"*" .. self.options.uservar ) and response.body:match("name=\"*" .. self.options.passvar ) ) ) ) then - - if ( not( nmap.registry['credentials'] ) ) then - nmap.registry['credentials'] = {} - end - if ( not( nmap.registry.credentials['http'] ) ) then - nmap.registry.credentials['http'] = {} - end + if ( success ) then + nmap.registry['credentials'] = nmap.registry['credentials'] or {} + nmap.registry.credentials['http'] = nmap.registry.credentials['http'] or {} table.insert( nmap.registry.credentials.http, { username = username, password = password } ) return true, brute.Account:new( username, password, "OPEN") end @@ -99,15 +146,19 @@ Driver = { end, check = function( self ) - local response = http.get( self.host, self.port, self.path ) - - -- do a *very* simple check - if ( response.status == 200 and response.body:match("type=\"password\"")) then - return true - end - return false + return true end, + postRequest = function( host, port, path, options ) + local response = http.post( host, port, path, { no_cache = true }, nil, options ) + local status = ( response and tonumber(response.status) ) or 0 + if ( status > 300 and status < 400 ) then + local new_path = url.absolute(path, response.header.location) + response = http.get( host, port, new_path, { no_cache = true } ) + end + return response + end, + } --- Attempts to auto-detect known form-fields @@ -117,11 +168,11 @@ local function detectFormFields( host, port, path ) local user_field, pass_field if ( response.status == 200 ) then - user_field = response.body:match("") - pass_field = response.body:match("") + user_field = response.body:match("<[Ii][Nn][Pp][Uu][Tt].-name=[\"]*([^\"]-[Uu][Ss][Ee][Rr].-)[\"]*.->") + pass_field = response.body:match("<[Ii][Nn][Pp][Uu][Tt].-name=[\"]*([Pp][Aa][Ss][Ss].-)[\"]*.->") if ( not(pass_field) ) then - pass_field = response.body:match("") + pass_field = response.body:match("<[Ii][Nn][Pp][Uu][Tt].-name=[\"]-([^\"]-[Kk][Ee][Yy].-)[\"].->") end end @@ -129,34 +180,65 @@ local function detectFormFields( host, port, path ) end action = function( host, port ) - local uservar = nmap.registry.args['http-form-brute.uservar'] - local passvar = nmap.registry.args['http-form-brute.passvar'] - local path = nmap.registry.args['http-form-brute.path'] or "/" - local status, result, engine, _ + local uservar = stdnse.get_script_args('http-form-brute.uservar') + local passvar = stdnse.get_script_args('http-form-brute.passvar') + local path = stdnse.get_script_args('http-form-brute.path') or "/" + local force = stdnse.get_script_args("http-form-brute.force") + local onsuccess = stdnse.get_script_args("http-form-brute.onsuccess") + local onfailure = stdnse.get_script_args("http-form-brute.onfailure") + + local _ + -- if now fields were given attempt to autodetect if ( not(uservar) and not(passvar) ) then uservar, passvar = detectFormFields( host, port, path ) - elseif ( not(uservar) ) then - uservar, _ = detectFormFields( host, port, path ) + -- if now passvar was detected attempt to autodetect elseif ( not(passvar) ) then _, passvar = detectFormFields( host, port, path ) end - if ( not( uservar ) ) then - return " \n ERROR: No uservar was specified (see http-form-brute.uservar)" - end + + -- uservar is optional, so only make sure we have a passvar if ( not( passvar ) ) then - return " \n ERROR: No passvar was specified (see http-form-brute.passvar)" + return "\n ERROR: No passvar was specified (see http-form-brute.passvar)" end - if ( not(nmap.registry.args['http-form-brute.path']) ) then - return " \n ERROR: No path was specified (see http-form-brute.path)" + if ( not(path) ) then + return "\n ERROR: No path was specified (see http-form-brute.path)" end - - engine = brute.Engine:new( Driver, host, port, { uservar = uservar, passvar = passvar } ) + + if ( onsuccess and onfailure ) then + return "\n ERROR: Either the onsuccess or onfailure argument should be passed, not both." + end + + local options = { [passvar] = "this_is_not_a_valid_password" } + if ( uservar ) then options[uservar] = "this_is_not_a_valid_user" end + + local response = Driver.postRequest( host, port, path, options ) + if ( not(response) or not(response.body) or response.status ~= 200 ) then + return ("\n ERROR: Failed to retrieve path (%s) from server"):format(path) + end + + -- try to detect onfailure match + if ( onfailure and not(response.body:match(onfailure)) ) then + return ("\n ERROR: Failed to match password failure message (%s)"):format(onfailure) + elseif ( not(onfailure) and + not(onsuccess) and + not(response.body:match("input.-type=[\"]*[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd][\"]*")) ) then + return ("\n ERROR: Failed to detect password form field see (http-form-brute.onsuccess or http-form-brute.onfailure)") + end + + local engine = brute.Engine:new( Driver, host, port, { + uservar = uservar, passvar = passvar, + path = path, onsuccess = onsuccess, onfailure = onfailure + } + ) -- there's a bug in http.lua that does not allow it to be called by -- multiple threads engine:setMaxThreads(1) - status, result = engine:start() + if ( not(uservar) ) then + engine.options:setOption( "passonly", true ) + end + local status, result = engine:start() return result end \ No newline at end of file