#!/usr/bin/env python
"""
Python hydra wrapper module.  Includes classes hydra, hydraResult, and hydraException.  
See Docstrings for examples.

Supported protocols (as defined by hydra)
 telnet ftp pop3[-ntlm] imap[-ntlm] smb smbnt http[s]-{head|get}
 http-{get|post}-form http-proxy cisco cisco-enable vnc ldap2 ldap3 mssql
 mysql oracle-listener postgres nntp socks5 rexec rlogin pcnfs snmp rsh cvs
 svn icq sapr3 ssh2 smtp-auth[-ntlm] pcanywhere teamspeak sip vmauthd

Module options are passed in with hydra.moduleOptions, and as defined by the
hydra man page are as follows:

	http[s]-{head|get}
			 specifies the page to authentication at (REQUIRED)
			  Value can be "/secret" or "http://bla.com/foo/bar" or
			  "https://test.com:8080/members"
	http-proxy       specifies the page to authentication at (OPTIONAL,
			  default is http://www.suse.com/)
	http[s]-form-{get|post}
			 specifies the page and the parameters for the web form.
			 the keyword "^USER^" is replaced with the login and
			 ^PASS^ with the password.
			 syntax:   <url>:<form parameters>:<failure string>
			 e.g.: /login.php:user=^USER^&pass=^PASS^&mid=123:incorrect
	smbnt            value [L,LH,D,DH,B,BH] (REQUIRED)
			  (L) Check local accounts, (D) Domain Accounts, (B) Either
			  (H) interpret passwords as NTLM hashes
	ldap2, ldap3     specifies the DN (OPTIONAL, you can also specify the DN
			  as login with -l)
	cisco-enable     specifies the logon password for the cisco device (OPTIONAL)
			 Note: if you have an AAA, use the -l option for the username
			 and the optional parameter for the password of the user.
	sapr3            specifies the client id, a number between 0 and 99 (REQUIRED)
	telnet           specified the string which is displayed after a successful
			  login (case insensitive), use if the default in the telnet
			  module produces too many false positives (OPTIONAL)
	postgres         database name to attack (OPTIONAL, default is template1)

TODO:
	- Make private functions _private
	- Need more explicit tests for hydra itself in doctests

"""

import tempfile, os, subprocess, time, re, sys

class hydra(object):
	"""
	Create and run a hydra scan.  

	Example usage:

		hm = hydra.hydra(host="hostname", module="ssh2")
		hm.passwordFile="/tmp/passwords"
		hm.userFile="/tmp/usernames"
		hydraResults = hm.scan()                 # Run the scan

		# other possible parameters
		hm.combinedFile="/tmp/input"            # Specifies a combined file with user:password
		hm.combinedUsers=["adm:pass", "tst:tt"] # Specifies a list of combined users and passwords
		hm.usernames=["admin", "root"]          # Specifies a list of usernames
		hm.passwords=["test", "admin"]          # Specifies a list of passwords
		hm.extraOptions="-f -e ns"              # Any extra cli params
		hm.port="22"                            # Use a port instaed of a specific module to define a service
		hm.moduleOptions="/secret"              # Something to pass in as module parameters


	Any value can be passed in during object creation, or changed directly
	with the object instance.

	Doctests:
	>>> hm = hydra(host="127.0.0.1", module="ssh2")
	>>> hm.combinedUsers=["admin:admin", "root:root", "guest:guest"]
	>>> hr = hm.scan()
	>>> hr.runtime >= 0
	True
	>>> len(hr)
	0
	>>> len(hr.errors)
	0
	>>> hr.attempts
	3
	>>> hr.command.startswith("hydra -C")
	True
	>>> hm = hydra(host="127.0.0.1", module="ssh2")
	>>> hm.usernames=["root", "guest", "admin"]
	>>> hm.passwords=["toor", "guest", "admin"]
	>>> hr = hm.scan()
	>>> hr.runtime >= 0
	True
	>>> len(hr)
	0
	>>> len(hr.errors)
	0
	>>> hr.attempts
	9
	>>> hr.command.startswith("hydra -L")
	True
	"""

	# Shared static data.  Map modules to specific ports.  
	# If a port is passed in, it will use the appropriate module
	# Most of this comes from hydra.h
	modules={ "cvs" :        (2401,5999), 
	          "ftp" :        (21, 990),
		  "telnet" :     (23, 992),
		  "http" :       (80,443,591,2301,8000,8001,8008,8080,8081,8082,9090),
		  "http-proxy" : (3128,),
		  "pop3" :       (110,995),
		  "nntp" :       (119, 563),
		  "smb" :        (139,),
		  "smbnt" :      (445,),
		  "imap" :       (143,220,585,993),
		  "ldap" :       (363, 636),
		  "rexec" :      (512,),
		  "rlogin" :     (513,),
		  "rsh" :        (514,),
		  "sock5" :      (1080,),
		  "icq" :        (4000,),
		  "vnc" :        (5900, 5901), 
		  "mysql" :      (3306,),
		  "mssql" :      (1433,),
		  "postgres":    (5432,),
		  "oracle" :     (1521,),
		  "pcanywhere":  (5631,),
		  "smtp-auth":   (25, 465),
		  "svn" :        (3690,),
		  "snmp" :       (161, 1993),
		  "ssh2" :        (22,),
		  "teamspeak":   (8767,),
		  "sip" :        (5060,),
		  "vmauthd" :    (902,) }

	# if a port is listed here, we'll automatically turn on ssl
	sslports=(990, 992, 443, 995, 563, 993, 636, 5901, 1993, 465)

	# Create temp files for Users, Passwords or a combined file as needed
	def writeUserPassFiles(self):
		if not self._userFilePath and self._usernames:
			self._isUserFileTemp = 1
			(self._userFileFh, self._userFilePath) = tempfile.mkstemp(".txt", "hydraUsers_")
			self._userFileFh = os.fdopen(self._userFileFh, "w")

			# Write out the users file
			[self._userFileFh.write(line + "\n") for line in self._usernames]
			self._userFileFh.close()

		if not self._passFilePath and self._passwords:
			self._isPasswordFileTemp = 1
			(self._passFileFh, self._passFilePath) = tempfile.mkstemp(".txt", "hydraPasswords_")
			self._passFileFh = os.fdopen(self._passFileFh, "w")

			# Write out the password file
			[self._passFileFh.write(line + "\n") for line in self._passwords]
			self._passFileFh.close()

		if not self._combinedFilePath and self._combinedUsers:
			self._isCombinedFileTemp = 1
			(self._combinedFileFh, self._combinedFilePath) = tempfile.mkstemp(".txt", "hydraCombinedUsers_")
			self._combinedFileFh = os.fdopen(self._combinedFileFh, "w")

			# Write out the combined user/pass file
			[self._combinedFileFh.write(line + "\n") for line in self._combinedUsers]
			self._combinedFileFh.close()


	### Generic Getters
	### Documentation for each is in the property creation section
	def getCombinedFile(self):  return self._combinedFilePath
	def getCombinedUsers(self): return self._combinedUsers
	def getExtraOptions(self):  return self._extraOptions
	def getHost(self):          return self._host
	def getModule(self):        return self._module
	def getModuleOptions(self): return self._moduleOptions
	def getPasswordFile(self):  return self._passFilePath
	def getPasswords(self):     return self._passwords
	def getPort(self):          return self._port
	def getScanCmd(self):       return self._scancmd
	def getSsl(self):           return self._ssl
	def getTasks(self):         return self._tasks
	def getUserFile(self):      return self._userFilePath
	def getUsernames(self):     return self._usernames

	### Generic Setters
	def setCombinedUsers(self, combinedUsers): self._combinedUsers=combinedUsers
	def setExtraOptions(self, opts=None):      self._extraOptions=opts
	def setHost(self, host):                   self._host=host
	def setModuleOptions(self, moduleOptions): self._moduleOptions=moduleOptions
	def setPasswords(self, passwords):         self._passwords=passwords
	def setSsl(self, ssl):                     self._ssl=ssl
	def setUsernames(self, usernames):         self._usernames=usernames

	### All other getters and setters
	def setCombinedFile(self, combinedFile): 
		if os.path.exists(combinedFile):
			self._combinedFilePath=combinedFile
		else:
			raise hydraException("Input combined user/pass file [%s] does not exist")

		self._isCombinedFileTemp = 0

	def setPasswordFile(self, passwordFile): 
		if os.path.exists(passwordFile):
			self._passFilePath=passwordFile
		else:
			raise hydraException("Input password file [%s] does not exist")

		self._isPasswordFileTemp = 0

	def setUserFile(self, userFile): 
		if os.path.exists(userFile):
			self._userFilePath=userFile
		else:
			raise hydraException("Input user file [%s] does not exist")

		self._isUserFileTemp = 0

	def setModule(self, module=None):
		if module in hydra.modules:
			self._module=module
		else:
			raise hydraException("Invalid hydra scan service module [%s]" % type)

		if module == "smbnt" and not self._moduleOptions:
			self._moduleOptions = "B"

		return self._module

	def setPort(self, port):
		# If the module is not already set, we'll set it here
		try:
			port=int(port)
		except ValueError:
			raise hydraException("Must pass in valid port (port was [%s])" % port)

		if not self._module:
			for (module, items) in self.modules.items():
				if port in items:
					self._module=module

		# Use ssl if needed
		if port in self.sslports: self._ssl=1

		self._port=port

	def setTasks(self, tasks):
		try:
			tasks = int(tasks)
		except ValueError:
			raise hydraException("Task num must be positive integer")	
		self._tasks=tasks

	def scan(self):
		"""
		Run the hydra scan.  Returns a hydraResult()
		"""

		self._scancmd="hydra "

		#########################################
		###  Assemble command
		#########################################
		self.writeUserPassFiles()

		# Need one or the other
		if not (((self._userFilePath and self._passFilePath) or self._combinedFilePath)):
			raise hydraException("Need User and Pass file, or combined files set")

		# Need one or the other, but not both (Does python not have a logical xor?)
		if ((self._userFilePath or self._passFilePath) and self._combinedFilePath):
			raise hydraException("Need User and Pass file, or a combined set, but not both")

		# Example hydra usage:
		# Syntax: hydra [[[-l LOGIN|-L FILE] [-p PASS|-P FILE]] | [-C FILE]] [-e ns]  [-o FILE] [-t TASKS] 
		#               [-M FILE [-T TASKS]] [-w TIME] [-f] [-s PORT] [-S] [-vV]  server service [OPT]

		if self._userFilePath:
			self._scancmd += "-L %s " % self._userFilePath

		if self._passFilePath:
			self._scancmd += "-P %s " % self._passFilePath

		if self._combinedFilePath:
			self._scancmd += "-C %s " % self._combinedFilePath

		if self._extraOptions:
			self._scancmd += "%s " % self._extraOptions

		if self._tasks:
			self._scancmd += "-t %s " % self._tasks

		if self._ssl:
			self._scancmd += "-S "

		if self._debug:
			self._scancmd += "-V "

		if self._host:
			self._scancmd += "%s " % self._host
		else:
			raise hydraException("No Host defined")

		if self._module:
			self._scancmd += "%s " % self._module
		else:
			raise hydraException("No Module defined, must pass in a valid port or module name")

		if self._moduleOptions:
			self._scancmd += "%s " % self._moduleOptions

		# Set results in case of multiple scans
		self._results=hydraResult(command=self._scancmd, host=self._host)

		#########################################
		###  Start running hydra
		#########################################

		runTime=int(time.time())

		#print " DEBUG: running [%s]" % self._scancmd
		# line buffered hydra process
		try:
			proc = subprocess.Popen(self._scancmd.rstrip().split(" "), stderr=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=1)
		except OSError, msg:
			raise hydraException("FATAL ERROR: Hydra could not be run, please check the binary [%s]" % msg)

		output = proc.stdout	
		errors = proc.stderr

		# parse hydra output
		for line in output:
			# Example sucess input line:
			# [22][ssh2] host: 127.0.0.1   login: aaronp   password: testpassword
			match = re.search(r"\[(\d+)\]\[(.*?)] host: ([^\s]+)[\s]*login: ([^\s]+)[\s]*password: ([^\s]+)[\s]*$", line)
			if match:
				(port, protocol, host, login, password) = match.groups()
				
				# Add found password to result set
				self._results.append((host, port, protocol, login, password))

			# Find the number of login tries that were performed
			match = re.search(r"^\[DATA\].*\s+(\d+)\s+login tries", line)
			if match:
				attempts = int(match.group(1))
				self._results.attempts=attempts

			# Grab all hydra data in case we need it for debugging later.
			if self._debug: self._results.debug.append(line)

		# This is what you get in stdout when a module isn't available: "Error: Unknown service"
		for line in errors:
			if line.startswith("Error: Unknown service"):
				raise hydraException("Module [%s] was unknown by hydra..." % self._module)
				continue

			if line.startswith("Error: Compiled without"):
				raise hydraException("Module [%s] was not compiled into hydra..." % self._module)
				continue

			if re.search(r"^Error:.*can not connect", line):
				self._results.errors.append("ERROR: hydra could not connect remote service")
				continue

			if re.search(r"^Error:.*$", line):
				self._results.errors.append("ERROR: Unknown hydra error [%s]" % line.rstrip())
				continue

		# Make sure the number of attempts isn't too high, otherwise it's probably
		# false positive results...
		if self._results.attempts > 6 and len(self._results) >= 0.2 * attempts:
			errstr = "WARN: Too many successful hydra logins, they are probably false positives [%s] of [%s]" % (len(self._results), self._results.attempts)
			self._results.errors.append(errstr)

		# Get actual return code
		rc = proc.wait()
		if rc == 127:
			raise hydraException("hydra not found in path")
		elif rc >= 1:
			raise hydraException("Execution of hydra failed, rc was [%s]" % rc)
		runTime = int(time.time()) - runTime

		# Other results updates
		self._results.errors += errors.readlines()
		self._results.runtime = runTime

		# Clean up temp files as needed
		# We should be done with these files now that the scan is finished
		if self._userFilePath and os.path.exists(self._userFilePath) and self._isUserFileTemp: 
			os.unlink(self._userFilePath)
			self._userFilePath=None
		if self._passFilePath and os.path.exists(self._passFilePath) and self._isPasswordFileTemp: 
			os.unlink(self._passFilePath)
			self._passFilePath=None
		if self._combinedFilePath and os.path.exists(self._combinedFilePath) and self._isCombinedFileTemp: 
			os.unlink(self._combinedFilePath)
			self._combinedFilePath=None

		return self._results

	def __repr__(self):
		print self.getScanCmd()

	def __str__(self):
		print self.getScanCmd()

	#host, port, module, passwordFile, userFile, combinedFile, usernames, passwords, extraOptions, moduleOptions
	def __init__(self, host=None, port=None, module=None, passwordFile=None, userFile=None, combinedFile=None, 
	             usernames=[], passwords=[], combinedUsers=[], extraOptions=None, moduleOptions="", tasks=None, 
	             debug=None, ssl=None):

		# This is where all results of the scan are stored
		self._results=hydraResult()

		# Some other flags so we don't write over or delete user specified files
		self._isUserFileTemp = 0 
		self._isPassFileTemp = 0 
		self._isCombinedFileTemp = 0 

		self._userFilePath = None
		self._userFileFh = None
		self._passFilePath = None
		self._passFileFh = None
		self._combinedFilePath = None
		self._combinedFile = None
		self._combinedFileFh = None
		self._combinedUsers = None
		self._extraOptions = None
		self._host = None
		self._module = None
		self._moduleOptions = None
		self._passwords = None
		self._port = None
		self._scancmd = None
		self._ssl = None
		self._tasks = None
		self._usernames = None
		self._isPasswordFileTemp = None

		self._debug = debug

		if userFile:      self.setUserFile(userFile)
		if passwordFile:  self.setPasswordFile(passwordFile)
		if combinedFile:  self.setCombinedFile(combinedFile)
		if combinedUsers: self.setCombinedUsers(passwords)

		if host:          self.setHost(host)
		if port:          self.setPort(port)
		if ssl:           self.setSsl(ssl)
		if tasks:         self.setTasks(tasks)
		if usernames:     self.setUsernames(usernames)
		if passwords:     self.setPasswords(passwords)
		if extraOptions:  self.setExtraOptions(extraOptions)
		if moduleOptions: self.setModuleOptions(moduleOptions)

		if module:        self.setModule(module)


	# passing in a pre-existing file that has user:password entries per line
	combinedFile =  property(getCombinedFile, setCombinedFile, None, None)
	# a List of user/passwords in the form of user:password
	combinedUsers = property(getCombinedUsers, setCombinedUsers, None, None)
	# extra CLI parameters as a text string
	extraOptions =  property(getExtraOptions, setExtraOptions, None, None)
	# set the hostname of the system to scan
	host =          property(getHost, setHost, None, None)
	# the name of the module or service to run (e.g. ssh2)
	module =        property(getModule, setModule, None, None)
	# a string for module specific options (see hydra README)
	moduleOptions = property(getModuleOptions, setModuleOptions, None, None)
	# passing in a pre-existing file that has password entries per line
	passwordFile =  property(getPasswordFile, setPasswordFile, None, None)
	# a List of passwords to try
	passwords =     property(getPasswords, setPasswords, None, None)
	# the port to run on (which will imply which service to run)
	port =          property(getPort, setPort, None, None)
	# whether to use ssl to connect to the service
	ssl =           property(getSsl, setSsl, None, None)
	# The number of parallel tasks to run (default: 16)
	tasks =         property(getTasks, setTasks, None, None)
	# passing in a pre-existing file that has user entries per line
	userFile =      property(getUserFile, setUserFile, None, None)
	# a List of usernames to try
	usernames =     property(getUsernames, setUsernames, None, None)


class hydraResult(list):
	"""
	Results from a hydra.scan()

	Example usage:
		hydraResults = hydra.scan()

		for result in hydraResults:
			(host, port, protocol, login, password) = result
			print "Runtime was  %s" % result.runtime

	Any errors will be in hydraResults.errors

	"""

	__slots__ = ['attempts', 'command', 'debug', 'host', 'output', 'errors', 'runtime']

	def __init__(self, attempts=0, command=None, errors=[], host=None, runtime=None):
		# This is the command that hydra was called with
		self.command=command
		self.debug=[]
		self.errors=errors
		self.host=host
		self.runtime=runtime
		self.attempts=attempts
		# This is for the __str methods
		self.output=""
		list.__init__(self)

	def __str__(self):
		"""
		Print a basic output report from the results
		"""

		self.output=""
		if self.command:
			self.output = " [*] Hydra command was [%s]\n" % self.command
			if self.runtime:
				self.output+= " [*] Runtime was [%s] seconds\n" % self.runtime
			if len(self) == 0:
				self.output += " [-] No tested logins were successful...\n"
			for result in self:
				self.output += " [***] Host: [%s] Port: [%s] Protocol: [%s] Login: [%s] Password: [%s]\n" % result
			if self.errors:
				for error in self.errors:
					self.output += " [!] ERROR: [%s]\n" % error
			if self.attempts:
				self.output += " [*] [%s] login attempts were made" % self.attempts
				
		return self.output

	def __repr__(self): 
		return self.__str__()

class hydraException(Exception): 
	"""
	Exceptions within the hydra module
	"""
	pass


if __name__=="__main__":
	"""
	A test of the hydra module against a few ports on 127.0.0.1
	"""

	import doctest
	print " [*] Running Hydra module tests against localhost ssh (tests will fail if it's not open)"
	doctest.testmod()

	print " [*] Done."
