Merge branch 'master' into master
commit
8e09a273d2
|
@ -3,15 +3,16 @@
|
||||||
# from https://github.com/SecureAuthCorp/impacket/blob/master/examples/GetNPUsers.py
|
# from https://github.com/SecureAuthCorp/impacket/blob/master/examples/GetNPUsers.py
|
||||||
# https://troopers.de/downloads/troopers19/TROOPERS19_AD_Fun_With_LDAP.pdf
|
# https://troopers.de/downloads/troopers19/TROOPERS19_AD_Fun_With_LDAP.pdf
|
||||||
|
|
||||||
import requests
|
|
||||||
import logging
|
import logging
|
||||||
import configparser
|
from argparse import _StoreTrueAction
|
||||||
from binascii import b2a_hex, unhexlify, hexlify
|
from binascii import b2a_hex, unhexlify, hexlify
|
||||||
from cme.connection import *
|
from cme.connection import *
|
||||||
from cme.helpers.logger import highlight
|
from cme.helpers.logger import highlight
|
||||||
from cme.logger import CMEAdapter
|
from cme.logger import CMEAdapter
|
||||||
from cme.helpers.bloodhound import add_user_bh
|
from cme.helpers.bloodhound import add_user_bh
|
||||||
|
from cme.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||||
from cme.protocols.ldap.kerberos import KerberosAttacks
|
from cme.protocols.ldap.kerberos import KerberosAttacks
|
||||||
|
from Cryptodome.Hash import MD4
|
||||||
from impacket.smbconnection import SMBConnection, SessionError
|
from impacket.smbconnection import SMBConnection, SessionError
|
||||||
from impacket.smb import SMB_DIALECT
|
from impacket.smb import SMB_DIALECT
|
||||||
from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION
|
from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION
|
||||||
|
@ -20,11 +21,10 @@ from impacket.krb5.types import KerberosTime, Principal
|
||||||
from impacket.ldap import ldap as ldap_impacket
|
from impacket.ldap import ldap as ldap_impacket
|
||||||
from impacket.krb5 import constants
|
from impacket.krb5 import constants
|
||||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||||
import argparse
|
from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pywerview.cli.helpers import *
|
from pywerview.cli.helpers import *
|
||||||
from pywerview.requester import RPCRequester
|
from re import sub, I
|
||||||
import re
|
|
||||||
|
|
||||||
ldap_error_status = {
|
ldap_error_status = {
|
||||||
"533":"STATUS_ACCOUNT_DISABLED",
|
"533":"STATUS_ACCOUNT_DISABLED",
|
||||||
|
@ -82,7 +82,7 @@ class ldap(connection):
|
||||||
ldap_parser.add_argument("--no-bruteforce", action='store_true', help='No spray when using file for username and password (user1 => password1, user2 => password2')
|
ldap_parser.add_argument("--no-bruteforce", action='store_true', help='No spray when using file for username and password (user1 => password1, user2 => password2')
|
||||||
ldap_parser.add_argument("--continue-on-success", action='store_true', help="continues authentication attempts even after successes")
|
ldap_parser.add_argument("--continue-on-success", action='store_true', help="continues authentication attempts even after successes")
|
||||||
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
|
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
|
||||||
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(argparse._StoreTrueAction), make_required=[], help='No smb connection')
|
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help='No smb connection')
|
||||||
|
|
||||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||||
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, default=None, help="domain to authenticate to")
|
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, default=None, help="domain to authenticate to")
|
||||||
|
@ -99,6 +99,7 @@ class ldap(connection):
|
||||||
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
||||||
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
|
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
|
||||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||||
|
vgroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@ -125,7 +126,7 @@ class ldap(connection):
|
||||||
for attribute in item['attributes']:
|
for attribute in item['attributes']:
|
||||||
if str(attribute['type']) == 'defaultNamingContext':
|
if str(attribute['type']) == 'defaultNamingContext':
|
||||||
baseDN = str(attribute['vals'][0])
|
baseDN = str(attribute['vals'][0])
|
||||||
targetDomain = re.sub(',DC=', '.', baseDN[baseDN.lower().find('dc='):], flags=re.I)[3:]
|
targetDomain = sub(',DC=', '.', baseDN[baseDN.lower().find('dc='):], flags=I)[3:]
|
||||||
if str(attribute['type']) == 'dnsHostName':
|
if str(attribute['type']) == 'dnsHostName':
|
||||||
target = str(attribute['vals'][0])
|
target = str(attribute['vals'][0])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -217,7 +218,6 @@ class ldap(connection):
|
||||||
|
|
||||||
#Re-connect since we logged off
|
#Re-connect since we logged off
|
||||||
self.create_conn_obj()
|
self.create_conn_obj()
|
||||||
|
|
||||||
|
|
||||||
def print_host_info(self):
|
def print_host_info(self):
|
||||||
if self.args.no_smb:
|
if self.args.no_smb:
|
||||||
|
@ -239,8 +239,6 @@ class ldap(connection):
|
||||||
|
|
||||||
def kerberos_login(self, domain, username, password = '', ntlm_hash = '', aesKey = '', kdcHost = '', useCache = False):
|
def kerberos_login(self, domain, username, password = '', ntlm_hash = '', aesKey = '', kdcHost = '', useCache = False):
|
||||||
logging.getLogger("impacket").disabled = True
|
logging.getLogger("impacket").disabled = True
|
||||||
self.logger.extra['protocol'] = "LDAP"
|
|
||||||
self.logger.extra['port'] = "389"
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
@ -262,7 +260,8 @@ class ldap(connection):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Connect to LDAP
|
# Connect to LDAP
|
||||||
self.ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN)
|
proto = "ldaps" if self.args.gmsa else "ldap"
|
||||||
|
self.ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % target, self.baseDN)
|
||||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash,
|
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash,
|
||||||
aesKey, kdcHost=kdcHost, useCache=useCache)
|
aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||||
|
|
||||||
|
@ -276,6 +275,8 @@ class ldap(connection):
|
||||||
self.username,
|
self.username,
|
||||||
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
||||||
|
|
||||||
|
self.logger.extra['protocol'] = "LDAP"
|
||||||
|
self.logger.extra['port'] = "389" if not self.args.gmsa else "636"
|
||||||
self.logger.success(out)
|
self.logger.success(out)
|
||||||
|
|
||||||
if not self.args.local_auth:
|
if not self.args.local_auth:
|
||||||
|
@ -330,8 +331,6 @@ class ldap(connection):
|
||||||
color='magenta' if errorCode in ldap_error_status else 'red')
|
color='magenta' if errorCode in ldap_error_status else 'red')
|
||||||
|
|
||||||
def plaintext_login(self, domain, username, password):
|
def plaintext_login(self, domain, username, password):
|
||||||
self.logger.extra['protocol'] = "LDAP"
|
|
||||||
self.logger.extra['port'] = "389"
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
@ -349,7 +348,8 @@ class ldap(connection):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Connect to LDAP
|
# Connect to LDAP
|
||||||
self.ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN)
|
proto = "ldaps" if self.args.gmsa else "ldap"
|
||||||
|
self.ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % target, self.baseDN)
|
||||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||||
self.check_if_admin()
|
self.check_if_admin()
|
||||||
|
|
||||||
|
@ -358,6 +358,9 @@ class ldap(connection):
|
||||||
self.username,
|
self.username,
|
||||||
self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
||||||
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
||||||
|
|
||||||
|
self.logger.extra['protocol'] = "LDAP"
|
||||||
|
self.logger.extra['port'] = "389" if not self.args.gmsa else "636"
|
||||||
self.logger.success(out)
|
self.logger.success(out)
|
||||||
|
|
||||||
if not self.args.local_auth:
|
if not self.args.local_auth:
|
||||||
|
@ -443,7 +446,8 @@ class ldap(connection):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Connect to LDAP
|
# Connect to LDAP
|
||||||
self.ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % target, self.baseDN)
|
proto = "ldaps" if self.args.gmsa else "ldap"
|
||||||
|
self.ldapConnection = ldap_impacket.LDAPConnection(proto + '://%s' % target, self.baseDN)
|
||||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||||
self.check_if_admin()
|
self.check_if_admin()
|
||||||
|
|
||||||
|
@ -453,7 +457,7 @@ class ldap(connection):
|
||||||
self.nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
self.nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
||||||
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
||||||
self.logger.extra['protocol'] = "LDAP"
|
self.logger.extra['protocol'] = "LDAP"
|
||||||
self.logger.extra['port'] = "389"
|
self.logger.extra['port'] = "389" if not self.args.gmsa else "636"
|
||||||
self.logger.success(out)
|
self.logger.success(out)
|
||||||
|
|
||||||
if not self.args.local_auth:
|
if not self.args.local_auth:
|
||||||
|
@ -991,8 +995,34 @@ class ldap(connection):
|
||||||
self.logger.highlight(value[0])
|
self.logger.highlight(value[0])
|
||||||
else:
|
else:
|
||||||
self.logger.error("No entries found!")
|
self.logger.error("No entries found!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def gmsa(self):
|
||||||
|
self.logger.info("Getting GMSA Passwords")
|
||||||
|
search_filter = '(objectClass=msDS-GroupManagedServiceAccount)'
|
||||||
|
gmsa_accounts = self.ldapConnection.search(searchFilter=search_filter,
|
||||||
|
attributes=['sAMAccountName', 'msDS-ManagedPassword','msDS-GroupMSAMembership'],
|
||||||
|
sizeLimit=0,
|
||||||
|
searchBase=self.baseDN)
|
||||||
|
if gmsa_accounts:
|
||||||
|
answers = []
|
||||||
|
logging.debug('Total of records returned %d' % len(gmsa_accounts))
|
||||||
|
|
||||||
|
for item in gmsa_accounts:
|
||||||
|
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||||
|
continue
|
||||||
|
sAMAccountName = ''
|
||||||
|
managedPassword = ''
|
||||||
|
for attribute in item['attributes']:
|
||||||
|
if str(attribute['type']) == 'sAMAccountName':
|
||||||
|
sAMAccountName = str(attribute['vals'][0])
|
||||||
|
if str(attribute['type']) == 'msDS-ManagedPassword':
|
||||||
|
data = attribute['vals'][0].asOctets()
|
||||||
|
blob = MSDS_MANAGEDPASSWORD_BLOB()
|
||||||
|
blob.fromString(data)
|
||||||
|
currentPassword = blob['CurrentPassword'][:-2]
|
||||||
|
ntlm_hash = MD4.new ()
|
||||||
|
ntlm_hash.update (currentPassword)
|
||||||
|
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
|
||||||
|
self.logger.highlight("Account: {:<20} NTLM: {}".format(sAMAccountName, passwd))
|
||||||
|
return True
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
from impacket.structure import Structure
|
||||||
|
|
||||||
|
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']:]
|
Loading…
Reference in New Issue