File: http-form-brute.nse

package info (click to toggle)
nmap 6.47-3%2Bdeb8u2
  • links: PTS, VCS
  • area: main
  • in suites: jessie
  • size: 44,788 kB
  • ctags: 25,108
  • sloc: ansic: 89,741; cpp: 62,412; sh: 19,492; python: 17,323; xml: 11,413; perl: 2,529; makefile: 2,503; yacc: 608; lex: 469; asm: 372; java: 45
file content (250 lines) | stat: -rw-r--r-- 9,379 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
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.
]]

---
-- @usage
-- nmap --script http-form-brute -p 80 <host>
--
-- 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 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
-- | http-brute:
-- |   Accounts
-- |     Patrik Karlsson:secret => Login correct
-- |   Statistics
-- |_    Perfomed 60023 guesses in 467 seconds, average tps: 138
--
-- Summary
-- -------
--   x The Driver class contains the driver implementation used by the brute
--     library
--
-- @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 (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.3
-- 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
--

author = "Patrik Karlsson"
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 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 ( 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,

  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
--
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 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 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 )
  -- 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 "\n  ERROR: No passvar was specified (see http-form-brute.passvar)"
  end

  if ( not(path) ) then
    return "\n  ERROR: No path was specified (see http-form-brute.path)"
  end

  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)
  engine.options.script_name = SCRIPT_NAME

  if ( not(uservar) ) then
    engine.options:setOption( "passonly", true )
  end
  local status, result = engine:start()

  return result
end