mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
There's no reason we can't use other verbs besides GET and POST. Other verbs are handled like GET requests (parameters in the URI string). Any redirect responses will be followed with GET requests, though.
261 lines
9.9 KiB
Lua
261 lines
9.9 KiB
Lua
local brute = require "brute"
|
|
local creds = require "creds"
|
|
local http = require "http"
|
|
local nmap = require "nmap"
|
|
local shortport = require "shortport"
|
|
local stdnse = require "stdnse"
|
|
local table = require "table"
|
|
local url = require "url"
|
|
|
|
description = [[
|
|
Performs brute force password auditing against http form-based authentication.
|
|
|
|
This script uses the unpwdb and brute libraries to perform password
|
|
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 GET or 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
|
|
]]
|
|
|
|
---
|
|
-- @usage
|
|
-- nmap --script http-form-brute -p 80 <host>
|
|
--
|
|
-- @output
|
|
-- PORT STATE SERVICE REASON
|
|
-- 80/tcp open http syn-ack
|
|
-- | http-brute:
|
|
-- | Accounts
|
|
-- | Patrik Karlsson:secret => Login correct
|
|
-- | Statistics
|
|
-- |_ Perfomed 60023 guesses in 467 seconds, average tps: 138
|
|
--
|
|
-- @args http-form-brute.path points to the path protected by authentication
|
|
-- (default: "/")
|
|
-- @args http-form-brute.method sets the HTTP method (default: "POST")
|
|
-- @args http-form-brute.hostname sets the host header in case of virtual
|
|
-- hosting
|
|
-- @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.4
|
|
-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
|
-- Revised 05/23/2011 - v0.2 - changed so that uservar is optional
|
|
-- Revised 06/05/2011 - v0.3 - major re-write, added onsuccess, onfailure and
|
|
-- support for redirects
|
|
-- Revised 08/12/2014 - v0.4 - added support for GET method
|
|
--
|
|
|
|
author = "Patrik Karlsson, nnposter"
|
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
|
categories = {"intrusive", "brute"}
|
|
|
|
|
|
portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
|
|
|
|
local form_params = {}
|
|
|
|
Driver = {
|
|
|
|
new = function(self, host, port, options)
|
|
local o = {}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
o.host = nmap.registry.args['http-form-brute.hostname'] or host
|
|
o.port = port
|
|
o.options = options
|
|
return o
|
|
end,
|
|
|
|
connect = function( self )
|
|
-- This will cause problems, as there is no way for us to "reserve"
|
|
-- a socket. We may end up here early with a set of credentials
|
|
-- which won't be guessed until the end, due to socket exhaustion.
|
|
return true
|
|
end,
|
|
|
|
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 successful
|
|
local params = { [self.options.passvar] = password }
|
|
if ( self.options.uservar ) then params[self.options.uservar] = username end
|
|
|
|
local response = Driver.sendLogin(self.host, self.port, self.options.path, self.options.method, params)
|
|
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 ( 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, creds.State.VALID)
|
|
end
|
|
|
|
return false, brute.Error:new( "Incorrect password" )
|
|
end,
|
|
|
|
disconnect = function( self )
|
|
return true
|
|
end,
|
|
|
|
check = function( self )
|
|
return true
|
|
end,
|
|
|
|
sendLogin = function( host, port, path, method, params )
|
|
local response
|
|
local uri = path
|
|
if method == "POST" then
|
|
response = http.post(host, port, path, {no_cache = true}, nil, params)
|
|
else
|
|
uri = path .. (path:find("?", 1, true) and "&" or "?")
|
|
.. url.build_query(params)
|
|
response = http.generic_request(host, port, method, uri, {no_cache = true})
|
|
end
|
|
local u = http.parse_redirect(host, port, uri, response)
|
|
if u then
|
|
response = http.get( u.host, u.port, u.path, {no_cache = true} )
|
|
end
|
|
return response
|
|
end,
|
|
|
|
}
|
|
|
|
--- Attempts to auto-detect known form-fields
|
|
--
|
|
local function detectFormFields( host, port, path )
|
|
local response = http.get( host, port, path )
|
|
local user_field, pass_field
|
|
|
|
if ( response.status == 200 ) then
|
|
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("<[Ii][Nn][Pp][Uu][Tt].-name=[\"]-([^\"]-[Kk][Ee][Yy].-)[\"].->")
|
|
end
|
|
end
|
|
|
|
return user_field, pass_field
|
|
end
|
|
|
|
action = function( host, port )
|
|
local path = stdnse.get_script_args('http-form-brute.path') or "/"
|
|
local method = stdnse.get_script_args('http-form-brute.method') or "POST"
|
|
local uservar = stdnse.get_script_args('http-form-brute.uservar')
|
|
local passvar = stdnse.get_script_args('http-form-brute.passvar')
|
|
local onsuccess = stdnse.get_script_args("http-form-brute.onsuccess")
|
|
local onfailure = stdnse.get_script_args("http-form-brute.onfailure")
|
|
|
|
local _
|
|
|
|
method=method:upper()
|
|
if not (method=="GET" or method=="POST") then
|
|
stdnse.debug1("Using non-standard HTTP method: %s", method)
|
|
end
|
|
|
|
-- if now fields were given attempt to autodetect
|
|
if ( not(uservar) and not(passvar) ) then
|
|
uservar, passvar = detectFormFields( host, port, path )
|
|
-- if now passvar was detected attempt to autodetect
|
|
elseif ( not(passvar) ) then
|
|
_, passvar = detectFormFields( host, port, path )
|
|
end
|
|
|
|
-- uservar is optional, so only make sure we have a passvar
|
|
if ( not( passvar ) ) then
|
|
return stdnse.format_output(false, "No passvar was specified (see http-form-brute.passvar)")
|
|
end
|
|
|
|
if ( onsuccess and onfailure ) then
|
|
return stdnse.format_output(false, "Either the onsuccess or onfailure argument should be passed, not both.")
|
|
end
|
|
|
|
local params = { [passvar] = "this_is_not_a_valid_password" }
|
|
if ( uservar ) then params[uservar] = "this_is_not_a_valid_user" end
|
|
|
|
local response = Driver.sendLogin( host, port, path, method, params )
|
|
if ( not(response) or not(response.body) or response.status ~= 200 ) then
|
|
return stdnse.format_output(false, ("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 stdnse.format_output(false, ("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 stdnse.format_output(false, ("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, {
|
|
path = path,
|
|
method = method,
|
|
uservar = uservar,
|
|
passvar = passvar,
|
|
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)
|
|
engine.options.script_name = SCRIPT_NAME
|
|
|
|
if ( not(uservar) ) then
|
|
engine.options:setOption( "passonly", true )
|
|
end
|
|
local status, result = engine:start()
|
|
|
|
return result
|
|
end
|