Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/fuzzers/http/http_form_field.rb
19535 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
include Msf::Exploit::Remote::HttpClient
8
9
def initialize(info = {})
10
super(
11
update_info(
12
info,
13
'Name' => 'HTTP Form Field Fuzzer',
14
'Description' => %q{
15
This module will grab all fields from a form,
16
and launch a series of POST actions, fuzzing the contents
17
of the form fields. You can optionally fuzz headers too
18
(option is enabled by default)
19
},
20
'Author' => [
21
'corelanc0d3r',
22
'Paulino Calderon <calderon[at]websec.mx>' # Added cookie handling
23
],
24
'License' => MSF_LICENSE,
25
'References' => [
26
['URL', 'http://www.corelan.be:8800/index.php/2010/11/12/metasploit-module-http-form-field-fuzzer'],
27
],
28
'Notes' => {
29
'Stability' => [CRASH_SERVICE_DOWN],
30
'SideEffects' => [],
31
'Reliability' => []
32
}
33
)
34
)
35
36
register_options(
37
[
38
OptString.new('URL', [ false, 'The URL that contains the form', '/']),
39
OptString.new('FORM', [ false, 'The name of the form to use. Leave empty to fuzz all forms', '' ]),
40
OptString.new('FIELDS', [ false, 'Name of the fields to fuzz. Leave empty to fuzz all fields', '' ]),
41
OptString.new('ACTION', [ false, 'Form action full URI. Leave empty to autodetect', '' ]),
42
OptInt.new('STARTSIZE', [ true, 'Fuzzing string startsize.', 1000]),
43
OptInt.new('ENDSIZE', [ true, 'Max Fuzzing string size.', 40000]),
44
OptInt.new('STEPSIZE', [ true, 'Increment fuzzing string each attempt.', 1000]),
45
OptInt.new('TIMEOUT', [ true, 'Number of seconds to wait for response on GET or POST', 15]),
46
OptInt.new('DELAY', [ true, 'Number of seconds to wait between 2 actions', 0]),
47
OptInt.new('STOPAFTER', [ false, 'Stop after x number of consecutive errors', 2]),
48
OptBool.new('CYCLIC', [ true, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),
49
OptBool.new('FUZZHEADERS', [ true, 'Fuzz headers', true]),
50
OptString.new('HEADERFIELDS', [ false, 'Name of the headerfields to fuzz. Leave empty to fuzz all fields', '' ]),
51
OptString.new('TYPES', [ true, 'Field types to fuzz', 'text,password,inputtextbox']),
52
OptString.new('CODE', [ true, 'Response code(s) indicating OK', '200,301,302,303' ]),
53
OptBool.new('HANDLECOOKIES', [ true, 'Appends cookies with every request.', false])
54
]
55
)
56
end
57
58
def init_vars
59
proto = 'http://'
60
if datastore['SSL']
61
proto = 'https://'
62
end
63
64
@send_data = {
65
uri: '',
66
version: '1.1',
67
method: 'POST',
68
headers: {
69
'Content-Length' => 100,
70
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
71
'Accept-Language' => 'en-us,en;q=0.5',
72
'Accept-Encoding' => 'gzip,deflate',
73
'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
74
'Keep-Alive' => '300',
75
'Connection' => 'keep-alive',
76
'Referer' => proto + datastore['RHOST'] + ':' + datastore['RPORT'].to_s,
77
'Content-Type' => 'application/x-www-form-urlencoded'
78
}
79
}
80
@get_data_headers = {
81
'Referer' => proto + datastore['RHOST'] + ':' + datastore['RPORT'].to_s
82
}
83
end
84
85
def init_fuzzdata
86
@fuzzsize = datastore['STARTSIZE']
87
@endsize = datastore['ENDSIZE']
88
set_fuzz_payload
89
@nrerrors = 0
90
end
91
92
def incr_fuzzsize
93
@stepsize = datastore['STEPSIZE'].to_i
94
@fuzzsize += @stepsize
95
end
96
97
def set_fuzz_payload
98
if datastore['CYCLIC']
99
@fuzzdata = Rex::Text.pattern_create(@fuzzsize)
100
else
101
@fuzzdata = 'A' * @fuzzsize
102
end
103
end
104
105
def is_error_code(code)
106
okcode = false
107
checkcodes = datastore['CODE'].split(',')
108
checkcodes.each do |testcode|
109
testcode = testcode.upcase.gsub(' ', '')
110
if testcode == code.to_s.upcase.gsub(' ', '')
111
okcode = true
112
end
113
end
114
return okcode
115
end
116
117
def fuzz_this_field(fieldname, fieldtype)
118
fuzzcommands = datastore['FIELDS'].split(',')
119
fuzzme = 0
120
if !fuzzcommands.empty?
121
fuzzcommands.each do |thiscmd|
122
thiscmd = thiscmd.strip
123
if ((fieldname.upcase == thiscmd.upcase) || (thiscmd == '')) && (fuzzme == 0)
124
fuzzme = 1
125
end
126
end
127
else
128
fuzztypes = datastore['TYPES'].split(',')
129
fuzztypes.each do |thistype|
130
if (fieldtype.upcase.strip == thistype.upcase.strip)
131
fuzzme = 1
132
end
133
end
134
end
135
if fuzzme == 1
136
set_fuzz_payload
137
end
138
return fuzzme
139
end
140
141
def fuzz_this_headerfield(fieldname)
142
fuzzheaderfields = datastore['HEADERFIELDS'].split(',')
143
fuzzme = 0
144
if !fuzzheaderfields.empty?
145
fuzzheaderfields.each do |thisfield|
146
thisfield = thisfield.strip
147
if ((fieldname.upcase == thisfield.upcase) || (thisfield == '')) && (fuzzme == 0)
148
fuzzme = 1
149
end
150
end
151
else
152
fuzzme = 1
153
end
154
if fuzzme == 1
155
set_fuzz_payload
156
end
157
return fuzzme
158
end
159
160
def do_fuzz_headers(form, headers)
161
headercnt = 0
162
datastr = ''
163
form[:fields].each do |thisfield|
164
normaldata = 'blah&'
165
if thisfield[:value] && thisfield[:value] != ('')
166
normaldata = thisfield[:value].strip + '&'
167
end
168
datastr << thisfield[:name].downcase.strip + '=' + normaldata
169
end
170
if !datastr.empty?
171
datastr = datastr[0, datastr.length - 1] + "\r\n"
172
else
173
datastr = "\r\n"
174
end
175
# first, check the original header fields and add some others - just for fun
176
myheaders = @send_data[:headers]
177
mysendheaders = @send_data[:headers].dup
178
# get or post ?
179
mysendheaders[:method] = form[:method].upcase
180
myheaders.each do |thisheader|
181
if !(headers[thisheader[0]])
182
# add header if needed
183
mysendheaders[thisheader[0]] = thisheader[1]
184
end
185
end
186
nrheaderstofuzz = mysendheaders.size
187
mysendheaders.each do |thisheader|
188
@fuzzheader = mysendheaders.dup
189
@nrerrors = 0
190
fuzzpacket = @send_data.dup
191
fuzzpacket[:method] = mysendheaders[:method]
192
headername = thisheader[0]
193
if fuzz_this_headerfield(headername.to_s.upcase) == 1
194
print_status(" - Fuzzing header '#{headername}' (#{headercnt + 1}/#{nrheaderstofuzz})")
195
init_fuzzdata
196
while @fuzzsize <= @endsize + 1
197
@fuzzheader[headername] = @fuzzdata
198
fuzzpacket[:headers] = @fuzzheader
199
response = send_fuzz(fuzzpacket, datastr)
200
if !process_response(response, headername, 'header')
201
@fuzzsize = @endsize + 2
202
end
203
if datastore['DELAY'] > 0
204
print_status(" (Sleeping for #{datastore['DELAY']} seconds...)")
205
select(nil, nil, nil, datastore['DELAY'])
206
end
207
incr_fuzzsize
208
end
209
else
210
print_status(" - Skipping header '#{headername}' (#{headercnt + 1}/#{nrheaderstofuzz})")
211
end
212
headercnt += 1
213
end
214
end
215
216
def do_fuzz_field(form, field)
217
fieldstofuzz = field.downcase.strip.split(',')
218
@nrerrors = 0
219
while @fuzzsize <= @endsize + 1
220
allfields = form[:fields]
221
datastr = ''
222
normaldata = ''
223
allfields.each do |thisfield|
224
dofuzzthis = false
225
next unless thisfield[:name]
226
227
fieldstofuzz.each do |fuzzthis|
228
next unless fuzzthis
229
230
if (thisfield[:name].downcase.strip == fuzzthis.downcase.strip)
231
dofuzzthis = true
232
end
233
end
234
if thisfield[:value]
235
normaldata = thisfield[:value].strip
236
else
237
normaldata = ''
238
end
239
if dofuzzthis
240
datastr << thisfield[:name].downcase.strip + '=' + @fuzzdata + '&'
241
else
242
datastr << thisfield[:name].downcase.strip + '=' + normaldata + '&'
243
end
244
end
245
datastr = datastr[0, datastr.length - 1]
246
@send_data[:uri] = form[:action]
247
@send_data[:uri] = "/#{form[:action]}" if @send_data[:uri][0, 1] != '/'
248
249
@send_data[:method] = form[:method].upcase
250
response = send_fuzz(@send_data, datastr)
251
if !process_response(response, field, 'field')
252
return
253
end
254
255
if datastore['DELAY'] > 0
256
print_status(" (Sleeping for #{datastore['DELAY']} seconds...)")
257
select(nil, nil, nil, datastore['DELAY'])
258
end
259
end
260
end
261
262
def process_response(response, field, type)
263
if response.nil?
264
print_error(" No response - #{@nrerrors + 1} / #{datastore['STOPAFTER']} - fuzzdata length : #{@fuzzsize}")
265
if @nrerrors + 1 >= datastore['STOPAFTER']
266
print_status(" *!* No response : #{type} #{field} | fuzzdata length : #{@fuzzsize}")
267
return false
268
else
269
@nrerrors += 1
270
end
271
else
272
okcode = is_error_code(response.code)
273
if okcode
274
@nrerrors = 0
275
incr_fuzzsize
276
end
277
if !okcode && (@nrerrors + 1 >= datastore['STOPAFTER'])
278
print_status(" *!* Error response code #{response.code} | #{type} #{field} | fuzzdata length #{@fuzzsize}")
279
return false
280
else
281
@nrerrors += 1
282
end
283
end
284
return true
285
end
286
287
def send_fuzz(postdata, data)
288
header = postdata[:headers]
289
response = send_request_raw({
290
'uri' => postdata[:uri],
291
'version' => postdata[:version],
292
'method' => postdata[:method],
293
'headers' => header,
294
'data' => data
295
}, datastore['TIMEOUT'])
296
return response
297
end
298
299
def get_field_val(input)
300
tmp = input.split(/=/)
301
# get delimiter
302
tmp2 = tmp[1].strip
303
delim = tmp2[0, 1]
304
if delim != "'" && delim != '"'
305
delim = ''
306
end
307
tmp3 = tmp[1].split(/>/)
308
tmp4 = tmp3[0].gsub(delim, '')
309
return tmp4
310
end
311
312
def get_form_data(body)
313
print_status('Enumerating form data')
314
body = body.gsub("\r", '')
315
body = body.gsub("\n", '')
316
bodydata = body.downcase.split(/<form/)
317
# we need part after <form
318
totalforms = bodydata.size - 1
319
print_status(" Number of forms : #{totalforms}")
320
formcnt = 0
321
formidx = 1
322
forms = []
323
while formcnt < totalforms
324
fdata = bodydata[formidx]
325
print_status(" - Enumerating form ##{formcnt + 1}")
326
data = fdata.downcase.split(%r{</form>})
327
# first, get action and name
328
formdata = data[0].downcase.split(/>/)
329
subdata = formdata[0].downcase.split(/ /)
330
namefound = false
331
actionfound = false
332
idfound = false
333
actionname = ''
334
formname = ''
335
formid = ''
336
formmethod = 'post'
337
subdata.each do |thisfield|
338
if thisfield.match(/^name=/) && !namefound
339
formname = get_field_val(thisfield)
340
namefound = true
341
end
342
if thisfield.match(/^id=/) && !idfound
343
formid = get_field_val(thisfield)
344
idfound = true
345
end
346
if thisfield.match(/^method=/)
347
formmethod = get_field_val(thisfield)
348
end
349
next unless thisfield.match(/^action=/) && !actionfound
350
351
actionname = get_field_val(thisfield)
352
if (actionname.length < datastore['URL'].length) && (datastore['URL'].downcase.index(actionname.downcase).to_i > -1)
353
actionname = datastore['URL']
354
end
355
actionfound = true
356
end
357
if !datastore['ACTION'].empty?
358
actionname = datastore['ACTION']
359
actionfound = true
360
end
361
362
if (formname == '') && (formid != '')
363
formname = formid
364
end
365
if (formid == '') && (formname != '')
366
formid = formname
367
end
368
if (formid == '') && (formname == '')
369
formid = 'noname_' + (formcnt + 1).to_s
370
formname = formid
371
end
372
idfound = true
373
namefound = true
374
375
formfields = []
376
# input boxes
377
fieldtypemarks = [ '<input', '<select' ]
378
fieldtypemarks.each do |currfieldmark|
379
formfieldcnt = 0
380
next unless (namefound || idfound) && actionfound
381
382
# get fields in current form - data[0]
383
subdata = data[0].downcase.split(currfieldmark)
384
skipflag = 0
385
next unless subdata.size > 1
386
387
subdata.each do |thisinput|
388
if skipflag == 1
389
# first, find the delimiter
390
fielddata = thisinput.downcase.split(/>/)
391
fields = fielddata[0].split(/ /)
392
fieldname = ''
393
fieldtype = ''
394
fieldvalue = ''
395
fieldid = ''
396
fields.each do |thisfield|
397
if thisfield.match(/^type=/)
398
fieldtype = get_field_val(thisfield)
399
end
400
if (currfieldmark == '<select') && thisfield.match(/^class=/)
401
fieldtype = get_field_val(thisfield)
402
end
403
if thisfield.match(/^name=/)
404
fieldname = get_field_val(thisfield)
405
end
406
if thisfield.match(/^id=/)
407
fieldid = get_field_val(thisfield)
408
end
409
next unless thisfield.match(/^value=/)
410
411
# special case
412
location = fielddata[0].index(thisfield)
413
delta = fielddata[0].size - location
414
remaining = fielddata[0][location, delta]
415
tmp = remaining.strip.split(/=/)
416
next unless tmp.size > 1
417
418
delim = tmp[1][0, 1]
419
tmp2 = tmp[1].split(delim)
420
fieldvalue = tmp2[1]
421
end
422
if (fieldname == '') && (fieldid != '')
423
fieldname = fieldid
424
end
425
if (fieldid == '') && (fieldname != '')
426
fieldid = fieldname
427
end
428
print_status(" Field : #{fieldname}, type #{fieldtype}")
429
if fieldid != ''
430
formfields << {
431
id: fieldid,
432
name: fieldname,
433
type: fieldtype,
434
value: fieldvalue
435
}
436
formfieldcnt += 1
437
end
438
else
439
skipflag += 1
440
end
441
end
442
end
443
print_status(" Nr of fields in form '#{formname}' : #{formfields.size}")
444
# store in multidimensional array
445
forms << {
446
name: formname,
447
id: formid,
448
action: actionname,
449
method: formmethod,
450
fields: formfields
451
}
452
formidx += 1
453
formcnt += 1
454
end
455
456
if !forms.empty?
457
print_status(' Forms : ')
458
end
459
460
forms.each do |thisform|
461
print_status(" - Name : #{thisform[:name]}, ID : #{thisform[:id]}, Action : #{thisform[:action]}, Method : #{thisform[:method]}")
462
end
463
464
return forms
465
end
466
467
def set_cookie(cookie)
468
@get_data_headers['Cookie'] = cookie
469
@send_data[:headers]['Cookie'] = cookie
470
end
471
472
def run
473
init_fuzzdata
474
init_vars
475
476
print_status("Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']}")
477
response = send_request_raw(
478
{
479
'uri' => normalize_uri(datastore['URL']),
480
'version' => '1.1',
481
'method' => 'GET',
482
'headers' => @get_data_headers
483
484
}, datastore['TIMEOUT']
485
)
486
if response.nil?
487
print_error('No response')
488
return
489
end
490
491
if datastore['HANDLECOOKIES']
492
cookie = response.get_cookies
493
set_cookie(cookie)
494
print_status("Set cookie: #{cookie}")
495
print_status("Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']} using cookies")
496
497
response = send_request_raw(
498
{
499
'uri' => normalize_uri(datastore['URL']),
500
'version' => '1.1',
501
'method' => 'GET',
502
'headers' => @get_data_headers
503
}, datastore['TIMEOUT']
504
)
505
end
506
if response.nil?
507
print_error('No response')
508
return
509
end
510
print_status("Code : #{response.code}")
511
okcode = is_error_code(response.code)
512
if !okcode
513
print_error('Server replied with error code. Check URL or set CODE to another value, and try again.')
514
return
515
end
516
if response.body
517
formfound = response.body.downcase.index('<form')
518
if formfound
519
formdata = get_form_data(response.body)
520
# fuzz !
521
# for each form that needs to be fuzzed
522
formdata.each do |thisform|
523
next if thisform[:name].empty?
524
525
if ((datastore['FORM'].strip == '') || (datastore['FORM'].upcase.strip == thisform[:name].upcase.strip)) && !thisform[:fields].empty?
526
print_status("Fuzzing fields in form #{thisform[:name].upcase.strip}")
527
# for each field in this form, fuzz one field at a time
528
formfields = thisform[:fields]
529
formfields.each do |thisfield|
530
next unless thisfield[:name]
531
532
next unless fuzz_this_field(thisfield[:name], thisfield[:type]) == 1
533
534
print_status(" - Fuzzing field #{thisfield[:name]}")
535
do_fuzz_field(thisform, thisfield[:name])
536
init_fuzzdata
537
end
538
print_status("Done fuzzing fields in form #{thisform[:name].upcase.strip}")
539
end
540
# fuzz headers ?
541
if datastore['FUZZHEADERS']
542
print_status('Fuzzing header fields')
543
do_fuzz_headers(thisform, response.headers)
544
end
545
end
546
547
else
548
print_error('No form found in response body')
549
print_status(response.body)
550
return
551
end
552
else
553
print_error('No response data')
554
end
555
end
556
end
557
558