Skip to content

Commit e07a426

Browse files
authored
Merge pull request #96 from TahiTi/master
added machineAccountQuota.py
2 parents 3277816 + 0138ae4 commit e07a426

File tree

1 file changed

+340
-0
lines changed

1 file changed

+340
-0
lines changed

examples/machineAccountQuota.py

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
#!/usr/bin/env python3
2+
#Impacket - Collection of Python classes for working with network protocols.
3+
#
4+
# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved.
5+
6+
# Description:
7+
# This module will try to get the Machine Account Quota from the domain attribute ms-DS-MachineAccountQuota.
8+
# If the value is superior to 0, it tries to list any computer object created by a user and returns the machine
9+
# name and its creator sAMAccountName and SID.
10+
#
11+
# Author:
12+
# TahiTi
13+
#
14+
15+
import argparse
16+
import logging
17+
import sys
18+
import ldap3
19+
import ssl
20+
import traceback
21+
from binascii import unhexlify
22+
23+
from ldap3.protocol.formatters.formatters import format_sid
24+
import ldapdomaindump
25+
from impacket import version
26+
from impacket.examples import logger, utils
27+
from impacket.examples.utils import parse_credentials
28+
from impacket.ldap import ldap, ldapasn1
29+
from impacket.smbconnection import SMBConnection
30+
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
31+
32+
class GetMachineAccountQuota:
33+
def __init__(self, ldap_server, ldap_session, args):
34+
self.ldap_server = ldap_server
35+
self.ldap_session = ldap_session
36+
37+
logging.debug('Initializing domainDumper()')
38+
cnf = ldapdomaindump.domainDumpConfig()
39+
cnf.basepath = None
40+
self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf)
41+
42+
def machineAccountQuota(self, maq):
43+
try:
44+
self.ldap_session.search(self.domain_dumper.root, '(objectClass=*)', attributes=['mS-DS-MachineAccountQuota'])
45+
maq = self.ldap_session.entries[0]['mS-DS-MachineAccountQuota'].values[0]
46+
logging.info('MachineAccountQuota: %s' % maq)
47+
return maq
48+
except ldap.LDAPSearchError:
49+
raise
50+
51+
def maqUsers(self):
52+
self.ldap_session.search(self.domain_dumper.root, '(&(objectCategory=computer)(mS-DS-CreatorSID=*))', attributes=['mS-DS-CreatorSID'])
53+
logging.info("Retrieving non privileged domain users that added a machine account...")
54+
users_sid = []
55+
if len(self.ldap_session.entries) != 0:
56+
for entry in self.ldap_session.entries:
57+
user_sid = format_sid(entry['mS-DS-CreatorSID'].values[0])
58+
self.ldap_session.search(self.domain_dumper.root, '(objectSID=%s)' % user_sid, attributes=['objectSID', 'sAMAccountName'])
59+
if user_sid in users_sid:
60+
continue
61+
else:
62+
users_sid.append(user_sid)
63+
logging.info('sAMAccountName : %s' % self.ldap_session.entries[0]['sAMAccountName'].values[0])
64+
logging.info('User SID : %s ' % user_sid)
65+
else:
66+
logging.info("No non-privileged user added a computer to the domain.")
67+
68+
def parse_args():
69+
parser = argparse.ArgumentParser(add_help=True, description='Retrieve the machine account quota value from the domain.')
70+
71+
parser.add_argument('identity', action='store', help='domain/username[:password]')
72+
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
73+
parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP')
74+
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
75+
76+
group = parser.add_argument_group('authentication')
77+
78+
group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH')
79+
group.add_argument('-no-pass', action='store_true', help='don\'t ask for password (useful for -k)')
80+
group.add_argument('-k', action='store_true',
81+
help='Use Kerberos authentication. Grabs credentials from ccache file '
82+
'(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the '
83+
'ones specified in the command line')
84+
group.add_argument('-aesKey', action='store', metavar='hex key', help='AES key to use for Kerberos Authentication '
85+
'(128 or 256 bits)')
86+
group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If '
87+
'omitted it use the domain part (FQDN) specified in the target parameter')
88+
89+
if len(sys.argv) == 1:
90+
parser.print_help()
91+
sys.exit(1)
92+
93+
return parser.parse_args()
94+
95+
def parse_identity(args):
96+
domain, username, password = utils.parse_credentials(args.identity)
97+
98+
if domain == '':
99+
logging.critical('Domain should be specified!')
100+
sys.exit(1)
101+
102+
if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None:
103+
from getpass import getpass
104+
logging.info("No credentials supplied, supply password")
105+
password = getpass("Password:")
106+
107+
if args.aesKey is not None:
108+
args.k = True
109+
110+
if args.hashes is not None:
111+
lmhash, nthash = args.hashes.split(':')
112+
else:
113+
lmhash = ''
114+
nthash = ''
115+
116+
return domain, username, password, lmhash, nthash
117+
118+
def init_logger(args):
119+
#Init the example's logger theme and debug level
120+
logger.init(args.ts)
121+
if args.debug is True:
122+
logging.getLogger().setLevel(logging.DEBUG)
123+
# Print the Library's installation path
124+
logging.debug(version.getInstallationPath())
125+
else:
126+
logging.getLogger().setLevel(logging.INFO)
127+
logging.getLogger('impacket.smbserver').setLevel(logging.ERROR)
128+
129+
def get_machine_name(args, domain):
130+
if args.dc_ip is not None:
131+
s = SMBConnection(args.dc_ip, args.dc_ip)
132+
else:
133+
s = SMBConnection(domain, domain)
134+
try:
135+
s.login('', '')
136+
except Exception:
137+
if s.getServerName() == '':
138+
raise Exception('Error while anonymous logging into %s' % domain)
139+
else:
140+
s.logoff()
141+
return s.getServerName()
142+
143+
def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None,
144+
TGT=None, TGS=None, useCache=True):
145+
from pyasn1.codec.ber import encoder, decoder
146+
from pyasn1.type.univ import noValue
147+
"""
148+
logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
149+
:param string user: username
150+
:param string password: password for the user
151+
:param string domain: domain where the account is valid for (required)
152+
:param string lmhash: LMHASH used to authenticate using hashes (password is not used)
153+
:param string nthash: NTHASH used to authenticate using hashes (password is not used)
154+
:param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
155+
:param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
156+
:param struct TGT: If there's a TGT available, send the structure here and it will be used
157+
:param struct TGS: same for TGS. See smb3.py for the format
158+
:param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
159+
:return: True, raises an Exception if error.
160+
"""
161+
162+
if lmhash != '' or nthash != '':
163+
if len(lmhash) % 2:
164+
lmhash = '0' + lmhash
165+
if len(nthash) % 2:
166+
nthash = '0' + nthash
167+
try: # just in case they were converted already
168+
lmhash = unhexlify(lmhash)
169+
nthash = unhexlify(nthash)
170+
except TypeError:
171+
pass
172+
173+
# Importing down here so pyasn1 is not required if kerberos is not used.
174+
from impacket.krb5.ccache import CCache
175+
from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
176+
from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
177+
from impacket.krb5 import constants
178+
from impacket.krb5.types import Principal, KerberosTime, Ticket
179+
import datetime
180+
181+
if TGT is not None or TGS is not None:
182+
useCache = False
183+
184+
target = 'ldap/%s' % target
185+
if useCache:
186+
logging.info('dans la co kerberos la target est : %s' % target)
187+
domain, user, TGT, TGS = CCache.parseFile(domain, user, target)
188+
189+
# First of all, we need to get a TGT for the user
190+
userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
191+
if TGT is None:
192+
if TGS is None:
193+
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,
194+
aesKey, kdcHost)
195+
else:
196+
tgt = TGT['KDC_REP']
197+
cipher = TGT['cipher']
198+
sessionKey = TGT['sessionKey']
199+
200+
if TGS is None:
201+
serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value)
202+
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,
203+
sessionKey)
204+
else:
205+
tgs = TGS['KDC_REP']
206+
cipher = TGS['cipher']
207+
sessionKey = TGS['sessionKey']
208+
209+
# Let's build a NegTokenInit with a Kerberos REQ_AP
210+
211+
blob = SPNEGO_NegTokenInit()
212+
213+
# Kerberos
214+
blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
215+
216+
# Let's extract the ticket from the TGS
217+
tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
218+
ticket = Ticket()
219+
ticket.from_asn1(tgs['ticket'])
220+
221+
# Now let's build the AP_REQ
222+
apReq = AP_REQ()
223+
apReq['pvno'] = 5
224+
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
225+
226+
opts = []
227+
apReq['ap-options'] = constants.encodeFlags(opts)
228+
seq_set(apReq, 'ticket', ticket.to_asn1)
229+
230+
authenticator = Authenticator()
231+
authenticator['authenticator-vno'] = 5
232+
authenticator['crealm'] = domain
233+
seq_set(authenticator, 'cname', userName.components_to_asn1)
234+
now = datetime.datetime.utcnow()
235+
236+
authenticator['cusec'] = now.microsecond
237+
authenticator['ctime'] = KerberosTime.to_asn1(now)
238+
239+
encodedAuthenticator = encoder.encode(authenticator)
240+
241+
# Key Usage 11
242+
# AP-REQ Authenticator (includes application authenticator
243+
# subkey), encrypted with the application session key
244+
# (Section 5.5.1)
245+
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
246+
247+
apReq['authenticator'] = noValue
248+
apReq['authenticator']['etype'] = cipher.enctype
249+
apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
250+
251+
blob['MechToken'] = encoder.encode(apReq)
252+
253+
request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO',
254+
blob.getData())
255+
256+
# Done with the Kerberos saga, now let's get into LDAP
257+
if connection.closed: # try to open connection if closed
258+
connection.open(read_server_info=False)
259+
260+
connection.sasl_in_progress = True
261+
response = connection.post_send_single_response(connection.send('bindRequest', request, None))
262+
connection.sasl_in_progress = False
263+
if response[0]['result'] != 0:
264+
raise Exception(response)
265+
266+
connection.bound = True
267+
268+
return True
269+
270+
def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash):
271+
user = '%s\\%s' % (domain, username)
272+
connect_to = target
273+
if args.dc_ip is not None:
274+
connect_to = args.dc_ip
275+
if tls_version is not None:
276+
use_ssl = True
277+
port = 636
278+
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version)
279+
else:
280+
use_ssl = False
281+
port = 389
282+
tls = None
283+
ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls)
284+
if args.k:
285+
ldap_session = ldap3.Connection(ldap_server)
286+
ldap_session.bind()
287+
ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip)
288+
elif args.hashes is not None:
289+
ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True)
290+
else:
291+
ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True)
292+
293+
return ldap_server, ldap_session
294+
295+
def init_ldap_session(args, domain, username, password, lmhash, nthash):
296+
if args.k:
297+
target = get_machine_name(args, domain)
298+
else:
299+
if args.dc_ip is not None:
300+
target = args.dc_ip
301+
else:
302+
target = domain
303+
304+
if args.use_ldaps is True:
305+
try:
306+
return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash)
307+
except ldap3.core.exceptions.LDAPSocketOpenError:
308+
return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash)
309+
else:
310+
return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash)
311+
312+
def main():
313+
print(version.BANNER)
314+
args = parse_args()
315+
init_logger(args)
316+
317+
if args.debug is True:
318+
logging.getLogger().setLevel(logging.DEBUG)
319+
# Print the Library's installation path
320+
logging.debug(version.getInstallationPath())
321+
else:
322+
logging.getLogger().setLevel(logging.INFO)
323+
324+
domain, username, password, lmhash, nthash = parse_identity(args)
325+
machine_account_quota = 0
326+
327+
try:
328+
ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash)
329+
execute = GetMachineAccountQuota(ldap_server, ldap_session, args)
330+
331+
if execute.machineAccountQuota(machine_account_quota) != 0:
332+
execute.maqUsers()
333+
334+
except Exception as e:
335+
if logging.getLogger().level == logging.DEBUG:
336+
traceback.print_exc()
337+
logging.error(str(e))
338+
339+
if __name__ == '__main__':
340+
main()

0 commit comments

Comments
 (0)