├── CONTRIBUTORS ├── README.md └── ldap-scanner.py /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Rcarnus (Romain CARNUS) 2 | Techmasterz (Ali KOCA) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ldap-scanner scanner 2 | 3 | Checks for signature requirements over LDAP. 4 | The script will establish a connection to the target host(s) and request 5 | authentication without signature capability. If this is accepted, it means that the target hosts 6 | allows unsigned LDAP sessions and NTLM relay attacks are possible to this LDAP service (whenever signing is not requested by the client). 7 | 8 | # Installation 9 | 10 | ``` 11 | $ pip install impacket 12 | $ python3 ldap-scanner.py 13 | ``` 14 | 15 | 16 | # Usage 17 | ``` 18 | [*] ldap scanner by @romcar / GoSecure - Based on impacket by SecureAuth 19 | usage: ldap-scanner.py [-h] [-target-file file] 20 | [-hashes LMHASH:NTHASH] 21 | target 22 | 23 | ldap scanner - Connects over LDAP and attempts to authenticate with 24 | invalid NTLM packets. If accepted, target is vulnerable to relay attack 25 | 26 | positional arguments: 27 | target [[domain/]username[:password]@] 28 | 29 | optional arguments: 30 | -h, --help show this help message and exit 31 | 32 | connection: 33 | -target-file file Use the targets in the specified file instead of the 34 | one on the command line (you must still specify 35 | something as target name) 36 | 37 | authentication: 38 | -hashes LMHASH:NTHASH 39 | NTLM hashes, format is LMHASH:NTHASH 40 | ``` 41 | -------------------------------------------------------------------------------- /ldap-scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #################### 3 | # 4 | # Copyright (c) 2020 Romain Carnus / GoSecure (@romcar) 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # Checks remotely LDAP signature requirement options 25 | # 26 | # Author: 27 | # Romain Carnus (@romcar) 28 | # 29 | #################### 30 | import sys 31 | import logging 32 | import argparse 33 | import codecs 34 | import calendar 35 | import struct 36 | import time 37 | from impacket import version 38 | from impacket.examples.logger import ImpacketFormatter 39 | from impacket import ntlm 40 | from impacket.ldap import ldap 41 | from impacket.ntlm import NTLMAuthNegotiate,AV_PAIRS, NTLMSSP_AV_TIME, NTLMSSP_AV_FLAGS, NTOWFv2, NTLMSSP_AV_TARGET_NAME, NTLMSSP_AV_HOSTNAME,USE_NTLMv2, hmac_md5 42 | 43 | class checker(object): 44 | def __init__(self, username='', password='', domain='', port=None, 45 | hashes=None): 46 | 47 | self.__username = username 48 | self.__password = password 49 | self.__port = port #not used for now 50 | self.__domain = domain 51 | self.__lmhash = '' 52 | self.__nthash = '' 53 | if hashes is not None: 54 | self.__lmhash, self.__nthash = hashes.split(':') 55 | 56 | 57 | def check(self, remote_host): 58 | try: 59 | ldapclient = ldap.LDAPConnection('ldap://%s' % remote_host) 60 | except: 61 | return 62 | 63 | try: 64 | #Default login method does not request for signature, allowing us to check auth result 65 | ldapclient.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 66 | logging.info('LDAP signature not required on target %s (authentication was accepted)', remote_host) 67 | except ldap.LDAPSessionError as exc: 68 | if 'strongerAuthRequired:' in str(exc): 69 | logging.info('LDAP signature was required on target %s (authentication was rejected)', remote_host) 70 | else: 71 | logging.warning('Unexpected Exception while authenticating to %s: %s', remote_host, exc) 72 | 73 | ldapclient.close() 74 | 75 | 76 | # Process command-line arguments. 77 | def main(): 78 | # Init the example's logger theme 79 | handler = logging.StreamHandler(sys.stderr) 80 | handler.setFormatter(ImpacketFormatter()) 81 | logging.getLogger().addHandler(handler) 82 | logging.getLogger().setLevel(logging.INFO) 83 | 84 | # Explicitly changing the stdout encoding format 85 | if sys.stdout.encoding is None: 86 | # Output is redirected to a file 87 | sys.stdout = codecs.getwriter('utf8')(sys.stdout) 88 | 89 | logging.info('LDAP security scanner by @romcar / GoSecure - Based on impacket by SecureAuth') 90 | 91 | parser = argparse.ArgumentParser(description="LDAP scanner - Connects over LDAP, attempts to authenticate without signing capabilities.") 92 | 93 | parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') 94 | 95 | group = parser.add_argument_group('connection') 96 | 97 | group.add_argument('-target-file', 98 | action='store', 99 | metavar="file", 100 | help='Use the targets in the specified file instead of the one on'\ 101 | ' the command line (you must still specify something as target name)') 102 | 103 | """ 104 | #Not supported for now as impacket's ldap client does not allow to specify the port number 105 | group.add_argument('-port', choices=['389'], nargs='?', default='389', metavar="destination port", 106 | help='Destination port to connect to LDAP Server') 107 | """ 108 | 109 | group = parser.add_argument_group('authentication') 110 | 111 | group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 112 | 113 | if len(sys.argv)==1: 114 | parser.print_help() 115 | sys.exit(1) 116 | 117 | options = parser.parse_args() 118 | 119 | import re 120 | 121 | domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( 122 | options.target).groups('') 123 | 124 | #In case the password contains '@' 125 | if '@' in remote_name: 126 | password = password + '@' + remote_name.rpartition('@')[0] 127 | remote_name = remote_name.rpartition('@')[2] 128 | 129 | if domain is None: 130 | domain = '' 131 | 132 | if password == '' and username == '': 133 | logging.error("Please supply a username/password (you can't use this scanner with anonymous authentication)") 134 | return 135 | 136 | if password == '' and username != '' and options.hashes is None: 137 | from getpass import getpass 138 | password = getpass("Password:") 139 | 140 | remote_names = [] 141 | if options.target_file is not None: 142 | with open(options.target_file, 'r') as inf: 143 | for line in inf: 144 | remote_names.append(line.strip()) 145 | else: 146 | remote_names.append(remote_name) 147 | 148 | lookup = checker(username, password, domain, 389, options.hashes) 149 | for remote_name in remote_names: 150 | try: 151 | lookup.check(remote_name) 152 | except KeyboardInterrupt: 153 | break 154 | 155 | 156 | if __name__ == '__main__': 157 | main() 158 | --------------------------------------------------------------------------------