View Issue Details

IDProjectCategoryView StatusLast Update
0003821Kali LinuxKali Package Bugpublic2017-12-05 13:37
ReporterTheNaterz Assigned To 
PrioritynormalSeveritytweakReproducibilityalways
Status resolvedResolutionfixed 
Product Version2016.2 
Fixed in Version2018.1 
Summary0003821: 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:

  1. On a Windows host, set either 'Account lockout duration' or 'Reset account lockout counter after' to 60 minutes. (gpedit.msc > Computer Configuration > Windows Settings > Security Settings > Account Policies > Account Lockout Policy). An 'Account lockout threshold' will need to be set to change these values.

  2. Using enum4linux, and with the current polenum-0.2 in PATH, run the following command: ./enum4linux -u windows_username -p windows_password -P windows_host_ip

  3. Either 'Locked Account Duration' or 'Reset Account Lockout Counter' will report 0 hour instead of 1 hour.

2nd issue:

  1. On a Windows host, set either 'Account lockout duration' or 'Reset account lockout counter after' to 61 minutes.

  2. Using enum4linux, and with the current polenum-0.2 in PATH, run the following command: ./enum4linux -u windows_username -p windows_password -P windows_host_ip

  3. Either 'Locked Account Duration' or 'Reset Account Lockout Counter' will report 53 minutes instead of 1 hour 1 minute.

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)
polenum.py (12,053 bytes)   

Activities

TheNaterz

TheNaterz

2017-01-13 18:35

reporter   ~0006245

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.

sbrun

sbrun

2017-12-05 13:36

manager   ~0007664

fixed in new version 1.4-0kali1

Issue History

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