CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/http/client_request.rb
Views: 11704
1
# -*- coding: binary -*-
2
require 'uri'
3
4
require 'rex/mime'
5
require 'rex/socket'
6
require 'rex/text'
7
8
require 'pp'
9
10
module Rex
11
module Proto
12
module Http
13
14
class ClientRequest
15
16
DefaultConfig = {
17
#
18
# Regular HTTP stuff
19
#
20
'agent' => nil,
21
'cgi' => true,
22
'cookie' => nil,
23
'data' => '',
24
'headers' => nil,
25
'raw_headers' => '',
26
'method' => 'GET',
27
'partial' => false,
28
'path_info' => '',
29
'port' => 80,
30
'proto' => 'HTTP',
31
'query' => '',
32
'ssl' => false,
33
'uri' => '/',
34
'vars_get' => {},
35
'vars_post' => {},
36
'vars_form_data' => [],
37
'version' => '1.1',
38
'vhost' => nil,
39
'ssl_server_name_indication' => nil,
40
41
#
42
# Evasion options
43
#
44
'encode_params' => true,
45
'encode' => false,
46
'uri_encode_mode' => 'hex-normal', # hex-normal, hex-all, hex-noslashes, hex-random, u-normal, u-all, u-noslashes, u-random
47
'uri_encode_count' => 1, # integer
48
'uri_full_url' => false, # bool
49
'pad_method_uri_count' => 1, # integer
50
'pad_uri_version_count' => 1, # integer
51
'pad_method_uri_type' => 'space', # space, tab, apache
52
'pad_uri_version_type' => 'space', # space, tab, apache
53
'method_random_valid' => false, # bool
54
'method_random_invalid' => false, # bool
55
'method_random_case' => false, # bool
56
'version_random_valid' => false, # bool
57
'version_random_invalid' => false, # bool
58
'uri_dir_self_reference' => false, # bool
59
'uri_dir_fake_relative' => false, # bool
60
'uri_use_backslashes' => false, # bool
61
'pad_fake_headers' => false, # bool
62
'pad_fake_headers_count' => 16, # integer
63
'pad_get_params' => false, # bool
64
'pad_get_params_count' => 8, # integer
65
'pad_post_params' => false, # bool
66
'pad_post_params_count' => 8, # integer
67
'uri_fake_end' => false, # bool
68
'uri_fake_params_start' => false, # bool
69
'shuffle_get_params' => false, # bool
70
'shuffle_post_params' => false, # bool
71
'header_folding' => false, # bool
72
'chunked_size' => 0, # integer
73
74
#
75
# NTLM Options
76
#
77
'usentlm2_session' => true,
78
'use_ntlmv2' => true,
79
'send_lm' => true,
80
'send_ntlm' => true,
81
'SendSPN' => true,
82
'UseLMKey' => false,
83
'domain' => 'WORKSTATION',
84
#
85
# Digest Options
86
#
87
'DigestAuthIIS' => true
88
}
89
90
attr_reader :opts
91
92
def initialize(opts={})
93
@opts = DefaultConfig.merge(opts)
94
@opts['agent'] ||= Rex::UserAgent.session_agent
95
@opts['headers'] ||= {}
96
end
97
98
def to_s(headers_only: false)
99
# Start GET query string
100
qstr = opts['query'] ? opts['query'].dup : ""
101
102
# Start POST data string
103
pstr = opts['data'] ? opts['data'].dup : ""
104
105
ctype = opts['ctype']
106
107
if opts['cgi']
108
uri_str = set_uri
109
110
if (opts['pad_get_params'])
111
1.upto(opts['pad_get_params_count'].to_i) do |i|
112
qstr << '&' if qstr.length > 0
113
qstr << set_encode_uri(Rex::Text.rand_text_alphanumeric(rand(32)+1))
114
qstr << '='
115
qstr << set_encode_uri(Rex::Text.rand_text_alphanumeric(rand(32)+1))
116
end
117
end
118
if opts.key?("vars_get") && opts['vars_get']
119
opts['vars_get'] = Hash[opts['vars_get'].to_a.shuffle] if (opts['shuffle_get_params'])
120
121
opts['vars_get'].each_pair do |var,val|
122
var = var.to_s
123
124
qstr << '&' if qstr.length > 0
125
qstr << (opts['encode_params'] ? set_encode_uri(var) : var)
126
# support get parameter without value
127
# Example: uri?parameter
128
if val
129
val = val.to_s
130
qstr << '='
131
qstr << (opts['encode_params'] ? set_encode_uri(val) : val)
132
end
133
end
134
end
135
if (opts['pad_post_params'])
136
1.upto(opts['pad_post_params_count'].to_i) do |i|
137
rand_var = Rex::Text.rand_text_alphanumeric(rand(32)+1)
138
rand_val = Rex::Text.rand_text_alphanumeric(rand(32)+1)
139
pstr << '&' if pstr.length > 0
140
pstr << (opts['encode_params'] ? set_encode_uri(rand_var) : rand_var)
141
pstr << '='
142
pstr << (opts['encode_params'] ? set_encode_uri(rand_val) : rand_val)
143
end
144
end
145
146
opts['vars_post'] = Hash[opts['vars_post'].to_a.shuffle] if (opts['shuffle_post_params'])
147
148
opts['vars_post'].each_pair do |var,val|
149
var = var.to_s
150
unless val.is_a?(Array)
151
val = [val]
152
end
153
val.each do |v|
154
v = v.to_s
155
pstr << '&' if pstr.length > 0
156
pstr << (opts['encode_params'] ? set_encode_uri(var) : var)
157
pstr << '='
158
pstr << (opts['encode_params'] ? set_encode_uri(v) : v)
159
end
160
end
161
162
if opts['vars_form_data'] && opts['vars_form_data'].length > 0
163
unless opts['vars_form_data'].is_a?(::Array)
164
raise ::ArgumentError, "request_cgi: The provided `form_data` option is not valid. Expected: Array, Got: #{opts['form_data'].class}"
165
end
166
167
form_data = Rex::MIME::Message.new
168
# Initialize or reuse the previous form data boundary to ensure idempotency
169
opts['vars_form_data_boundary'] ||= form_data.bound
170
form_data.bound = opts['vars_form_data_boundary']
171
172
opts['vars_form_data'].each do |field_hash|
173
# The name of the HTTP form field
174
field_name = field_hash.fetch('name', nil)
175
unless field_name.is_a?(::String) || field_name == nil
176
raise ::ArgumentError, "to_s: The provided field `name` option is not valid. Expected: String, Got: #{field_name.class}"
177
end
178
179
mime_type = field_hash.fetch('content_type', nil)
180
encoding = field_hash.fetch('encoding', nil)
181
182
file_contents = get_file_data(field_hash['data'])
183
filename = field_hash.fetch('filename') { get_filename(field_hash['data']) }
184
185
content_disposition = 'form-data'
186
content_disposition << "; name=\"#{field_name}\"" if field_name
187
# NOTE: The file name is intentionally unescaped, as exploits such as playsms_filename_exec embed payloads into the file name which shouldn't be escaped
188
content_disposition << "; filename=\"#{filename}\"" if filename
189
190
form_data.add_part(file_contents, mime_type, encoding, content_disposition)
191
end
192
193
pstr += form_data.to_s
194
end
195
196
ctype ||= "multipart/form-data; boundary=#{opts['vars_form_data_boundary']}" if opts['vars_form_data_boundary']
197
ctype ||= 'application/x-www-form-urlencoded' if opts['method'] == 'POST'
198
else
199
if opts['encode']
200
qstr = set_encode_uri(qstr)
201
end
202
uri_str = set_uri
203
end
204
205
req = ''
206
req << set_method
207
req << set_method_uri_spacer()
208
req << set_uri_prepend()
209
210
if opts['encode']
211
req << set_encode_uri(uri_str)
212
else
213
req << uri_str
214
end
215
216
217
if (qstr.length > 0)
218
req << '?'
219
req << qstr
220
end
221
222
req << set_path_info
223
req << set_uri_append()
224
req << set_uri_version_spacer()
225
req << set_version
226
227
# Set a default Host header if one wasn't passed in
228
unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?('host')
229
req << set_host_header
230
end
231
232
# If an explicit User-Agent header is set, then use that instead of
233
# the default
234
unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?('user-agent')
235
req << set_agent_header
236
end
237
238
# Similar to user-agent, only add an automatic auth header if a
239
# manual one hasn't been provided
240
unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?('authorization')
241
req << set_auth_header
242
end
243
244
req << set_cookie_header
245
req << set_connection_header
246
req << set_extra_headers
247
248
req << set_content_type_header(ctype)
249
req << set_content_len_header(pstr.length)
250
req << set_chunked_header
251
req << opts['raw_headers']
252
req << set_body(pstr) unless headers_only
253
254
req
255
end
256
257
protected
258
259
def set_uri
260
uri_str = opts['uri'].dup
261
if (opts['uri_dir_self_reference'])
262
uri_str.gsub!('/', '/./')
263
end
264
265
if (opts['uri_dir_fake_relative'])
266
buf = ""
267
uri_str.split('/',-1).each do |part|
268
cnt = rand(8)+2
269
1.upto(cnt) { |idx|
270
buf << "/" + Rex::Text.rand_text_alphanumeric(rand(32)+1)
271
}
272
buf << ("/.." * cnt)
273
buf << "/" + part
274
end
275
uri_str = buf
276
end
277
278
if (opts['uri_full_url'])
279
url = opts['ssl'] ? "https://" : "http://"
280
url << opts['vhost']
281
url << ((opts['port'] == 80) ? "" : ":#{opts['port']}")
282
url << uri_str
283
url
284
else
285
uri_str
286
end
287
end
288
289
def set_encode_uri(str)
290
a = str.to_s.dup
291
opts['uri_encode_count'].times {
292
a = Rex::Text.uri_encode(a, opts['uri_encode_mode'])
293
}
294
return a
295
end
296
297
def set_method
298
ret = opts['method'].dup
299
300
if (opts['method_random_valid'])
301
ret = ['GET', 'POST', 'HEAD'][rand(3)]
302
end
303
304
if (opts['method_random_invalid'])
305
ret = Rex::Text.rand_text_alpha(rand(20)+1)
306
end
307
308
if (opts['method_random_case'])
309
ret = Rex::Text.to_rand_case(ret)
310
end
311
312
ret
313
end
314
315
def set_method_uri_spacer
316
len = opts['pad_method_uri_count'].to_i
317
set = " "
318
buf = ""
319
320
case opts['pad_method_uri_type']
321
when 'tab'
322
set = "\t"
323
when 'apache'
324
set = "\t \x0b\x0c\x0d"
325
end
326
327
while(buf.length < len)
328
buf << set[ rand(set.length) ]
329
end
330
331
return buf
332
end
333
334
#
335
# Return the padding to place before the uri
336
#
337
def set_uri_prepend
338
prefix = ""
339
340
if (opts['uri_fake_params_start'])
341
prefix << '/%3fa=b/../'
342
end
343
344
if (opts['uri_fake_end'])
345
prefix << '/%20HTTP/1.0/../../'
346
end
347
348
prefix
349
end
350
351
#
352
# Return the HTTP path info
353
# TODO:
354
# * Encode path information
355
def set_path_info
356
opts['path_info'] ? opts['path_info'] : ''
357
end
358
359
#
360
# Return the padding to place before the uri
361
#
362
def set_uri_append
363
# TODO:
364
# * Support different padding types
365
""
366
end
367
368
#
369
# Return the spacing between the uri and the version
370
#
371
def set_uri_version_spacer
372
len = opts['pad_uri_version_count'].to_i
373
set = " "
374
buf = ""
375
376
case opts['pad_uri_version_type']
377
when 'tab'
378
set = "\t"
379
when 'apache'
380
set = "\t \x0b\x0c\x0d"
381
end
382
383
while(buf.length < len)
384
buf << set[ rand(set.length) ]
385
end
386
387
return buf
388
end
389
390
#
391
# Return the HTTP version string
392
#
393
def set_version
394
ret = opts['proto'] + "/" + opts['version']
395
396
if (opts['version_random_valid'])
397
ret = opts['proto'] + "/" + ['1.0', '1.1'][rand(2)]
398
end
399
400
if (opts['version_random_invalid'])
401
ret = Rex::Text.rand_text_alphanumeric(rand(20)+1)
402
end
403
404
ret << "\r\n"
405
end
406
407
#
408
# Return a formatted header string
409
#
410
def set_formatted_header(var, val)
411
if (self.opts['header_folding'])
412
"#{var}:\r\n\t#{val}\r\n"
413
else
414
"#{var}: #{val}\r\n"
415
end
416
end
417
418
#
419
# Return the HTTP agent header
420
#
421
def set_agent_header
422
opts['agent'] ? set_formatted_header("User-Agent", opts['agent']) : ""
423
end
424
425
def set_auth_header
426
opts['authorization'] ? set_formatted_header("Authorization", opts['authorization']) : ""
427
end
428
429
#
430
# Return the HTTP cookie header
431
#
432
def set_cookie_header
433
opts['cookie'] ? set_formatted_header("Cookie", opts['cookie']) : ""
434
end
435
436
#
437
# Return the HTTP connection header
438
#
439
def set_connection_header
440
opts['connection'] ? set_formatted_header("Connection", opts['connection']) : ""
441
end
442
443
#
444
# Return the content type header
445
#
446
def set_content_type_header(ctype)
447
ctype ? set_formatted_header("Content-Type", ctype) : ""
448
end
449
450
#
451
# Return the content length header
452
#
453
def set_content_len_header(clen)
454
if opts['method'] == 'GET' && (clen == 0 || opts['chunked_size'] > 0)
455
# This condition only applies to GET because of the specs.
456
# RFC-7230:
457
# A Content-Length header field is normally sent in a POST
458
# request even when the value is 0 (indicating an empty payload body)
459
return ''
460
elsif opts['headers'] && opts['headers']['Content-Length']
461
# If the module has a modified content-length header, respect that by
462
# not setting another one.
463
return ''
464
end
465
set_formatted_header("Content-Length", clen)
466
end
467
468
#
469
# Return the HTTP Host header
470
#
471
def set_host_header
472
return "" if opts['uri_full_url']
473
host = opts['vhost']
474
475
# IPv6 addresses must be placed in brackets
476
if Rex::Socket.is_ipv6?(host)
477
host = "[#{host}]"
478
end
479
480
# The port should be appended if non-standard
481
if not [80,443].include?(opts['port'])
482
host = host + ":#{opts['port']}"
483
end
484
485
set_formatted_header("Host", host)
486
end
487
488
#
489
# Return a string of formatted extra headers
490
#
491
def set_extra_headers
492
buf = ''
493
494
if (opts['pad_fake_headers'])
495
1.upto(opts['pad_fake_headers_count'].to_i) do |i|
496
buf << set_formatted_header(
497
Rex::Text.rand_text_alphanumeric(rand(32)+1),
498
Rex::Text.rand_text_alphanumeric(rand(32)+1)
499
)
500
end
501
end
502
503
opts['headers'].each_pair do |var,val|
504
buf << set_formatted_header(var, val)
505
end
506
507
buf
508
end
509
510
def set_chunked_header
511
return "" if opts['chunked_size'] == 0
512
set_formatted_header('Transfer-Encoding', 'chunked')
513
end
514
515
#
516
# Return the HTTP separator and body string
517
#
518
def set_body(bdata)
519
return "\r\n" + bdata if opts['chunked_size'] == 0
520
str = bdata.dup
521
chunked = ''
522
while str.size > 0
523
chunk = str.slice!(0,rand(opts['chunked_size']) + 1)
524
chunked << sprintf("%x", chunk.size) + "\r\n" + chunk + "\r\n"
525
end
526
"\r\n" + chunked + "0\r\n\r\n"
527
end
528
529
def get_file_data(file)
530
file.respond_to?('read') ? (file.rewind; contents = file.read; file.rewind; contents) : file.to_s
531
end
532
533
def get_filename(data)
534
data.is_a?(::Pathname) || data.is_a?(::File) ? ::File.basename(data) : nil
535
end
536
537
end
538
539
540
541
end
542
end
543
end
544
545