View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update | 
|---|---|---|---|---|---|
| 0003821 | Kali Linux | Kali Package Bug | public | 2017-01-06 18:47 | 2017-12-05 13:37 | 
| Reporter | TheNaterz | Assigned To | |||
| Priority | normal | Severity | tweak | Reproducibility | always | 
| Status | resolved | Resolution | fixed | ||
| Product Version | 2016.2 | ||||
| Fixed in Version | 2018.1 | ||||
| Summary | 0003821: polenum-0.2 inaccurately reports time values associated with password policies | ||||
| Description | polenum is a python script used by enum4linux to report password policy information. We identified and fixed 2 issues related to this report. In the first issue, polenum was using the 'days' variable for 1 hour and 1 minute values. These have been changed to the 'hours' and 'minutes' variables respectively. Polenum was also not correctly concatenating the time string. If a duration is set to 1 day 1 hour 1 minutes (1501 minutes), polenum will report it as 1 minute. This has been fixed as well. The second issue deals with how polenum was evaluating the 8-byte integer representing time values. Polenum was converting this number into 2 4-byte integers, determining if a value is 'Not Set' or 'None', then converting back into an 8-byte integer. If an account lockout period is set to a non-standard value (e.g. 61 minutes), the final 8-byte integer differs from the original and incorrectly reports the lockout period. We've submitted a fix for this so that the original 8-byte integer is preserved while still allowing for the ability to evaluate 'Not Set' and 'None' values.  | ||||
| Steps To Reproduce | 1st issue: 
 2nd issue: 
  | ||||
| Additional Information | We're currently hosting a patched version of polenum on our public Github: https://github.com/RiskSense-Ops/polenum. I've also attached the patched polenum.py script. Please review and let us know if any further issues are encountered.  | ||||
| Attached Files |  polenum.py (12,053 bytes)   
 
#!/usr/bin/python
"""
	This Python Script Uses Core's Impacket Library to get the password policy from a windows machine
		Testing has been limited so Let me know if it works/fails
	Version 0.2
			
 	Usage:./polenum.py [username[:password]@]<address> [protocol list...]
                Available protocols: ['445/SMB', '139/SMB']	
                	
	example: polenum aaa:[email protected] 
	
	Copyright (C) 20/08/2008 - deanx <RID[at]portcullis-secuirty.com>
	
	Version 0.2 
	
	* "This product includes software developed by
	*	   CORE Security Technologies (http://www.coresecurity.com/)."
"""
import socket
import string
import sys
import types
import time
from impacket import uuid
from impacket.dcerpc import dcerpc_v4, dcerpc, transport, samr
from impacket import ImpactPacket
from impacket.dcerpc.samr import * 
from impacket.smb import SessionError
def get_obj(name): return eval(name)
version = '0.2'
def usage(): # Self explanitory
    print __doc__
    
def d2b(a):
	bin = []
	while a:
		bin.append(a%2)
		a /= 2
	return bin[::-1]
class ExtendInplace(type):
	def __new__(self, name, bases, dict):
		prevclass = get_obj(name)
		del dict['__module__']
		del dict['__metaclass__']
		# We can't use prevclass.__dict__.update since __dict__
		# isn't a real dict
		for k,v in dict.iteritems():
			setattr(prevclass, k, v)
		return prevclass
def display_time(filetime_high, filetime_low, minutes_utc=0):
	import __builtins__
	d = filetime_low + (filetime_high)*16**8 # convert to 64bit int
	d *= 1.0e-7 # convert to seconds
	d -= 11644473600 # remove 3389 years?
	try:
		return strftime("%a, %d %b %Y %H:%M:%S +0000ddddd", localtime(d)) # return the standard format day
	except ValueError,e:
		return "0"		
def convert(value):
	low, high = unpack('<ll', pack("<q", value))
	if low == 0 and hex(high) == "-0x80000000":
		return "Not Set"
	if low == 0 and high == 0:
		return "None"
	tmp = int(1e-7*-value)
	try:
		minutes = int(strftime("%M", gmtime(tmp)))  # do the conversion to human readable format
	except ValueError, e:
		return "BAD TIME:"
	print(tmp)
	hours = int(strftime("%H", gmtime(tmp)))
	print(hours)
	days = int(strftime("%j", gmtime(tmp)))-1
	time = ""
	if days > 1:
		time += str(days) + " days "
	elif days == 1:
		time += str(days) + " day "
	if hours > 1:
		time += str(hours) + " hours "
	elif hours == 1:
		time += str(hours) + " hour "	
	if minutes > 1:
		time += str(minutes) + " minutes"
	elif minutes == 1:
		time += str(minutes) + " minute "
	return time
class MSRPCPassInfo:
	ITEMS = {'Minimum password':0,
			 'Password history':1,
			 'Maximum password age (d)':2,
			 'Password must meet complexity requirements':3,
			 'Minimum password age (d)':4,
			 'Forced logoff time (s)':5,
			 'Locked account time (s)':6,
			 'Time between failed logon (s)':7,
			 'Number of invalid logon before locked out (s)':8
			 }
			 
	PASSCOMPLEX = {	5:'Domain Password Complex:',
					4:'Domain Password No Anon Change:',
					3:'Domain Password No Clear Change:',
					2:'Domain Password Lockout Admins:',
					1:'Domain Password Store Cleartext:',
					0:'Domain Refuse Password Change:'
					}
				
	def __init__(self, data = None):
		self._min_pass_length = 0
		self._pass_hist = 0
		self._pass_prop= 0
		self._min_age_low = 0
		self._min_age_high = 0
		self._max_age_low = 0
		self._max_age_high = 0
		self._pwd_can_change_low = 0
		self._pwd_can_change_high = 0
		self._pwd_must_change_low = 0
		self._pwd_must_change_high = 0
		self._max_force_low = 0
		self._max_force_high = 0
		self._role = 0
		self._lockout_window_low = 0
		self._lockout_window_high = 0
		self._lockout_dur_low = 0
		self._lockout_dur_high = 0
		self._lockout_thresh = 0
	
		if data: self.set_header(data, 1)
	def set_header(self,data,level):
		index = 8
		if level == 1: 
			self._min_pass_length, self._pass_hist, self._pass_prop, self._max_age, self._min_age = unpack('<HHLqq',data[index:index+24])
			bin = d2b(self._pass_prop)
			if len(bin) != 8:
				for x in xrange(6 - len(bin)):
					bin.insert(0,0)
			self._pass_prop =  ''.join([str(g) for g in bin])	
		if level == 3:
			self._max_force = unpack('<q',data[index:index+8])[0]
		if level == 7:
			self._role = unpack('<L',data[index:index+4])
		if level == 12:
			self._lockout_dur, self._lockout_window, self._lockout_thresh = unpack('<qqH',data[index:index+18])
		
		
	def print_friendly(self):
	
		print "\n\t[+] Minimum password length: " + str(self._min_pass_length or "None")
		print "\t[+] Password history length: " + str(self._pass_hist or "None" )
		print "\t[+] Maximum password age: " + str(convert(self._max_age))
		print "\t[+] Password Complexity Flags: " + str(self._pass_prop or "None") + "\n"
		i = 0
		for a in self._pass_prop:
			#print "BIT " +str(i) + a
			print "\t\t[+] " + self.PASSCOMPLEX[i] + " " + str(a)
			i+= 1
		print "\n\t[+] Minimum password age: " + str(convert(self._min_age))
		print "\t[+] Reset Account Lockout Counter: " + str(convert(self._lockout_window))
		print "\t[+] Locked Account Duration: " + str(convert(self._lockout_dur))
		print "\t[+] Account Lockout Threshold: " + str(self._lockout_thresh or "None")
		#print "Server Role: " + str(self._role[0])
		print "\t[+] Forced Log off Time: " + str(convert(self._max_force))
		return
	
class SAMREnumDomainsPass(ImpactPacket.Header):
	OP_NUM = 0x2E
	__SIZE = 22
	def __init__(self, aBuffer = None):
		ImpactPacket.Header.__init__(self, SAMREnumDomainsPass.__SIZE)
		if aBuffer: self.load_header(aBuffer)
	def get_context_handle(self):
		return self.get_bytes().tolist()[:20]
	def set_context_handle(self, handle):
		assert 20 == len(handle)
		self.get_bytes()[:20] = array.array('B', handle)
	def get_resume_handle(self):
		return self.get_long(20, '<')
	def set_resume_handle(self, handle):
		self.set_long(20, handle, '<')
	def get_account_control(self):
		return self.get_long(20, '<')
	def set_account_control(self, mask):
		self.set_long(20, mask, '<')
	def get_pref_max_size(self):
		return self.get_long(28, '<')
	def set_pref_max_size(self, size):
		self.set_long(28, size, '<')
	def get_header_size(self):
		return SAMREnumDomainsPass.__SIZE
	
	def get_level(self):
		return self.get_word(20, '<')
	def set_level(self, level):
		self.set_word(20, level, '<')
class SAMRRespLookupPassPolicy(ImpactPacket.Header):
	__SIZE = 4
	def __init__(self, aBuffer = None):
		ImpactPacket.Header.__init__(self, SAMRRespLookupPassPolicy.__SIZE)
		if aBuffer: self.load_header(aBuffer)
	def get_pass_info(self):
		return MSRPCPassInfo(self.get_bytes()[:-4].tostring())
	def set_pass_info(self, info, level):
		assert isinstance(info, MSRPCPassInfo)
		self.get_bytes()[:-4] = array.array('B', info.rawData())
	def get_return_code(self):
		return self.get_long(-4, '<')
	def set_return_code(self, code):
		self.set_long(-4, code, '<')
	def get_context_handle(self):
		return self.get_bytes().tolist()[:12]
	def get_header_size(self):
		var_size = len(self.get_bytes()) - SAMRRespLookupPassPolicy.__SIZE
		assert var_size > 0
		return SAMRRespLookupPassPolicy.__SIZE + var_size
class DCERPCSamr:
	__metaclass__=ExtendInplace
		
	def enumPass(self,context_handle): # needs to make 3 requests to get all pass policy
		enumpas = SAMREnumDomainsPass()
		enumpas.set_context_handle(context_handle)
		enumpas.set_level(1)
		self._dcerpc.send(enumpas)
		data = self._dcerpc.recv()
		retVal = SAMRRespLookupPassPolicy(data)
		pspol = retVal.get_pass_info()
		enumpas = SAMREnumDomainsPass()
		enumpas.set_context_handle(context_handle)
		enumpas.set_level(3)
		self._dcerpc.send(enumpas)
		data = self._dcerpc.recv()
		pspol.set_header(data,3)
		enumpas = SAMREnumDomainsPass()
		enumpas.set_context_handle(context_handle)
		enumpas.set_level(7)
		self._dcerpc.send(enumpas)
		data = self._dcerpc.recv()
		pspol.set_header(data,7)
		enumpas = SAMREnumDomainsPass()
		enumpas.set_context_handle(context_handle)
		enumpas.set_level(12)
		self._dcerpc.send(enumpas)
		data = self._dcerpc.recv()
		pspol.set_header(data,12)
		#return retVal
		return pspol 
	
	def opendomain(self,context_handle,domain_sid):
		opendom = SAMROpenDomainHeader()
		opendom.set_access_mask(0x305)
		opendom.set_context_handle(context_handle)
		opendom.set_domain_sid(domain_sid)
		self._dcerpc.send(opendom)
		data = self._dcerpc.recv()
		retVal = SAMRRespOpenDomainHeader(data)
		return retVal
class ListUsersException(Exception):
	pass
class SAMRDump:
	KNOWN_PROTOCOLS = {
		'139/SMB': (r'ncacn_np:%s[\pipe\samr]', 139),
		'445/SMB': (r'ncacn_np:%s[\pipe\samr]', 445),
		}
	def __init__(self, protocols = None,
				 username = '', password = ''):
		if not protocols:
			protocols = SAMRDump.KNOWN_PROTOCOLS.keys()
		self.__username = username
		self.__password = password
		self.__protocols = protocols
	def dump(self, addr):
		"""Dumps the list of users and shares registered present at
		addr. Addr is a valid host name or IP address.
		"""
		encoding = sys.getdefaultencoding()
		print
		if (self.__username and self.__password):
			print '[+] Attaching to ' + addr + ' using ' + self.__username + ":" + self.__password
		elif (self.__username):
			print '[+] Attaching to ' + addr + ' using ' + self.__username
		else:
			print '[+] Attaching to ' + addr + ' using a NULL share'
		# Try all requested protocols until one works.
		entries = []
		for protocol in self.__protocols:
			try:
				protodef = SAMRDump.KNOWN_PROTOCOLS[protocol]
				port = protodef[1]
			except KeyError,e:
				print "\n\t[!] Invalid Protocol \'%s\'\n" % protocol
				usage()
				sys.exit(1)
			print "\n\t[+] Trying protocol %s..." % protocol
			rpctransport = transport.SMBTransport(addr, port, r'\samr', self.__username, self.__password)
			try:
				entries = self.__fetchList(rpctransport)
			except Exception, e:
				print '\n\t[!] Protocol failed: %s' % e
				#raise
			else:
				# Got a response. No need for further iterations.
				break
	def __fetchList(self, rpctransport):
		dce = dcerpc.DCERPC_v5(rpctransport)
		#dce.set_auth_level(2)
		encoding = sys.getdefaultencoding()
		entries = []
		try:
			dce.connect()
			#sys.exit()
			dce.bind(samr.MSRPC_UUID_SAMR)
			#sys.exit()
			rpcsamr = samr.DCERPCSamr(dce)
			resp = rpcsamr.connect()
			if resp.get_return_code() != 0:
				raise ListUsersException, 'Connect error'
			_context_handle = resp.get_context_handle()
			resp = rpcsamr.enumdomains(_context_handle)
			if resp.get_return_code() != 0:
				raise ListUsersException, 'EnumDomain error'
			domains = resp.get_domains().elements()
			print '\n[+] Found domain(s):\n'
			for i in range(0, resp.get_entries_num()):
				print "\t[+] %s" % domains[i].get_name()
			print "\n[+] Password Info for Domain: %s" % domains[0].get_name()
			resp = rpcsamr.lookupdomain(_context_handle, domains[0])
			if resp.get_return_code() != 0:
				raise ListUsersException, 'LookupDomain error'
			resp = rpcsamr.opendomain(_context_handle, resp.get_domain_sid())
			if resp.get_return_code() != 0:
				raise ListUsersException, 'OpenDomain error'
			domain_context_handle = resp.get_context_handle()
			resp = rpcsamr.enumPass(domain_context_handle)
			resp.print_friendly()
		except ListUsersException, e:
			print "Error Getting Password Policy: %s" % e
			dce.disconnect()
		return entries
__doc__ = '\n  polenum ' + version + ' - (C) 2008 deanx\n\n' 
__doc__ += '\t\t\t RID[at]Portcullis-Security.com\n\n' 
__doc__ += '  Usage:' + sys.argv[0] + ' [username[:password]@]<address> [protocol list...]'
__doc__ += '\n\n\t\tAvailable protocols: ' + str(SAMRDump.KNOWN_PROTOCOLS.keys()) + '\n'
# Process command-line arguments.
if __name__ == '__main__':
	if len(sys.argv) <= 1:
		usage()
		sys.exit(1)
	import re
	username, password, address = re.compile('(?:([^@:]*)(?::([^@]*))?@)?(.*)').match(sys.argv[1]).groups('')
	if len(sys.argv) > 2:
		dumper = SAMRDump(sys.argv[2:], username, password)
	else:
		dumper = SAMRDump(username = username, password = password)
	try:
		dumper.dump(address)
		print
	except KeyboardInterrupt:
		print
		print "\n\t[!] Ctrl-C Caught, ByeBye\n"
		sys.exit(2) | ||||
| 
	 Actually, a much more decent polenum can be found here: https://github.com/Wh1t3Fox/polenum Notably, this version is more regularly maintained and also supports the latest impacket dcerpc v5 library.  | 
|
| 
	 fixed in new version 1.4-0kali1  | 
|
| Date Modified | Username | Field | Change | 
|---|---|---|---|
| 2017-01-06 18:47 | TheNaterz | New Issue | |
| 2017-01-06 18:47 | TheNaterz | File Added: polenum.py | |
| 2017-01-13 18:35 | TheNaterz | Note Added: 0006245 | |
| 2017-12-05 13:36 | sbrun | Note Added: 0007664 | |
| 2017-12-05 13:37 | sbrun | Status | new => resolved | 
| 2017-12-05 13:37 | sbrun | Resolution | open => fixed | 
| 2017-12-05 13:37 | sbrun | Fixed in Version | => 2018.1 |