#!/usr/bin/env python
"""
depant -- (DE)fault (PA)ssword (N)etwork (T)ool
(c) 2008, Aaron Peterson, Midnight Research Labs

Scans a given network for open services, and then checks them for default passwords.
See README and INSTALL files for more details.  See LICENSE for that sort of info.


TODO:
	- Add auto-parsing of web-pages for login forms to test
	- Test individual services and add more ports to hydraSafePorts
	- Real hackers do it in parallel
	- Add ctrl-c trapping to make sure subprocesses don't get orphaned
"""

import getopt, sys, os, time, socket, re
import nmap, hydra

try:
        import ipcalc
except:
        print " [*] The 'ipcalc' python module needs to be installed (easy_install ipcalc)"
	sys.exit(1)

class depant(object):
	"""
	Parent class for depant
	"""

	# Ports that we're pretty sure (in theory) will work fine without extra module options or high error rates
	# It's probably better to remove ports from this list than even try to test every single option. 
	# Removed: 139
	hydraSafePorts = [2401, 5999, 21, 990, 23, 992, 80,443,591,2301,8000,8001,8008,8080,8081,8082,9090, 3128, 110,995, 119, 563, 445, 143,220,585,993, 363, 636, 512, 513, 514, 1080, 4000, 5900, 5901, 3306, 1433, 5432, 1521, 5631, 25, 465, 3690, 161, 1993, 22, 8767, 5060, 902]
	# This is list of ports that can't run unless extra options are specified
	# http*, and ldap
	portsRequireOptions = [80,443,591,2301,8000,8001,8008,8080,8081,8082,9090,3128,363,636]

	def scan(self):
		"""
		Start the actual scan of hosts.  This will run both phases as required.
		"""
		self._openLog()
		self._phase1Scan()

		if self.dryRun: 
			print " [*] Operating in dry run mode, so not going on to test passwords."
			return self.results
		elif not self.results.numResults and self.runSecondPhase:
			print " [*] We did not find results in phase one... going to second phase"
			self._phase2Scan()
		elif not self.results.numResults and not self.runSecondPhase:
			print " [*] We did not find results in phase one... but not configured to run second phase"
		elif self.results.numResults and self.runSecondPhase:
			print " [*] Already found results, no need to run second phase"

		return self.results

	def _openLog(self):
		if self.logPath:
			try:
				self.logFile = open(self.logPath, "w")
			except IOError:
				print " [!] There was a problem opening logfile [%s]" % self.logPath
				sys.exit(1)

			# Write header
			self.logFile.write("# output in csv: host,port,user,pass\n")

	def _phase1Scan(self):
		print " [*] Starting phase 1 nmap scan of [%s] host(s)" % len(self.hosts)
		nm = nmap.nmap(hosts=self.hosts, ports=self.ports, extraOptions=self.nmapOptions)
		try:
			nmapResults = nm.scan()
		except nmap.nmapException, msg:
			print " [!!] There was an error running nmap, msg was [%s]" % msg
			sys.exit(1)

		if self.debug: print " [-] Nmap command was [%s]" % nmapResults.command

		count=0
		for host in nmapResults:
			for port in host.getPorts("open"):
				print " [*] Adding host [%s] port [%s] to list of services to test" % (host.hostname, port.port)
				count += 1

		print " [*] Found [%s] thing(s) to check for default passwords" % count

		# Check if there's anything to do
		if not count:
			return self.results

		if self.dryRun: return 0

		print " [*] Starting phase 1 hydra scans"
		for host in nmapResults:
			for port in host.getPorts("open"):
				self._runHydra(host=host.hostname, port=port.port, passwords=self.phase1Passwords, 
				               usernames=self.phase1Users, combinedUsers=self.phase1Combined)

		if self.fastestHost[2]:
			print " [*] Fastest service to run second phase on is [%s] port [%s]" % (self.fastestHost[0], self.fastestHost[1])

	def _phase2Scan(self):
		print " [*] Starting phase 2"

		if self.fastestHost[2]:
			self._runHydra(self.fastestHost[0], self.fastestHost[1], self.phase2Passwords, self.phase2Users, self.phase2Combined)
		else:
			print " [*] Couldn't find valid results to base phase 2 hosts on"

	def _printHydraErrors(self, hydraResults):
		errCounts = {}
		for err in hydraResults.errors:
			errCounts[err] = errCounts.setdefault(err, 0) + 1

		for err in errCounts.keys():
			print " [!] [%s] seen [%s] times" % (err, errCounts[err])

	def _runHydra(self, host, port, passwords, usernames, combinedUsers):
		print " [*] Checking for default passwords on host [%s] port [%s]" % (host, port)
		try:
			hm = hydra.hydra(host=host, port=port, passwordFile=passwords, userFile=usernames, combinedFile=combinedUsers, debug=self.debug)
			hr = hm.scan()
		except hydra.hydraException, msg:
			print " [!!] There was a problem running hydra, msg was [%s]" % msg
			print " [!!] You may get inconsistent results..."
			hr = None

		if type(hr) is hydra.hydraResult:
			self.results.append(hr)
			for (host, port, protocol, login, password) in hr:
				print " [!!!] Found user [%s] with pass [%s] on [%s] service/port [%s]" % (login, password, host, port)
				if self.logFile:
					self.logFile.write("%s,%s,%s,%s\n" % (host, port, login, password))

			# Need to set the "fastest host" for phase2 scanning...
			if (self.fastestHost[2]==None) or (self.fastestHost[2] > hr.runtime):
				self.fastestHost=(host, port, hr.runtime)

			self._printHydraErrors(hr)

			if self.debug:
				print " [-] Hydra command was [%s]" % hr.command
				for line in hr.debug:
					print " [-] Hydra Debug: [%s]" % line.rstrip()

	def __init__(self=None, phase1Users=None, phase1Passwords=None, phase1Combined=None,
		                phase2Users=None, phase2Passwords=None, phase2Combined=None,
				debug=None, dryRun=None, hosts=None, ports=None, runSecondPhase=None, 
	                        nmapOptions="", logPath=None):

		self.phase1Users = phase1Users 
		self.phase1Passwords = phase1Passwords 
		self.phase1Combined = phase1Combined
		self.phase2Users = phase2Users 
		self.phase2Passwords = phase2Passwords 
		self.phase2Combined = phase2Combined
		self.runSecondPhase = runSecondPhase
		self.fastestHost=(None,None,None)
		self.nmapOptions = nmapOptions
		self.debug = debug
		self.dryRun = dryRun
		self.hosts = hosts
		self.logPath = logPath
		self.logFile = None
		self.ports = ports
		self.results = depantResult()

class depantException(Exception):
	pass

class depantResult(list):
	"""
	This is the results from running the depant class. It is a 
	List of hydraResults with a couple extra members
	"""

	__slots__ = ["errors", "runTime", "numResults"]

	def getNumResults(self):
		"""
		Get the number results, or successful logins
		"""
		rlen=0
		# Because the actual results are in the List of Lists, we want to only count sub list items
		for result in self:
			rlen += len(result)
		return rlen

	def __init__(self, errors=[], runTime=None):
		self.errors=errors
		self.runTime=runTime

	numResults=property(getNumResults, None, None, None)


###########################################################
# Local stuff
###########################################################
def usage():
	print """
usage: %s ( -H <host> | -f <hostList>) ( -c <userPassList> | -u <userList> -p <passList>)  <options>
   Options:
	-H <host (or CIDR block) to scan>
	-f <host list file> (each ip or CIDR block per line)
	-e <exclude hosts list> (each ip or CIDR block per line) 
	-g <output file for default password list> (Gets list from Phenoelit site)
	-c <combined user:password list> (not in conjunction with -u/-p)
	-u <username list>  (used in conjunction with password list)
	-p <password list>  (used in conjunction with username list)
	-n <nmap options>   (default is "-n -T4 -P0", and anything you specify will replace these options)
	-o <port list>      (e.g. 21,22,137-139 default is "safe ports")
	-O <output file>    (CSV log of any user/passwords we find)
	-C <second phase combined user:password list> (not in conjunction with -U/-P)
	-U <second phase user list>
	-P <second phase password list>
	-A (Run all ports hydra knows about)
	-i (Do ICMP scan.  By default it will scan everything.  This removes -P0 from nmap scan.
	    If mapping the network is taking forever, try enabling this)
	-D (Do a dry run only, map network, and output what things are going to be checked)
	-h (help)
	-d (debug)

   Examples:
	Downloads the default password list into dpl.txt:

		depant -g ./dpl.txt

	Checks for the user:pass combinations in dpl.txt on all ports for ips in hosts.txt:

		depant -f ~/hosts.txt -d -A -c dpl.txt

	Checks the network services anywhere in 192.168.1.1/24 (excluding hosts listed in exclude.txt)
	with the users and passwords specified, and if nothing is found, it will check the 
	larger user and dictionary list against the fastest service.  Also adds optional flags to send 
	to nmap for optimization:

		depant -A -H 192.168.1.1/24 -e exclude.txt -u users.txt -p passwd.txt -U more-users.txt -P big-dict.txt -n "-T5 -P0"

""" % sys.argv[0]
	sys.exit(1)

def checkFile(filename):
	"""
	Check if the file exists...
	"""
	if filename:
		if not os.path.exists(filename):
			print " [!] Specified user/pass file [%s] does not exist..." % filename
			sys.exit(1)

def enumerateHosts(hosts, isExclude=None):
	"""
	Go through each of the hosts and validate them, and enumerate any CIDR blocks that we run across
	"""
	hostsRemove=[]
	hostsAdd=[]
	global debug
	for host in hosts:
		matchIp = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$").search(host)
		matchCidr = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\\|/)\d{1,2}$").search(host)
		if not matchIp and not matchCidr:
			# We can't parse it as an ip, so it must be a hostname.  We remove it from the 
			# list, resolve it and add it again if it resolves.  If not, then something's 
			# wrong, so we don't re-add it.
			hostsRemove.append(host)
			try:
				host = socket.gethostbyname(host)
			except (socket.gaierror, socket.error):
				print " [*] Host [%s] cannot be resolved... ignoring" % host
				continue
			hostsAdd.append(host)
		if matchCidr:
			if debug: print " [-] Enumerating hosts in CIDR block [%s]" % host
			try:
				network = ipcalc.Network(host)
			except ValueError:
				print " [*] Host [%s] not parseable, so ignored..." % host
				hostsRemove.append(host)
				continue
			count = -1
			for ip in network:
				count += 1
				# Not adding the network or broadcast ips
				if count == 0 or count == network.size() - 1: continue
				hostsAdd.append(str(ip))
			# remove the CIDR block because we added each ip instance
			hostsRemove.append(host)

	# Add or remove the old hosts as needed
	hosts+=hostsAdd
	for host in hostsRemove: hosts.remove(host)

	# We only want to error on the main hosts list, not the exclude list
	if len(hosts) == 0 and not isExclude:
		print " [*] No hosts defined..."
		usage()

	return hosts

def doExcludeHosts(hosts, exclusions):
	"""
	Remove everything in hosts that is also in exclusions
	"""
	global debug
	for excl in exclusions:
		if excl in hosts: 
			hosts.remove(excl)
			if debug: print " [-] Removing excluded host [%s]" % excl

	return hosts
		

def getDpl(dplFile):
	"""
	Get the latest default password list from the Phenoelit page...
	"""
	if os.path.exists(dplFile):
		print " [*] Output File [%s] exists... won't overwrite.." % dplFile
		sys.exit(1)

	dpl=[]
	dplHost = "www.phenoelit-us.org"
	dplPort = 80
	path = "/dpl/dpl.html"
	print " [*] Connecting to [%s] for default password list" % (dplHost + path)
	sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	addr  = (dplHost,dplPort)
	httpmsg = "GET "+path+" HTTP/1.0\r\nHost: www.phenoelit-us.org\r\n\r\n"
	try:
		sock.connect(addr)
		sock.send(httpmsg)
		lines = ""
		while(1):
			rec = sock.recv(1024)
			if not rec: break
			lines += rec
	except:
		print " [!] There was a problem getting the default password list from [%s].." % dplHost

	recArray = lines.split("<TR>")
	for line in recArray:
		username=""
		password=""
		lineElements = line.split("</TD>")
		try:
			# strip tags, etc.
			password = re.search(r"^\s*(<[^>]+>)+([^<>]*)(<[^>]+>)*$", lineElements[5]).group(2)
			username = re.search(r"^\s*(<[^>]+>)+([^<>]*)(<[^>]+>)*$", lineElements[4]).group(2)

			if ((username == "n/a") or (username == "(none)")): username = ""
			if ((password == "n/a") or (password == "(none)")): password = ""

			# take care of some annoying exceptions...
			password=password.replace("(exclamation)", "!")
			username=username.replace("(any 3 characters)", "aaa")
			password=password.replace("smcadmin OR administrator OR (none)", "smcadmin")
			password=password.replace("the same all over", "Administrator")
			if password.startswith(r"2 + last 4 of Audio Server"): continue
			if password.startswith(r"the 6 last digit of the MAC adress"): continue
			if password == "" and username == "": continue
			
			# There are lots of repeats, get a unique list...
			combo = username + ":" + password
			if combo not in dpl: dpl.append(combo)
		except:
			pass

	try:
		dplOut = open(dplFile, "w")
	except IOError:
		print " [*] Couldn't open output file [%s]" % dplFile
		sys.exit(1)

	# print out all matches
	for line in dpl: dplOut.write(line + "\n")
	print " [*] Default Password list (in combined format) is in [%s]" % dplFile
	print " [*] You can use these default passwords by specifying '-c %s'" % dplFile
	print " [*] Done."
	sys.exit(0)

def getPortsList(ports, useAllPorts):
	""" 
	Setup the ports to scan.  
	"""
	allPorts=[]
	# Look to see what ports hydra defines as known ports
	for key in hydra.hydra.modules.keys():
		for port in hydra.hydra.modules[key]:
			if port not in depant.portsRequireOptions:
				allPorts.append(port)
	# Use all ports that don't require extra module options...
	if not ports and useAllPorts:
		ports=allPorts
	# Verify that specified port list is valid
	elif ports:
		# iterate backwards so we can remove unneeded ports as we go
		for portIdx in xrange(len(ports) -1, -1, -1):
			if ports[portIdx] not in allPorts: 
				if debug: print " [-] Removing non-service port [%s]" % ports[portIdx]
				ports.pop(portIdx)

		# If we used to have ports, and now we don't, then we removed them all during the checks
		if not ports:
			print " [*] You specified only ports that are unknown by hydra, try using -A to scan all known ports"
			sys.exit(1)
	# Default to "safe" ports
	else:
		ports=depant.hydraSafePorts

	return ports

def readHostsFile(file, hosts):
	"""
	Reads in a file of hosts, and creates a list of the hosts from it
	"""
	try:
		hostfd=open(file, "r")
	except IOError:
		print " [*] Could not open hosts file [%s]" % file
		sys.exit(1)
		
	for line in hostfd:
		if line.startswith("#"): continue
		hosts.append(line.rstrip())

	hostfd.close()
	return hosts
	

##################################################################################
### Start Main
##################################################################################
def main():
	global debug
	global verbose
	global version
	global runTime

	# this is our top-level list of Hosts
	hosts=[]
	hostsFile=None
	excludeHosts=[]
	excludeFile=None
	# list of all ports, if empty, use all ports available.
	ports=[]
	dryRun=0
	logPath=None
	logFile=None
	nmapOptions="-n -T4 -P0"
	useAllPorts=0
	verbose=0
	version="0.3a"
	runSecondPhase=0
	debug=0
	phase1Users = phase1Passwords = phase1Combined = None
	phase2Users = phase2Passwords = phase2Combined = None
	runTime=int(time.time())

	##################################################################################
	### Get/parse opts and set things up
	##################################################################################
	print "\n\t-=[[ Depant v%s ]]=-" % version
	print "   -=[[ Midnight Research Labs ]]=-\n"

	try:
		(options, leftOvers)=getopt.getopt(sys.argv[1:], "2Ac:C:dDe:hH:if:g:n:o:O:p:P:u:U:v")
	except getopt.GetoptError:
		usage()

	for o, v in options:
		if o == "-A": useAllPorts=1
		if o == "-c": phase1Combined=v
		if o == "-C": phase2Combined=v
		if o == "-d": debug=1
		if o == "-D": dryRun=1
		if o == "-e": excludeFile=v
		if o == "-f": hostsFile=v
		if o == "-g": getDpl(v)
		if o == "-h": usage()
		if o == "-H": hosts.append(v)
		if o == "-i": nmapOptions = nmapOptions.replace(" -P0", "")
		if o == "-n": nmapOptions = v
		if o == "-o":
			# parse the ports list
			chunks=v.split(",")
			for chunk in chunks:
				chunk=chunk.strip()
				if "-" in chunk:
					(a,b)=chunk.split("-")
					for p in range(int(a), int(b) + 1):
						ports.append(p)
				else:
					try:
						ports.append(int(chunk))
					except ValueError:
						print " [!] Port [%s] is not valid" % chunk
						sys.exit(1)
		if o == "-O": logPath=v
		if o == "-p": phase1Passwords=v
		if o == "-P": phase2Passwords=v
		if o == "-u": phase1Users=v
		if o == "-U": phase2Users=v
		if o == "-v": 
			verbose=1 
			debug=1
			
	if logPath and os.path.exists(logPath):
		print " [!] Logfile [%s] already exists... won't overwrite." % logPath
		sys.exit(1)

	# Probably not the most pythonic way to do this...
	rc=os.system("which hydra>/dev/null 2>&1")
	if rc == 1: 
		print " [!] Couldn't find hydra.  If it's already installed make sure it's in your $PATH"
		print "     Check the hydra page for more info: http://www.thc.org/thc-hydra/\n"
		print "     There are also hydra build instructions in the depant INSTALL file.\n"
		print "     which may help if you run into problems compiling it...\n"
		sys.exit(1)
	rc=os.system("which nmap>/dev/null 2>&1")
	if rc == 1: 
		print " [!] Couldn't find nmap.  If it's already installed make sure it's in your $PATH"
		print "     Check the nmap page for more info: http://nmap.org/\n"
		sys.exit(1)

	# Check for proper user/pass files (first and second stage)
	for file in [phase1Users, phase2Users, phase1Passwords, phase2Passwords, phase1Combined, phase2Combined]:
		checkFile(file)

	# See if we have passwords for the second phase
	if (phase2Combined or (phase2Users and phase2Passwords)):
		print " [*] Phase 2 scanning enabled"
		runSecondPhase=1
	elif debug:
		print " [*] Phase 2 scanning disabled (no user/pass files to use)"
		
	# Make sure we don't have conflicting user/pass file combinations for first and second phases
	if (phase1Combined and (phase1Users or phase1Passwords) or (phase2Combined and (phase2Users or phase2Passwords))):
		print " [*] You must specify either username and password files or a combined file (but not both)\n"
		usage()

	# Make sure we have at least one set of u/p
	# These could all be simplified if there were a logical xor in python... :(
	if not (phase1Combined or (phase1Users and phase1Passwords)):
		print " [*] You must specify either username and password files or a combined file (but not both)\n"
		usage()

	# Same for the second phase
	if runSecondPhase and not (phase1Combined or (phase1Users and phase1Passwords)):
		print " [*] Second phase is selected, You must specify either username and password files or a combined file (but not both)\n"
		usage()

	if ports and useAllPorts:
		print " [*] Specifying specific ports (-p) and use all ports (-A) conflict\n"
		usage()

	if hostsFile:
		hosts=readHostsFile(hostsFile, hosts)
	if excludeFile:
		excludeHosts=readHostsFile(excludeFile, excludeHosts)

	hosts = enumerateHosts(hosts)
	excludeHosts = enumerateHosts(excludeHosts, isExclude=1)
	hosts=doExcludeHosts(hosts, excludeHosts)
	ports=getPortsList(ports, useAllPorts)

	if debug:
		print " [-] Will scan ports %s" % ports
		print " [-] Will scan hosts %s" % hosts

	##################################################################################
	### Start doing things
	##################################################################################

	dm = depant(phase1Users=phase1Users, phase1Passwords=phase1Passwords, phase1Combined=phase1Combined, 
	            phase2Users=phase2Users, phase2Passwords=phase2Passwords, phase2Combined=phase2Combined,
	            debug=debug, dryRun=dryRun, runSecondPhase=runSecondPhase, nmapOptions=nmapOptions, hosts=hosts, 
	            ports=ports, logPath=logPath)
	try:
		results = dm.scan()
	except depantException, msg:
		print " [*] There was a problem running depant, msg was [%s]" % msg

	if results.numResults:
		print " [!!!] We found logins on [%s] hosts" % results.numResults
	elif not dryRun:
		print " [*] No logins were found... :("

	print " [*] Total runtime was [%s] seconds" % str(int(time.time()) - runTime)
	print " [*] Finished."
	sys.exit(0)

if __name__=="__main__":
	main()
