├── .gitignore ├── LICENSE ├── README.md ├── assets └── demo.png ├── setup.py └── webclientservicescanner ├── __init__.py ├── console.py ├── core.py └── utils.py /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Idea 141 | .idea 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tamas Jos 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 | # WebClient Service Scanner 2 | 3 | 4 | ![Example](https://raw.githubusercontent.com/Hackndo/WebclientServiceScanner/master/assets/demo.png) 5 | 6 | 7 | Python tool to Check running WebClient services on multiple targets based on [@tifkin_ idea](https://twitter.com/tifkin_/status/1419806476353298442). 8 | 9 | This tool uses [impacket](https://github.com/SecureAuthCorp/impacket) project. 10 | 11 | 12 | ### Usage 13 | 14 | ```bash 15 | webclientservicescanner hackn.lab/user:S3cur3P4ssw0rd@10.10.10.0/24 16 | ``` 17 | 18 | Provided credentials will be tested against a domain controller before scanning so that a typo in the domain/username/password won't lock out the account. If you want to bypass this check, just use `-no-validation` flag. 19 | 20 | ### Exploitation 21 | 22 | Green entries mean that WebDav client is active on remote host. Using [PetitPotam](https://github.com/topotam/PetitPotam) or [PrinterBug](https://github.com/dirkjanm/krbrelayx/blob/master/printerbug.py), an HTTP authentication can be coerced and relayed to LDAP(S) on domain controllers. This relay can use [RBCD](https://shenaniganslabs.io/2019/01/28/Wagging-the-Dog.html) or [KeyCredentialLink](https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab) abuse to compromise relayed host. 23 | 24 | For more info about relaying, you can check out https://en.hackndo.com/ntlm-relay/ 25 | -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hackndo/WebclientServiceScanner/f0a73e67a35402d1dc7d342ed8889d129c96a48c/assets/demo.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Description: 3 | # Multithreaded tool to scan for Webclient service "DAV RPC SERVICE" named pipe 4 | # 5 | # Author: 6 | # pixis (@hackanddo) 7 | # 8 | # Acknowledgments: 9 | # @tifkin_ https://twitter.com/tifkin_/status/1419806476353298442 10 | 11 | 12 | import pathlib 13 | 14 | from setuptools import setup, find_packages 15 | 16 | from webclientservicescanner import __version__ 17 | 18 | HERE = pathlib.Path(__file__).parent 19 | README = (HERE / "README.md").read_text() 20 | 21 | setup( 22 | name="webclientservicescanner", 23 | version=__version__, 24 | author="Pixis", 25 | author_email="hackndo@gmail.com", 26 | description="Check running WebClient services on multiple targets", 27 | long_description=README, 28 | long_description_content_type="text/markdown", 29 | packages=find_packages(exclude=["assets",]), 30 | include_package_data=True, 31 | url="https://github.com/Hackndo/webclientservicescanner/", 32 | zip_safe = True, 33 | license="MIT", 34 | install_requires=[ 35 | 'impacket', 36 | 'netaddr' 37 | ], 38 | python_requires='>=3.6', 39 | classifiers=( 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | "Programming Language :: Python :: 3.8", 43 | "Programming Language :: Python :: 3.9", 44 | "License :: OSI Approved :: MIT License", 45 | "Operating System :: OS Independent", 46 | ), 47 | entry_points={ 48 | 'console_scripts': [ 49 | 'webclientservicescanner = webclientservicescanner.console:main', 50 | ], 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /webclientservicescanner/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Description: 3 | # Multithreaded tool to scan for Webclient service "DAV RPC SERVICE" named pipe 4 | # 5 | # Author: 6 | # pixis (@hackanddo) 7 | # 8 | # Acknowledgments: 9 | # @tifkin_ https://twitter.com/tifkin_/status/1419806476353298442 10 | 11 | 12 | __version__ = '0.1.0' 13 | -------------------------------------------------------------------------------- /webclientservicescanner/console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Description: 3 | # Multithreaded tool to scan for Webclient service "DAV RPC SERVICE" named pipe 4 | # 5 | # Author: 6 | # pixis (@hackanddo) 7 | # 8 | # Acknowledgments: 9 | # @tifkin_ https://twitter.com/tifkin_/status/1419806476353298442 10 | 11 | 12 | import argparse 13 | import sys 14 | 15 | from impacket.examples.utils import parse_target 16 | from impacket.smb import SMB_DIALECT 17 | from impacket.smb3structs import SMB2_DIALECT_21, SMB2_DIALECT_311 18 | 19 | from webclientservicescanner.core import ThreadPool 20 | from webclientservicescanner.utils import get_targets, banner, validate_credentials 21 | 22 | 23 | def main(): 24 | print(banner()) 25 | parser = argparse.ArgumentParser(add_help=True, description="SMB client implementation.") 26 | 27 | parser.add_argument('target', action='store', 28 | help='[[domain/]username[:password]@]') 29 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 30 | parser.add_argument('-smb-version', choices=['1', '2', '3'], help='SMB version to negotiate') 31 | 32 | group = parser.add_argument_group('authentication') 33 | 34 | group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 35 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 36 | group.add_argument('-k', action="store_true", 37 | help='Use Kerberos authentication. Grabs credentials from ccache file ' 38 | '(KRB5CCNAME) based on target parameters. If valid credentials ' 39 | 'cannot be found, it will use the ones specified in the command ' 40 | 'line') 41 | group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ') 42 | group.add_argument('-no-validation', action="store_true", help='Bypass credentials validation') 43 | 44 | group = parser.add_argument_group('connection') 45 | 46 | group.add_argument('-dc-ip', action='store', metavar="ip address", 47 | help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' 48 | 'the target parameter') 49 | 50 | group = parser.add_argument_group('parallelization') 51 | 52 | group.add_argument('-threads', action='store', default='256', metavar="threads", help='Max threads') 53 | 54 | if len(sys.argv) == 1: 55 | parser.print_help() 56 | sys.exit(1) 57 | 58 | options = parser.parse_args() 59 | 60 | domain, username, password, address = parse_target(options.target) 61 | 62 | targets = get_targets([address]) 63 | 64 | if domain is None: 65 | domain = '' 66 | 67 | if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 68 | from getpass import getpass 69 | password = getpass("Password:") 70 | 71 | if options.aesKey is not None: 72 | options.k = True 73 | 74 | if options.hashes is not None: 75 | lmhash, nthash = options.hashes.split(':') 76 | else: 77 | lmhash = '' 78 | nthash = '' 79 | 80 | if options.smb_version is not None: 81 | smb_versions = ( 82 | (SMB_DIALECT, '1'), 83 | (SMB2_DIALECT_21, '2'), 84 | (SMB2_DIALECT_311, '3') 85 | ) 86 | options.smb_version = [version[0] for version in smb_versions if version[1] == options.smb_version][0] 87 | 88 | if not options.no_validation: 89 | ret = validate_credentials(username, domain, password, options.dc_ip, options.k, lmhash, nthash, options.aesKey, options.debug) 90 | if not ret: 91 | return False 92 | 93 | threadPool = ThreadPool( 94 | targets, 95 | options.smb_version, 96 | username, 97 | password, 98 | domain, 99 | lmhash, 100 | nthash, 101 | options.aesKey, 102 | options.dc_ip, 103 | options.k, 104 | int(options.threads), 105 | options.debug 106 | ) 107 | 108 | threadPool.run() 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | -------------------------------------------------------------------------------- /webclientservicescanner/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Description: 3 | # Multithreaded tool to scan for Webclient service "DAV RPC SERVICE" named pipe 4 | # 5 | # Author: 6 | # pixis (@hackanddo) 7 | # 8 | # Acknowledgments: 9 | # @tifkin_ https://twitter.com/tifkin_/status/1419806476353298442 10 | 11 | 12 | from __future__ import division 13 | from __future__ import print_function 14 | 15 | import signal 16 | import threading 17 | from queue import Queue 18 | 19 | from impacket.smbconnection import SMBConnection 20 | 21 | lock = threading.RLock() 22 | 23 | 24 | class COLORS: 25 | GREEN = '\033[92m' 26 | RED = '\033[91m' 27 | ENDC = '\033[0m' 28 | 29 | 30 | class Worker(threading.Thread): 31 | def __init__(self, task_q): 32 | super().__init__() 33 | self.task_q = task_q 34 | self.shutdown_flag = threading.Event() 35 | 36 | def run(self): 37 | while not self.shutdown_flag.is_set(): 38 | worker_scanner = self.task_q.get() 39 | self.name = worker_scanner.address 40 | worker_scanner.run() 41 | self.task_q.task_done() 42 | 43 | 44 | class ThreadPool: 45 | def __init__(self, targets, smb_version, username, password, domain, lmhash, nthash, aesKey, dc_ip, k, threads, debug): 46 | self.targets = targets 47 | self.smb_version = smb_version 48 | self.username = username 49 | self.password = password 50 | self.domain = domain 51 | self.lmhash = lmhash 52 | self.nthash = nthash 53 | self.aesKey = aesKey 54 | self.dc_ip = dc_ip 55 | self.k = k 56 | self.threads = [] 57 | self.max_threads = threads 58 | self.debug = debug 59 | self.task_q = Queue(self.max_threads+10) 60 | signal.signal(signal.SIGINT, self.interrupt_event) 61 | signal.signal(signal.SIGTERM, self.interrupt_event) 62 | 63 | def interrupt_event(self, signum, stack): 64 | print("**CTRL+C** QUITTING GRACEFULLY") 65 | self.stop() 66 | raise KeyboardInterrupt 67 | 68 | def stop(self): 69 | for thread in self.threads: 70 | thread.shutdown_flag.set() 71 | for thread in self.threads: 72 | thread.join() 73 | 74 | def isRunning(self): 75 | return any(thread.is_alive() for thread in self.threads) 76 | 77 | def run(self): 78 | threading.current_thread().name = "[Main Thread]" 79 | 80 | try: 81 | # Turn-on the worker threads 82 | for i in range(self.max_threads): 83 | thread = Worker(self.task_q) 84 | thread.daemon = True 85 | self.threads.append(thread) 86 | thread.start() 87 | 88 | instance_id = 1 89 | for target in self.targets: 90 | self.task_q.put(WebdavClientScanner( 91 | target, 92 | target, 93 | self.smb_version, 94 | self.username, 95 | self.password, 96 | self.domain, 97 | self.lmhash, 98 | self.nthash, 99 | self.aesKey, 100 | self.dc_ip, 101 | self.k, 102 | self.debug 103 | )) 104 | instance_id += 1 105 | 106 | # Block until all tasks are done 107 | self.task_q.join() 108 | 109 | except KeyboardInterrupt as e: 110 | print("Quitting.") 111 | 112 | 113 | class WebdavClientScanner: 114 | def __init__(self, address, target_ip, smb_version, username, password, domain, lmhash, nthash, aesKey, dc_ip, k, debug): 115 | self.address = address 116 | self.target_ip = target_ip 117 | self.smb_version = smb_version 118 | self.username = username 119 | self.password = password 120 | self.domain = domain 121 | self.lmhash = lmhash 122 | self.nthash = nthash 123 | self.aesKey = aesKey 124 | self.dc_ip = dc_ip 125 | self.k = k 126 | self.debug = debug 127 | 128 | def run(self): 129 | try: 130 | smbClient = SMBConnection(self.address, self.target_ip, preferredDialect=self.smb_version, timeout=2) 131 | if self.k is True: 132 | smbClient.kerberosLogin(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, self.dc_ip) 133 | else: 134 | smbClient.login(self.username, self.password, self.domain, self.lmhash, self.nthash) 135 | 136 | pwd = '\\*' 137 | for f in smbClient.listPath('IPC$', pwd): 138 | if f.get_longname() == 'DAV RPC SERVICE': 139 | with lock: 140 | print("[{}] {}RUNNING{}".format(self.address, COLORS.GREEN, COLORS.ENDC)) 141 | return True 142 | with lock: 143 | print("[{}] {}STOPPED{}".format(self.address, COLORS.RED, COLORS.ENDC)) 144 | return False 145 | 146 | except Exception as e: 147 | if self.debug: 148 | import traceback 149 | with lock: 150 | traceback.print_exc() 151 | if self.debug or not (isinstance(e, OSError) and 'timed out' in str(e)): 152 | with lock: 153 | print(str(e)) 154 | return False 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /webclientservicescanner/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Description: 3 | # Multithreaded tool to scan for Webclient service "DAV RPC SERVICE" named pipe 4 | # 5 | # Author: 6 | # pixis (@hackanddo) 7 | # 8 | # Acknowledgments: 9 | # @tifkin_ https://twitter.com/tifkin_/status/1419806476353298442 10 | 11 | 12 | import os 13 | import sys 14 | import socket 15 | 16 | from impacket.smbconnection import SMBConnection, SessionError 17 | from netaddr import IPRange, AddrFormatError, IPAddress, IPNetwork 18 | 19 | from . import __version__ 20 | 21 | 22 | def banner(): 23 | return "WebClient Service Scanner v{} - pixis (@hackanddo) - Based on @tifkin_ idea\n".format(__version__) 24 | 25 | 26 | def parse_targets(target): 27 | """ 28 | Parse provided targets 29 | :param target: Targets 30 | :return: List of IP addresses 31 | """ 32 | if '-' in target: 33 | ip_range = target.split('-') 34 | try: 35 | t = IPRange(ip_range[0], ip_range[1]) 36 | except AddrFormatError: 37 | try: 38 | start_ip = IPAddress(ip_range[0]) 39 | 40 | start_ip_words = list(start_ip.words) 41 | start_ip_words[-1] = ip_range[1] 42 | start_ip_words = [str(v) for v in start_ip_words] 43 | 44 | end_ip = IPAddress('.'.join(start_ip_words)) 45 | 46 | t = IPRange(start_ip, end_ip) 47 | except AddrFormatError: 48 | t = target 49 | else: 50 | try: 51 | t = IPNetwork(target) 52 | except AddrFormatError: 53 | t = target 54 | if type(t) == IPNetwork or type(t) == IPRange: 55 | return list(t) 56 | else: 57 | return [t.strip()] 58 | 59 | 60 | def get_targets(targets): 61 | """ 62 | Get targets from file or string 63 | :param targets: List of targets 64 | :return: List of IP addresses 65 | """ 66 | ret_targets = [] 67 | for target in targets: 68 | if os.path.exists(target): 69 | with open(target, 'r') as target_file: 70 | for target_entry in target_file: 71 | ret_targets += parse_targets(target_entry) 72 | else: 73 | ret_targets += parse_targets(target) 74 | return [str(ip) for ip in ret_targets] 75 | 76 | 77 | def validate_credentials(username, domain, password, dc_ip, k, lmhash, nthash, aesKey, debug): 78 | if dc_ip is None: 79 | try: 80 | dc_ip = socket.gethostbyname(domain) 81 | except Exception as e: 82 | print("Couldn't retrieve {} domain controller, specify it with -dc-ip parameter".format(domain)) 83 | return False 84 | try: 85 | smbClient = SMBConnection(domain, dc_ip, timeout=2) 86 | if k is True: 87 | smbClient.kerberosLogin(username, password, domain, lmhash, nthash, aesKey, dc_ip) 88 | else: 89 | smbClient.login(username, password, domain, lmhash, nthash) 90 | return True 91 | except SessionError as e: 92 | if 'STATUS_LOGON_FAILURE' in str(e): 93 | print("Credentials validation failed against {}. If you really want to use these credentials, use -no-validation flag. Beware of lockout.".format(dc_ip)) 94 | return False 95 | else: 96 | if debug: 97 | import traceback 98 | traceback.print_exc() 99 | print("Credentials could not be checked, an error occurred") 100 | return False 101 | except Exception as e: 102 | if debug: 103 | import traceback 104 | traceback.print_exc() 105 | print("Credentials could not be checked, an error occurred") 106 | return False 107 | --------------------------------------------------------------------------------