├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pythonpublish.yml ├── .gitignore ├── LICENSE ├── README.md ├── paths.txt ├── requirements.pip ├── setup.py └── src └── ntlmrecon ├── __init__.py ├── inpututils.py ├── misc.py └── ntlmutil.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Kali Linux] 28 | - Version [e.g. 22] 29 | 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python setup.py sdist bdist_wheel 26 | twine upload dist/* 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Pycharm stuff 132 | .idea/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sachin S. Kamath (@sachinkamath) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](http://makeapullrequest.com) 2 | [](https://opensource.org/licenses/MIT) 3 | 4 | # NTLMRecon 5 | 6 | An NTLM reconnaissance tool without external dependencies. Useful to find out information about NTLM endpoints when working with a large set of potential IP addresses and domains. 7 | 8 | 9 | NTLMRecon is built with flexibilty in mind. Need to run recon on a single URL, an IP address, an entire CIDR range or combination of all of it all put in a single input file? No problem! NTLMRecon got you covered. Read on. 10 | 11 | # Overview 12 | 13 | NTLMRecon looks for NTLM enabled web endpoints, sends a fake authentication request and enumerates the following information from the NTLMSSP response: 14 | 15 | 1. AD Domain Name 16 | 2. Server name 17 | 3. DNS Domain Name 18 | 4. FQDN 19 | 5. Parent DNS Domain 20 | 21 | Since NTLMRecon leverages a python implementation of NTLMSSP, it eliminates the overhead of running Nmap NSE `http-ntlm-info` for every successful discovery. 22 | 23 | On every successful discovery of a NTLM enabled web endpoint, the tool enumerates and saves information about the domain as follows to a CSV file : 24 | 25 | 26 | | URL | Domain Name | Server Name | DNS Domain Name | FQDN | DNS Domain | 27 | |-------------------------- |------------- |------------- |------------------- |------------------------------ |------------- | 28 | | https://contoso.com/EWS/ | XCORP | EXCHANGE01 | xcorp.contoso.net | EXCHANGE01.xcorp.contoso.net | contoso.net | 29 | 30 | # Installation 31 | 32 | ### BlackArch 33 | 34 | NTLMRecon is already packaged for BlackArch and can be installed by running `pacman -S ntlmrecon` 35 | 36 | ### Build from source 37 | 38 | 1. Clone the repository : `git clone https://github.com/pwnfoo/ntlmrecon/` 39 | 2. RECOMMENDED - Install virtualenv : `pip install virtualenv` 40 | 3. Start a new virtual environment : `virtualenv venv` and activate it with `source venv/bin/activate` 41 | 4. Run the setup file : `python setup.py install` 42 | 5. Run ntlmrecon : `ntlmrecon --help` 43 | 44 | ## Example Usage 45 | 46 | ### Recon on a single URL 47 | 48 | ` $ ntlmrecon --input https://mail.contoso.com --outfile ntlmrecon.csv` 49 | 50 | ### Recon on a CIDR range or IP address 51 | 52 | ` $ ntlmrecon --input 192.168.1.1/24 --outfile ntlmrecon-ranges.csv` 53 | 54 | ### Recon on an input file 55 | 56 | The tool automatically detects the type of input per line and takes actions accordingly. CIDR ranges are expanded by default (please note that there is no de-duplication baked in just yet!) 57 | 58 | 59 | P.S Handles a good mix like this well : 60 | 61 |
62 | mail.contoso.com 63 | CONTOSOHOSTNAME 64 | 10.0.13.2/28 65 | 192.168.222.1/24 66 | https://mail.contoso.com 67 |68 | 69 | # TODO 70 | 71 | 1. Implement aiohttp based solution for sending requests 72 | 2. Integrate a spraying library 73 | 3. Add other authentication schemes found to the output 74 | 4. Automatic detection of autodiscover domains if domain 75 | 76 | # Acknowledgements 77 | 78 | * [@nyxgeek](https://github.com/nyxgeek) for the idea behind [ntlmscan](https://github.com/nyxgeek/ntlmscan). 79 | -------------------------------------------------------------------------------- /paths.txt: -------------------------------------------------------------------------------- 1 | /abs 2 | /adfs/services/trust/2005/windowstransport 3 | /aspnet_client/ 4 | /Autodiscover 5 | /Autodiscover/AutodiscoverService.svc/root 6 | /Autodiscover/Autodiscover.xml 7 | /AutoUpdate/ 8 | /CertEnroll/ 9 | /CertProv 10 | /CertSrv/ 11 | /Conf/ 12 | /debug/ 13 | /deviceupdatefiles_ext/ 14 | /deviceupdatefiles_int/ 15 | /dialin 16 | /ecp/ 17 | /Etc/ 18 | /EWS/ 19 | /Exchange/ 20 | /Exchweb/ 21 | /GroupExpansion/ 22 | /HybridConfig 23 | /iwa/authenticated.aspx 24 | /iwa/iwa_test.aspx 25 | /mcx 26 | /meet 27 | /Microsoft-Server-ActiveSync/ 28 | /OAB/ 29 | /ocsp/ 30 | /owa/ 31 | /PersistentChat 32 | /PhoneConferencing/ 33 | /PowerShell/ 34 | /Public/ 35 | /Reach/sip.svc 36 | /RequestHandler/ 37 | /RequestHandlerExt 38 | /RequestHandlerExt/ 39 | /Rgs/ 40 | /RgsClients 41 | /Rpc/ 42 | /RpcWithCert/ 43 | /scheduler 44 | /sso 45 | /Ucwa 46 | /UnifiedMessaging/ 47 | /WebTicket 48 | /WebTicket/WebTicketService.svc 49 | /_windows/default.aspx?ReturnUrl=/ 50 | -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwnfoo/NTLMRecon/b5778a042161d0b498ed1185d003453ec6266a3d/requirements.pip -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | 10 | setup( 11 | name='ntlmrecon', # Required 12 | 13 | version='0.4b0', # Required 14 | 15 | description='A tool to enumerate information from NTLM authentication enabled web endpoints', # Optional 16 | 17 | license='MIT', 18 | 19 | long_description=long_description, # Optional 20 | 21 | long_description_content_type='text/markdown', # Optional (see note above) 22 | 23 | url='https://github.com/sachinkamath/ntlmrecon', # Optional 24 | 25 | # This should be your name or the name of the organization which owns the 26 | # project. 27 | author='Sachin S Kamath (@sachinkamath)', # Optional 28 | 29 | # This should be a valid email address corresponding to the author listed 30 | # above. 31 | author_email='mail@skamath.me', # Optional 32 | 33 | keywords='security recon redteam cybersecurity ntlm ntlmrecon', # Optional 34 | 35 | package_dir={'': 'src'}, 36 | 37 | packages=find_packages(where='src'), # Required 38 | 39 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4', 40 | 41 | install_requires=['requests', 'colorama', 'termcolor', 'iptools'], # TODO 42 | 43 | # For example, the following would provide a command called `sample` which 44 | # executes the function `main` from this package when invoked: 45 | entry_points={ # Optional 46 | 'console_scripts': [ 47 | 'ntlmrecon=ntlmrecon:main', 48 | ], 49 | }, 50 | 51 | project_urls={ # Optional 52 | 'Bug Reports': 'https://github.com/sachinkamath/ntlmrecon/issues', 53 | 'Source': 'https://github.com/sachinkamath/ntlmrecon/', 54 | }, 55 | ) 56 | 57 | -------------------------------------------------------------------------------- /src/ntlmrecon/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import requests 4 | import csv 5 | import sys 6 | import os 7 | 8 | from colorama import init as init_colorama 9 | from multiprocessing.dummy import Pool as ThreadPool 10 | from ntlmrecon.ntlmutil import gather_ntlm_info 11 | from ntlmrecon.misc import print_banner, INTERNAL_WORDLIST 12 | from ntlmrecon.inpututils import readfile_and_gen_input, read_input_and_gen_list 13 | from termcolor import colored 14 | from urllib.parse import urlsplit 15 | 16 | # Initialize colors in Windows - Because I like Windows too! 17 | init_colorama() 18 | 19 | # make the Pool of workers 20 | # TODO: Make this an argument 21 | 22 | FOUND_DOMAINS = [] 23 | 24 | 25 | def in_found_domains(url): 26 | split_url = urlsplit(url) 27 | if split_url.hostname in FOUND_DOMAINS: 28 | return True 29 | else: 30 | return False 31 | 32 | 33 | def write_records_to_csv(records, filename): 34 | if os.path.exists(filename): 35 | append_write = 'a' 36 | else: 37 | append_write = 'w+' 38 | 39 | with open(filename, append_write) as file: 40 | writer = csv.writer(file) 41 | if append_write == 'w+': 42 | writer.writerow(['URL', 'AD Domain Name', 'Server Name', 'DNS Domain Name', 'FQDN', 'Parent DNS Domain']) 43 | for record in records: 44 | csv_record = list() 45 | url = list(record.keys())[0] 46 | csv_record.append(url) 47 | csv_record.extend(list(record[url]['data'].values())) 48 | writer.writerow(csv_record) 49 | 50 | 51 | def main(): 52 | # Init arg parser 53 | parser = argparse.ArgumentParser(description=print_banner()) 54 | group = parser.add_mutually_exclusive_group() 55 | group.add_argument('--input', '-i', help='Pass input as an IP address, URL or CIDR to enumerate NTLM endpoints') 56 | group.add_argument('--infile', '-I', help='Pass input from a local file') 57 | parser.add_argument('--wordlist', help='Override the internal wordlist with a custom wordlist', required=False) 58 | parser.add_argument('--threads', help="Set number of threads (Default: 10)", required=False, default=10) 59 | parser.add_argument('--output-type', '-o', help='Set output type. JSON (TODO) and CSV supported (Default: CSV)', 60 | required=False, default='csv', action="store_true") 61 | parser.add_argument('--outfile', '-O', help='Set output file name (Default: ntlmrecon.csv)', default='ntlmrecon.csv') 62 | parser.add_argument('--random-user-agent', help="TODO: Randomize user agents when sending requests (Default: False)", 63 | default=False, action="store_true") 64 | parser.add_argument('--force-all', help="Force enumerate all endpoints even if a valid endpoint is found for a URL " 65 | "(Default : False)", default=False, action="store_true") 66 | parser.add_argument('--shuffle', help="Break order of the input files", default=False, action="store_true") 67 | parser.add_argument('-f', '--force', help="Force replace output file if it already exists", action="store_true", 68 | default=False) 69 | args = parser.parse_args() 70 | 71 | if not args.input and not args.infile: 72 | print(colored("[!] How about you check the -h flag?", "red")) 73 | 74 | if os.path.isdir(args.outfile): 75 | print(colored("[!] Invalid filename. Please enter a valid filename!", "red")) 76 | sys.exit() 77 | elif os.path.exists(args.outfile) and not args.force: 78 | print(colored("[!] Output file {} already exists. " 79 | "Choose a different file name or use -f to overwrite the file".format(args.outfile), "red")) 80 | sys.exit() 81 | 82 | pool = ThreadPool(int(args.threads)) 83 | 84 | if args.input: 85 | records = read_input_and_gen_list(args.input, shuffle=args.shuffle) 86 | elif args.infile: 87 | records = readfile_and_gen_input(args.infile, shuffle=args.shuffle) 88 | else: 89 | sys.exit(1) 90 | 91 | # Check if a custom wordlist is specified 92 | if args.wordlist: 93 | try: 94 | with open(args.wordlist, 'r') as fr: 95 | wordlist = fr.read().split('\n') 96 | wordlist = [x for x in wordlist if x] 97 | except (OSError, FileNotFoundError): 98 | print(colored("[!] Cannot read the specified file {}. Check if file exists and you have " 99 | "permission to read it".format(args.wordlist), "red")) 100 | sys.exit(1) 101 | else: 102 | wordlist = INTERNAL_WORDLIST 103 | # Identify all URLs with web servers running 104 | for record in records: 105 | print(colored("[+] Brute-forcing {} endpoints on {}".format(len(wordlist), record), "yellow")) 106 | all_combos = [] 107 | for word in wordlist: 108 | if word.startswith('/'): 109 | all_combos.append(str(record+word)) 110 | else: 111 | all_combos.append(str(record+"/"+word)) 112 | 113 | results = pool.map(gather_ntlm_info, all_combos) 114 | results = [x for x in results if x] 115 | if results: 116 | write_records_to_csv(results, args.outfile) 117 | print(colored('[+] Output for {} saved to {} '.format(record, args.outfile), 'green')) 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/ntlmrecon/inpututils.py: -------------------------------------------------------------------------------- 1 | from iptools import IpRangeList 2 | import sys 3 | import re 4 | import random 5 | 6 | 7 | CIDR_REGEX = "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$" 8 | URL_REGEX = "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$" 9 | HOST_REGEX = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" \ 10 | "([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" 11 | 12 | 13 | def _cidr_to_iplist(cidr): 14 | try: 15 | ip_range = IpRangeList(cidr) 16 | return list(ip_range) 17 | except TypeError: 18 | print("[!] That's not a valid IP address or CIDR") 19 | return False 20 | 21 | 22 | def _identify_and_return_records(inputstr, shuffle=False): 23 | master_records = [] 24 | 25 | if re.match(CIDR_REGEX, inputstr): 26 | # Get results and add https prefix to it and pass it to master records 27 | iplist = ["https://" + str(x) for x in _cidr_to_iplist(inputstr)] 28 | master_records.extend(iplist) 29 | # Keep in intact after adding http prefix for all URL_REGEX URLs 30 | elif re.match(URL_REGEX, inputstr): 31 | if inputstr.startswith("http://") or inputstr.startswith("https://"): 32 | master_records.append(inputstr) 33 | else: 34 | master_records.append("https://" + str(inputstr)) 35 | elif re.match(HOST_REGEX, inputstr): 36 | master_records.append("https://" + str(inputstr)) 37 | 38 | if shuffle: 39 | random.shuffle(master_records) 40 | return master_records 41 | else: 42 | return master_records 43 | 44 | 45 | def readfile_and_gen_input(file, shuffle=False): 46 | master_records = [] 47 | try: 48 | with open(file, 'r') as fr: 49 | lines = fr.read().split('\n') 50 | except FileNotFoundError: 51 | print("[!] Input file specified by you does not exist. Please check file path and location") 52 | sys.exit() 53 | except OSError: 54 | print("[!] Unable to open the file. Please check file path and permissions!") 55 | sys.exit() 56 | else: 57 | for line in lines: 58 | if not line: 59 | continue 60 | else: 61 | master_records.extend(_identify_and_return_records(line, shuffle)) 62 | 63 | return master_records 64 | 65 | 66 | def read_input_and_gen_list(inputstr, shuffle=False): 67 | master_records = [] 68 | master_records.extend(_identify_and_return_records(inputstr, shuffle)) 69 | return master_records 70 | -------------------------------------------------------------------------------- /src/ntlmrecon/misc.py: -------------------------------------------------------------------------------- 1 | from termcolor import colored 2 | 3 | 4 | def print_banner(): 5 | print(colored(""" 6 | _ _ _____ _ ___ _________ 7 | | \ | |_ _| | | \/ || ___ \ 8 | | \| | | | | | | . . || |_/ /___ ___ ___ _ __ 9 | | . ` | | | | | | |\/| || // _ \/ __/ _ \| '_ \ 10 | | |\ | | | | |____| | | || |\ \ __/ (_| (_) | | | | 11 | \_| \_/ \_/ \_____/\_| |_/\_| \_\___|\___\___/|_| |_| - @pwnfoo 12 | 13 | """ + colored("""v.0.4 beta - Y'all still exposing NTLM endpoints? 14 | """, 'green') + colored(""" 15 | Bug Reports, Feature Requests : https://git.io/JIR5z 16 | 17 | """, "cyan"), 'red')) 18 | 19 | 20 | INTERNAL_WORDLIST = [ 21 | "/abs", 22 | "/adfs/services/trust/2005/windowstransport", 23 | "/adfs/ls/wia", 24 | "/aspnet_client/", 25 | "/Autodiscover", 26 | "/Autodiscover/AutodiscoverService.svc/root", 27 | "/Autodiscover/Autodiscover.xml", 28 | "/AutoUpdate/", 29 | "/CertEnroll/", 30 | "/CertProv", 31 | "/CertSrv/", 32 | "/Conf/", 33 | "/debug/", 34 | "/deviceupdatefiles_ext/", 35 | "/deviceupdatefiles_int/", 36 | "/dialin", 37 | "/ecp/", 38 | "/Etc/", 39 | "/EWS/", 40 | "/Exchange/", 41 | "/Exchweb/", 42 | "/GroupExpansion/", 43 | "/HybridConfig", 44 | "/iwa/authenticated.aspx", 45 | "/iwa/iwa_test.aspx", 46 | "/mcx", 47 | "/meet", 48 | "/Microsoft-Server-ActiveSync/", 49 | "/OAB/", 50 | "/ocsp/", 51 | "/owa/", 52 | "/PersistentChat", 53 | "/PhoneConferencing/", 54 | "/PowerShell/", 55 | "/Public/", 56 | "/Reach/sip.svc", 57 | "/RequestHandler/", 58 | "/RequestHandlerExt", 59 | "/RequestHandlerExt/", 60 | "/Rgs/", 61 | "/RgsClients", 62 | "/Rpc/", 63 | "/RpcWithCert/", 64 | "/scheduler", 65 | "/sso", 66 | "/Ucwa", 67 | "/UnifiedMessaging/", 68 | "/WebTicket", 69 | "/WebTicket/WebTicketService.svc", 70 | "_windows/default.aspx?ReturnUrl=/", 71 | ] 72 | -------------------------------------------------------------------------------- /src/ntlmrecon/ntlmutil.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | import requests 3 | from requests.adapters import HTTPAdapter 4 | from requests.packages.urllib3.util.retry import Retry 5 | import urllib3 6 | import sys 7 | import base64 8 | import struct 9 | import string 10 | import collections 11 | from random import choice 12 | import json 13 | from termcolor import colored 14 | from colorama import init 15 | 16 | init() 17 | 18 | 19 | # We are hackers. SSL warnings don't stop us, although this is not recommended. 20 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 21 | 22 | # Decoder taken from https://gist.github.com/aseering/829a2270b72345a1dc42, Python3 ported and modified 23 | VALID_CHRS = set(string.ascii_letters + string.digits + string.punctuation) 24 | 25 | FOUND_DOMAINS = ['google.com'] 26 | 27 | FAIL_DOMAINS = [] 28 | 29 | def clean_str(st): 30 | return ''.join((s if s in VALID_CHRS else '?') for s in st) 31 | 32 | 33 | class StrStruct(object): 34 | def __init__(self, pos_tup, raw): 35 | length, alloc, offset = pos_tup 36 | self.length = length 37 | self.alloc = alloc 38 | self.offset = offset 39 | self.raw = raw[offset:offset + length] 40 | self.utf16 = False 41 | 42 | if len(self.raw) >= 2 and self.raw[1] == '\0': 43 | self.string = self.raw.decode('utf-16') 44 | self.utf16 = True 45 | else: 46 | self.string = self.raw 47 | 48 | def __str__(self): 49 | st = "%s'%s' [%s] (%db @%d)" % ('u' if self.utf16 else '', 50 | clean_str(self.string), 51 | self.raw, 52 | self.length, self.offset) 53 | if self.alloc != self.length: 54 | st += " alloc: %d" % self.alloc 55 | return st 56 | 57 | 58 | msg_types = collections.defaultdict(lambda: "UNKNOWN") 59 | msg_types[1] = "Request" 60 | msg_types[2] = "Challenge" 61 | msg_types[3] = "Response" 62 | 63 | target_field_types = collections.defaultdict(lambda: "UNKNOWN") 64 | target_field_types[0] = "TERMINATOR" 65 | target_field_types[1] = "Server name" 66 | target_field_types[2] = "AD domain name" 67 | target_field_types[3] = "FQDN" 68 | target_field_types[4] = "DNS domain name" 69 | target_field_types[5] = "Parent DNS domain" 70 | 71 | 72 | def decode_ntlm_str(st_raw): 73 | try: 74 | st = base64.b64decode(st_raw) 75 | except Exception as e: 76 | print("Input is not a valid base64-encoded string") 77 | return 78 | if st[:7] == b"NTLMSSP": 79 | pass 80 | else: 81 | print("Decode failed. NTLMSSP header not found at start of input string") 82 | return False 83 | 84 | return get_server_details(st) 85 | 86 | 87 | def opt_str_struct(name, st, offset): 88 | nxt = st[offset:offset + 8] 89 | if len(nxt) == 8: 90 | hdr_tup = struct.unpack("