diff --git a/README.md b/README.md index 8fc8d3d..f072b23 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,19 @@ The blog detailing the original research largely from an engineering perspective @_logangoins github.com/jlevere -usage: soapy [-h] [--debug] [--ts] [-H nthash] [--users] [--computers] [--groups] [--constrained] - [--unconstrained] [--spns] [--asreproastable] [--admins] [--rbcds] [-q query] [-f attr,attr,...] - [-dn distinguishedname] [-p] [--rbcd source] [--spn value] [--asrep] [--account account] [--remove] +usage: soapy [-h] [--debug] [--ts] [-H nthash] [--users] [--computers] + [--groups] [--constrained] [--unconstrained] [--spns] + [--asreproastable] [--admins] [--rbcds] [-q query] + [-f attr,attr,...] [-dn distinguishedname] [-p] [--rbcd source] + [--spn value] [--asrep] [--account account] [--remove] + [--addcomputer [MACHINE]] [--computer-pass pass] [--ou ou] + [--delete-computer MACHINE] [--disable-account MACHINE] + [--dns-add FQDN] [--dns-modify FQDN] [--dns-remove FQDN] + [--dns-tombstone FQDN] [--dns-resurrect FQDN] [--dns-ip IP] + [--ldapdelete] [--allow-multiple] [--ttl TTL] [--tcp] connection -Perform AD reconnaisance and post-exploitation through ADWS from Linux +Perform AD reconnaissance and post-exploitation through ADWS from Linux positional arguments: connection domain/username[:password]@ @@ -34,34 +41,58 @@ options: -h, --help show this help message and exit --debug Turn DEBUG output ON --ts Adds timestamp to every logging output. - -H, --hash nthash Use an NT hash for authentication + -H nthash, --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 + --constrained Enumerate objects with msds-allowedtodelegateto + --unconstrained Enumerate objects with TRUSTED_FOR_DELEGATION + --spns Enumerate accounts with servicePrincipalName set + --asreproastable Enumerate accounts with DONT_REQ_PREAUTH set --admins Enumerate high privilege accounts - --rbcds Enumerate accounts with msDs-AllowedToActOnBehalfOfOtherIdentity set - -q, --query query Raw query to execute on the target - -f, --filter attr,attr,... - Attributes to select from the objects returned, in a comma seperated list - -dn, --distinguishedname distinguishedname - The root objects distinguishedName for the query + --rbcds Enumerate accounts with msDs- + AllowedToActOnBehalfOfOtherIdentity set + -q query, --query query + Raw query to execute on the target + -f attr,attr,..., --filter attr,attr,... + Attributes to select, comma separated + -dn distinguishedname, --distinguishedname distinguishedname + The root object's distinguishedName for the query -p, --parse Parse attributes to human readable format 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 + --rbcd source Write/remove RBCD (source computer) + --spn value Write servicePrincipalName value (use --remove to + delete) + --asrep Write DONT_REQ_PREAUTH flag (asrep roastable) + --account account Account to perform operations on + --remove Remove attribute value based on operation + --addcomputer [MACHINE] + Create a computer account in AD (optional MACHINE + name) + --computer-pass pass Password for the new computer account (optional). + --ou ou DN of the OU where to create the computer (optional). + --delete-computer MACHINE + Delete an existing computer account + --disable-account MACHINE + Disable a computer account (set AccountDisabled) + --dns-add FQDN Add A record (FQDN). Requires --dns-ip + --dns-modify FQDN Modify/replace A record (FQDN). Requires --dns-ip + --dns-remove FQDN Remove A record (FQDN). Requires --dns-ip unless + --ldapdelete + --dns-tombstone FQDN Tombstone a dnsNode (replace with TS record + set + dNSTombstoned=true) + --dns-resurrect FQDN Resurrect a tombstoned dnsNode + --dns-ip IP IP used with dns add/modify/remove + --ldapdelete Use delete on dnsNode object (when used with --dns- + remove) + --allow-multiple Allow multiple A records when adding + --ttl TTL TTL for new A record (default 180) + --tcp Use DNS over TCP when fetching SOA serial + ``` # Installation diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 8d68f68..0000000 --- a/poetry.lock +++ /dev/null @@ -1,753 +0,0 @@ -# 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" diff --git a/src/ad_dns_manager_adws.py b/src/ad_dns_manager_adws.py new file mode 100644 index 0000000..65cfc56 --- /dev/null +++ b/src/ad_dns_manager_adws.py @@ -0,0 +1,988 @@ +#!/usr/bin/env python3 +""" +ADWS DNS helpers (single-file). + +This module implements AD-integrated DNS operations over ADWS (Create / Put / Delete) +and reproduces the binary DNS structures required to serialize/deserialize the +addata:dnsRecord attribute. + +This version: + - Discovers the exact dnsZone DN as returned by the DC and uses it as the parent + container for dnsNode creation (avoids constructing "DC=example.local" mistakes). + - Creates dnsNode objects via LDAP ResourceFactory and deliberately omits addata:dNSTombstoned + because writing that attribute via ADWS often triggers validation errors (e.g. BadPutOrCreateValue). + Tombstoning is managed by the DC and does not affect normal create/delete operations, + so use the dedicated tombstone/resurrect helpers (or LDAP) later if you need to change that state. + - Removes inline SOAP/XML templates: they are now imported from src.soap_templates + to keep this file focused on logic and binary DNS structures. + - Prints debug payloads for inspection (use logger if desired). +""" + +from base64 import b64encode, b64decode +from uuid import uuid4 +import datetime +import socket +from struct import unpack +from typing import Tuple, List, Dict, Optional + +import dns.resolver +from impacket.structure import Structure + +from src.adws import ADWSConnect, NTLMAuth +# SOAP/XML templates and namespaces moved to src.soap_templates to keep this file lean. +from src.soap_templates import ( + NAMESPACES, + LDAP_PUT_FSTRING, + LDAP_DELETE_FOR_RESOURCE, + LDAP_CREATE_FOR_RESOURCEFACTORY, + LDAP_ROOT_DSE_FSTRING, +) + +# ----------------------- +# DNS binary structure classes (MS-DNSP) +# ----------------------- + +class DNS_RECORD(Structure): + structure = ( + ('DataLength', 'L'), + ('Reserved', ' str: + ind = 0 + labels = [] + for i in range(self['LabelCount']): + nextlen = unpack('B', self['RawName'][ind:ind+1])[0] + labels.append(self['RawName'][ind+1:ind+1+nextlen].decode('utf-8')) + ind += nextlen + 1 + labels.append('') + return '.'.join(labels) + +class DNS_RPC_NODE(Structure): + structure = ( + ('wLength', '>H'), + ('wRecordCount', '>H'), + ('dwFlags', '>L'), + ('dwChildCount', '>L'), + ('dnsNodeName', ':') + ) + +class DNS_RPC_RECORD_A(Structure): + structure = ( + ('address', ':'), + ) + + def formatCanonical(self) -> str: + return socket.inet_ntoa(self['address']) + + def fromCanonical(self, canonical: str): + self['address'] = socket.inet_aton(canonical) + +class DNS_RPC_RECORD_NODE_NAME(Structure): + structure = ( + ('nameNode', ':', DNS_COUNT_NAME), + ) + +class DNS_RPC_RECORD_SOA(Structure): + structure = ( + ('dwSerialNo', '>L'), + ('dwRefresh', '>L'), + ('dwRetry', '>L'), + ('dwExpire', '>L'), + ('dwMinimumTtl', '>L'), + ('namePrimaryServer', ':', DNS_COUNT_NAME), + ('zoneAdminEmail', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_NULL(Structure): + structure = ( + ('bData', ':'), + ) + +class DNS_RPC_RECORD_NAME_PREFERENCE(Structure): + structure = ( + ('wPreference', '>H'), + ('nameExchange', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_AAAA(Structure): + structure = ( + ('ipv6Address', '16s'), + ) + +class DNS_RPC_RECORD_SRV(Structure): + structure = ( + ('wPriority', '>H'), + ('wWeight', '>H'), + ('wPort', '>H'), + ('nameTarget', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_TS(Structure): + structure = ( + ('entombedTime', ' datetime.datetime: + microseconds = self['entombedTime'] / 10.0 + return datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=microseconds) + +# ----------------------- +# Utility helpers +# ----------------------- + +RECORD_TYPE_MAPPING = { + 0: 'ZERO', + 1: 'A', + 2: 'NS', + 5: 'CNAME', + 6: 'SOA', + 33: 'SRV', + 65281: 'WINS' +} + +def get_next_serial(dnsserver: Optional[str], dc: str, zone: str, tcp: bool) -> int: + resolver = dns.resolver.Resolver() + server = dnsserver if dnsserver else dc + try: + socket.inet_aton(server) + resolver.nameservers = [server] + except Exception: + pass + try: + res = resolver.resolve(zone, 'SOA', tcp=tcp) + except Exception: + return 1 + for answer in res: + try: + return answer.serial + 1 + except Exception: + try: + return int(answer.to_text().split()[-1]) + 1 + except Exception: + return 1 + return 1 + +def new_record(rtype: int, serial: int) -> DNS_RECORD: + nr = DNS_RECORD() + nr['Type'] = rtype + nr['Serial'] = serial + nr['TtlSeconds'] = 180 + nr['Rank'] = 240 + return nr + +def _xml_text_or_inner_value(elem, namespaces): + if elem is None: + return None + child = elem.find('.//ad:value', namespaces=namespaces) + if child is not None and child.text and child.text.strip(): + return child.text.strip() + if elem.text and elem.text.strip(): + return elem.text.strip() + return None + +def get_rootdse_contexts(adws_client: ADWSConnect) -> Dict[str, object]: + """ + Query RootDSE via ADWS to obtain naming contexts and schema DN etc. + Uses LDAP_ROOT_DSE_FSTRING imported from src.soap_templates. + """ + msgid = f"urn:uuid:{uuid4()}" + payload = LDAP_ROOT_DSE_FSTRING.format(uuid=msgid, fqdn=adws_client._fqdn) + adws_client._nmf.send(payload) + raw = adws_client._nmf.recv() + try: + et = adws_client._handle_str_to_xml(raw) + if et is None: + raise RuntimeError("client._handle_str_to_xml returned None") + except Exception: + from xml.etree import ElementTree as ET + s = raw if isinstance(raw, str) else raw.decode(errors="ignore") + start = s.find('<') + if start != -1: + s = s[start:] + try: + et = ET.fromstring(s) + except Exception as e: + raise RuntimeError(f"Failed parsing RootDSE XML: {e}\nRaw (truncated): {s[:1000]}") + ns = NAMESPACES if isinstance(NAMESPACES, dict) else { + 'ad': "http://schemas.microsoft.com/2008/1/ActiveDirectory", + 'addata': "http://schemas.microsoft.com/2008/1/ActiveDirectory/Data", + } + def _find_once(attr): + candidates = [ + f".//addata:{attr}/ad:value", + f".//ad:{attr}/ad:value", + f".//addata:{attr}", + f".//ad:{attr}", + f".//{attr}/ad:value", + f".//{attr}", + ] + for xp in candidates: + for elem in et.findall(xp, namespaces=ns): + v = _xml_text_or_inner_value(elem, namespaces=ns) + if v: + return v + return None + def _find_all(attr): + vals = [] + candidates = [ + f".//addata:{attr}/ad:value", + f".//ad:{attr}/ad:value", + f".//addata:{attr}", + f".//ad:{attr}", + f".//{attr}/ad:value", + f".//{attr}", + ] + for xp in candidates: + for elem in et.findall(xp, namespaces=ns): + v = _xml_text_or_inner_value(elem, namespaces=ns) + if v and v not in vals: + vals.append(v) + return vals + contexts = { + "schemaNamingContext": _find_once("schemaNamingContext"), + "rootDomainNamingContext": _find_once("rootDomainNamingContext"), + "configurationNamingContext": _find_once("configurationNamingContext"), + "defaultNamingContext": _find_once("defaultNamingContext"), + "namingContexts": _find_all("namingContexts") or _find_all("namingContext") or [], + "domainFunctionality": _find_all("domainFunctionality"), + "forestFunctionality": _find_all("forestFunctionality"), + } + if not contexts["defaultNamingContext"] and contexts["namingContexts"]: + contexts["defaultNamingContext"] = contexts["namingContexts"][0] + if not contexts["schemaNamingContext"] and contexts["defaultNamingContext"]: + contexts["schemaNamingContext"] = "CN=Schema," + contexts["defaultNamingContext"] + return contexts + +# ----------------------- +# dnsNode discovery and builders +# ----------------------- + +def find_dns_node( + target: str, + zone: str, + ip: str, + domain: str, + username: str, + auth: NTLMAuth, + forest: bool = False, + legacy: bool = False, + pull_client: Optional[ADWSConnect] = None, + res_client: Optional[ADWSConnect] = None, +) -> Tuple[Optional[str], List[bytes], bool]: + """ + Locate the dnsNode for the given target and zone. + + Optional: accept a pull_client and res_client to reuse existing ADWS connections + (reduces repeated "Connecting to ..." logs). + + Returns: + (node_dn or None, list_of_raw_dnsRecord_bytes, tombstoned_flag) + """ + # Reuse provided res_client / pull_client if available (avoid creating new connections) + if res_client is None: + res_client = ADWSConnect(ip, domain, username, auth, "Resource") + contexts = get_rootdse_contexts(res_client) + domainroot = contexts.get("defaultNamingContext") or contexts.get("rootDomainNamingContext") + if not domainroot: + domainroot = ",".join([f"DC={p}" for p in domain.split(".") if p]) + + if forest: + dnsroot = f"CN=MicrosoftDNS,DC=ForestDnsZones,{domainroot}" + else: + if legacy: + dnsroot = f"CN=MicrosoftDNS,CN=System,{domainroot}" + else: + dnsroot = f"CN=MicrosoftDNS,DC=DomainDnsZones,{domainroot}" + + searchtarget = f"DC={zone},{dnsroot}" + query = f"(&(objectClass=dnsNode)(name={target}))" + + if pull_client is None: + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + + try: + et = pull_client.pull(query=query, basedn=searchtarget, attributes=["dnsRecord", "dNSTombstoned", "distinguishedName", "name"]) + except Exception: + # fallback to a broader search under dnsroot if the constructed base didn't match + et = pull_client.pull(query=query, basedn=dnsroot, attributes=["dnsRecord", "dNSTombstoned", "distinguishedName", "name"]) + + node_dn: Optional[str] = None + raw_records: List[bytes] = [] + tombstoned = False + + nodes = et.findall(".//addata:dnsNode", namespaces=NAMESPACES) + et.findall(".//addata:entry", namespaces=NAMESPACES) + for node in nodes: + dn_elem = node.find(".//addata:distinguishedName/ad:value", namespaces=NAMESPACES) + if dn_elem is not None and dn_elem.text: + node_dn = dn_elem.text + ts_elem = node.find(".//addata:dNSTombstoned/ad:value", namespaces=NAMESPACES) + if ts_elem is not None and ts_elem.text: + tombstoned = ts_elem.text.lower() == "true" + for rec in node.findall(".//addata:dnsRecord/ad:value", namespaces=NAMESPACES): + if rec is None or rec.text is None: + continue + try: + raw = b64decode(rec.text) + except Exception: + raw = rec.text.encode("latin-1") + raw_records.append(raw) + + if node_dn is None: + for item in et.findall(".//addata:distinguishedName/ad:value", namespaces=NAMESPACES): + if item is not None and item.text: + node_dn = item.text + break + + return node_dn, raw_records, tombstoned + +# ----------------------- +# Build bytes helpers +# ----------------------- + +def build_a_record_bytes(ip_str: str, serial: int, ttl: int = 180) -> bytes: + rec = new_record(1, serial) + rec['TtlSeconds'] = ttl + a = DNS_RPC_RECORD_A() + a.fromCanonical(ip_str) + rec['Data'] = a + return rec.getData() + +def build_ts_record_bytes(entombed_time_filetime: int) -> bytes: + rec = new_record(0, 0) + ts = DNS_RPC_RECORD_TS() + ts['entombedTime'] = entombed_time_filetime + rec['Data'] = ts + return rec.getData() + +def _split_target_and_zone(fqdn_record: str, domain: str) -> Tuple[str, str]: + parts = fqdn_record.split(".") + if len(parts) >= 2: + target = parts[0] + zone = ".".join(parts[1:]) + elif len(parts) == 1 and parts[0] != "": + target = parts[0] + zone = domain + else: + raise ValueError("fqdn_record must be at least 'name.zone' or a single host name (zone fallback to domain).") + return target, zone + +def _b64(data: bytes) -> str: + return b64encode(data).decode("utf-8") + +def _make_msgid() -> str: + return f"urn:uuid:{uuid4()}" + +# ----------------------- +# Strict Replace setter for dNSTombstoned (single attempt only) +# ----------------------- + +def _set_dnstombstoned_replace_boolean(resource_client: ADWSConnect, object_ref: str, value: bool) -> bool: + """ + Send a single strict ModifyRequest Replace following Microsoft's ADWS example: + - SOAP Action: http://schemas.xmlsoap.org/ws/2004/09/transfer/Put + - Include IdentityManagementOperation element in header (mustUnderstand=1) + - Use ad:objectReferenceProperty (can be DN or GUID) and ad:instance + - Body: ModifyRequest (DirectoryAccess namespace) with a single Change Operation="replace" + and addata:dNSTombstoned plus + true|false + + Returns True on success (no SOAP Fault), False otherwise. Prints the raw response (truncated) + and any parsed Fault for debugging. + """ + val = "TRUE" if value else "FALSE" + msgid = _make_msgid() + + # Use LDAP_PUT_FSTRING imported from src.soap_templates + payload = LDAP_PUT_FSTRING.format( + uuid=msgid, + fqdn=resource_client._fqdn, + object_ref=object_ref, + operation="replace", + attribute="addata:dNSTombstoned", + data_type="boolean", + value=val, + ) + + # send and receive raw response + try: + resource_client._nmf.send(payload) + raw = resource_client._nmf.recv() + except Exception as e: + print(f"[ERROR] transport error sending Replace dNSTombstoned: {e}") + return False + + # try to parse and show response for debugging + try: + et = resource_client._handle_str_to_xml(raw) + except Exception: + et = None + + s = raw if isinstance(raw, str) else raw.decode(errors="ignore") + print("[DEBUG] Replace dNSTombstoned response (truncated):") + print(s[:2000]) + + # if parsed and contains a SOAP Fault -> failure + if et is not None: + # SOAP Fault elements are in the SOAP envelope namespace + fault = et.find(".//{http://www.w3.org/2003/05/soap-envelope}Fault") + if fault is not None: + # print fault detail for user + try: + from xml.etree import ElementTree as ET + print("[DEBUG] Parsed Fault:") + print(ET.tostring(fault, encoding="unicode")) + except Exception: + pass + return False + + # no Fault detected -> assume success + return True + +# ----------------------- +# High-level ADWS operations +# ----------------------- + +def add_dns_record_adws( + fqdn_record: str, + ip_addr: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth, + allow_multiple: bool = False, + ttl: int = 180, + tcp: bool = False, + forest: bool = False, + legacy: bool = False, +) -> bool: + """ + Add an A record via ADWS. Creates dnsNode if needed. + + Behavior (ADWS-only): + - If the dnsNode exists, append the A record via ADWS Put(add). + - If the dnsNode does not exist, create it via ResourceFactory Create + (without dNSTombstoned). The function will NOT set dNSTombstoned automatically + after creation to avoid ADWS BadPutOrCreateValue errors; if needed, set the flag + later with the explicit tombstone/resurrect helpers. + + This function now reuses a small set of ADWS clients (resource/pull/put) + to avoid creating many connections and producing repeated "Connecting to ..." logs. + """ + target, zone = _split_target_and_zone(fqdn_record, domain) + + # Reuse clients to reduce noisy connection logs + resource_client = ADWSConnect(ip, domain, username, auth, "Resource") + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + put_client = ADWSConnect.put_client(ip, domain, username, auth) + + node_dn, existing_raw, tomb = find_dns_node( + target=target, + zone=zone, + ip=ip, + domain=domain, + username=username, + auth=auth, + forest=forest, + legacy=legacy, + pull_client=pull_client, + res_client=resource_client, + ) + + serial = get_next_serial(None, ip, zone, tcp) + record_bytes = build_a_record_bytes(ip_addr, serial, ttl) + + # If node exists, add A record (respect allow_multiple) + if node_dn: + if not allow_multiple: + for raw in existing_raw: + try: + dr = DNS_RECORD(raw) + if dr["Type"] == 1: + a = DNS_RPC_RECORD_A(dr["Data"]) + raise RuntimeError( + f"A record already exists (points to {a.formatCanonical()}). Use modify or allow_multiple." + ) + except Exception: + pass + put_client.put( + object_ref=node_dn, + operation="add", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(record_bytes), + ) + print(f"[+] Added A record {target}.{zone} -> {ip_addr} (ADWS Put add on {node_dn})") + return True + + # Need to create dnsNode via ResourceFactory Create (do NOT include dNSTombstoned) + contexts = get_rootdse_contexts(resource_client) + schemaNamingContext = contexts.get("schemaNamingContext") + if not schemaNamingContext: + schemaNamingContext = "CN=Schema," + ",".join([f"DC={p}" for p in domain.split(".") if p]) + + objectCategory = f"CN=Dns-Node,{schemaNamingContext}" + + # domain_dn (DC=example,DC=local) + domain_parts = [p for p in domain.split(".") if p] + domain_dn = ",".join([f"DC={p}" for p in domain_parts]) + container_root = f"CN=MicrosoftDNS,DC=DomainDnsZones,{domain_dn}" + + # enumerate dnsZone objects and pick the correct one (reuse pull_client) + try: + et_zones = pull_client.pull(query="(objectClass=dnsZone)", basedn=container_root, attributes=["distinguishedName"]) + except Exception as e: + raise RuntimeError(f"Failed to enumerate dnsZone under {container_root}: {e}") + + zone_dns = [] + for elem in et_zones.findall(".//addata:distinguishedName/ad:value", namespaces=NAMESPACES): + if elem is not None and elem.text: + zone_dns.append(elem.text.strip()) + + # choose matching zone DN + chosen_zone_dn = None + zone_lc = zone.lower() + for zdn in zone_dns: + first_rdn = zdn.split(",", 1)[0].strip() + if first_rdn.lower().startswith("dc="): + rdn_val = first_rdn[3:].lower() + else: + rdn_val = first_rdn.lower() + if zone_lc == rdn_val or (zone_lc in rdn_val) or (rdn_val in zone_lc): + chosen_zone_dn = zdn + break + + if not chosen_zone_dn: + # try some candidates + zone_parts = [p for p in zone.split(".") if p] + if zone_parts: + cand1 = f"DC={zone},{container_root}" + cand2 = f"DC={zone_parts[0]},{container_root}" + for c in (cand1, cand2): + if c in zone_dns: + chosen_zone_dn = c + break + if not chosen_zone_dn and zone_dns: + chosen_zone_dn = zone_dns[0] + + if not chosen_zone_dn: + raise RuntimeError(f"Could not find dnsZone DN for zone '{zone}'. Enumerated: {zone_dns}") + + valid_container = chosen_zone_dn + + # build attributes for creation (without dNSTombstoned) + base_atav_parts = [] + base_atav_parts.append( + " \n" + " addata:objectClass\n" + " \n" + " dnsNode\n" + " \n" + " \n" + ) + base_atav_parts.append( + " \n" + " ad:container-hierarchy-parent\n" + " \n" + f" {valid_container}\n" + " \n" + " \n" + ) + base_atav_parts.append( + " \n" + " addata:objectCategory\n" + f" \n {objectCategory}\n" + " \n" + " \n" + ) + base_atav_parts.append( + " \n" + " addata:name\n" + " \n" + f" {target}\n" + " \n" + " \n" + ) + base_atav_parts.append( + " \n" + " addata:dnsRecord\n" + " \n" + f" {_b64(record_bytes)}\n" + " \n" + " \n" + ) + + # Try RDN styles: DC= then CN= + rdn_candidates = [f"DC={target}", f"CN={target}"] + last_exc = None + for rdn in rdn_candidates: + atav_parts = [] + atav_parts.append(base_atav_parts[0]) # objectClass + # relative RDN + atav_parts.append( + " \n" + " ad:relativeDistinguishedName\n" + " \n" + f" {rdn}\n" + " \n" + " \n" + ) + atav_parts.extend(base_atav_parts[1:]) + atav_xml = "".join(atav_parts) + + msg_id = _make_msgid() + create_payload = LDAP_CREATE_FOR_RESOURCEFACTORY.format( + uuid=msg_id, + fqdn=resource_client._fqdn, + atav_xml=atav_xml, + container_dn=valid_container, + object_class="dnsNode", + ) + + # Debug: show payload if needed + # print("[DEBUG] Create payload (RDN=%s):\n%s" % (rdn, create_payload)) + + try: + rf_client = ADWSConnect(ip, domain, username, auth, "ResourceFactory") + rf_client._nmf.send(create_payload) + response = rf_client._nmf.recv() + et = rf_client._handle_str_to_xml(response) + if et is None: + raise RuntimeError("Create/AddRequest returned empty or malformed response") + new_dn = f"{rdn},{valid_container}" + + # Do NOT attempt to set dNSTombstoned here. Creating without dNSTombstoned + # avoids ADWS BadPutOrCreateValue errors. If the caller needs the attribute, + # they should set it explicitly using the tombstone/resurrect helpers. + print(f"[+] Created dnsNode {new_dn} and added A record {ip_addr}") + return True + except Exception as e: + last_exc = e + print(f"[DEBUG] Create attempt with RDN {rdn} failed: {e}") + continue + + # if we get here, all create attempts failed + raise RuntimeError(f"All Create attempts failed. Last error: {last_exc}") + +def modify_dns_record_adws( + fqdn_record: str, + new_ip: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth, + ttl: int = 180, + tcp: bool = False, + forest: bool = False, + legacy: bool = False, +) -> bool: + target, zone = _split_target_and_zone(fqdn_record, domain) + + # reuse pull client for find_dns_node to reduce connections + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + resource_client = ADWSConnect(ip, domain, username, auth, "Resource") + + node_dn, existing_raw, tomb = find_dns_node( + target=target, + zone=zone, + ip=ip, + domain=domain, + username=username, + auth=auth, + forest=forest, + legacy=legacy, + pull_client=pull_client, + res_client=resource_client, + ) + if not node_dn: + raise RuntimeError("Target dnsNode not found; cannot modify") + + serial = get_next_serial(None, ip, zone, tcp) + new_bytes = build_a_record_bytes(new_ip, serial, ttl) + + put_client = ADWSConnect.put_client(ip, domain, username, auth) + + for raw in existing_raw: + try: + put_client.put( + object_ref=node_dn, + operation="delete", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(raw), + ) + except Exception: + pass + + put_client.put( + object_ref=node_dn, + operation="add", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(new_bytes), + ) + + print(f"[+] Replaced A record for {target}.{zone} with {new_ip} via ADWS Put operations") + return True + + +def remove_dns_record_adws( + fqdn_record: str, + ip_to_remove: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth, + tcp: bool = False, + forest: bool = False, + legacy: bool = False, + ldapdelete: bool = False, +) -> bool: + """ + Remove an A record via ADWS. + + Behavior change: + - If multiple DNS records exist for the node, remove only the matching A record (ADWS Put delete). + - If this is the only DNS record for the node, convert it to a tombstone: + * Replace dnsRecord with a TS (type 0) record + * (dNSTombstoned Modify is optional and may be performed separately) + - If ldapdelete=True perform a full delete of the dnsNode via Resource Delete. + """ + target, zone = _split_target_and_zone(fqdn_record, domain) + + # reuse clients to reduce connections + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + resource_client = ADWSConnect(ip, domain, username, auth, "Resource") + put_client = ADWSConnect.put_client(ip, domain, username, auth) + + node_dn, existing_raw, tomb = find_dns_node( + target=target, + zone=zone, + ip=ip, + domain=domain, + username=username, + auth=auth, + forest=forest, + legacy=legacy, + pull_client=pull_client, + res_client=resource_client, + ) + if not node_dn: + raise RuntimeError("Target dnsNode not found; cannot remove") + + # If caller requests full LDAP-style delete via Resource, do that + if ldapdelete: + msg_id = _make_msgid() + delete_payload = LDAP_DELETE_FOR_RESOURCE.format( + object_dn=node_dn, + uuid=msg_id, + fqdn=ip, + ) + resource_client._nmf.send(delete_payload) + response = resource_client._nmf.recv() + et = resource_client._handle_str_to_xml(response) + if et is None: + raise RuntimeError("DeleteResponse empty/malformed") + print(f"[+] Deleted dnsNode {node_dn} via ADWS Resource Delete") + return True + + # If multiple records, remove only the specific A record requested + if len(existing_raw) > 1: + found = None + for record in existing_raw: + try: + dr = DNS_RECORD(record) + if dr["Type"] == 1: + a = DNS_RPC_RECORD_A(dr["Data"]) + if a.formatCanonical() == ip_to_remove: + found = record + break + except Exception: + continue + if not found: + raise RuntimeError("Could not find a matching A record for the specified IP") + put_client.put( + object_ref=node_dn, + operation="delete", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(found), + ) + print(f"[+] Removed A record {target}.{zone} -> {ip_to_remove} via ADWS Put(delete)") + return True + + # If only one record exists, tombstone the node (TS record). dNSTombstoned modify not enforced here. + diff = datetime.datetime.utcnow() - datetime.datetime(1601, 1, 1) + tstime = int(diff.total_seconds() * 10000000) + + ts_bytes = build_ts_record_bytes(tstime) + + # Remove any existing records first + for raw in existing_raw: + try: + put_client.put( + object_ref=node_dn, + operation="delete", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(raw), + ) + except Exception: + pass + + # Add TS record + put_client.put( + object_ref=node_dn, + operation="add", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(ts_bytes), + ) + + # Note: intentionally NOT attempting to change dNSTombstoned here to avoid ADWS validation failures. + return True + + +def tombstone_dns_record_adws( + fqdn_record: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth, + tcp: bool = False, + forest: bool = False, + legacy: bool = False, +) -> bool: + target, zone = _split_target_and_zone(fqdn_record, domain) + + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + resource_client = ADWSConnect(ip, domain, username, auth, "Resource") + node_dn, existing_raw, _ = find_dns_node( + target=target, + zone=zone, + ip=ip, + domain=domain, + username=username, + auth=auth, + forest=forest, + legacy=legacy, + pull_client=pull_client, + res_client=resource_client, + ) + if not node_dn: + raise RuntimeError("Target dnsNode not found; cannot tombstone") + + diff = datetime.datetime.utcnow() - datetime.datetime(1601, 1, 1) + tstime = int(diff.total_seconds() * 10000000) + + ts_bytes = build_ts_record_bytes(tstime) + + put_client = ADWSConnect.put_client(ip, domain, username, auth) + + for raw in existing_raw: + try: + put_client.put( + object_ref=node_dn, + operation="delete", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(raw), + ) + except Exception: + pass + + put_client.put( + object_ref=node_dn, + operation="add", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(ts_bytes), + ) + + # Keep behavior same as other helpers: the strict Replace for dNSTombstoned can be done separately. + print(f"[+] Tombstone applied to dnsNode {target}.{zone} (TS record added).") + return True + + +def resurrect_dns_record_adws( + fqdn_record: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth, + tcp: bool = False, + forest: bool = False, + legacy: bool = False, +) -> bool: + target, zone = _split_target_and_zone(fqdn_record, domain) + + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + resource_client = ADWSConnect(ip, domain, username, auth, "Resource") + node_dn, existing_raw, _ = find_dns_node( + target=target, + zone=zone, + ip=ip, + domain=domain, + username=username, + auth=auth, + forest=forest, + legacy=legacy, + pull_client=pull_client, + res_client=resource_client, + ) + if not node_dn: + raise RuntimeError("Target dnsNode not found; cannot resurrect") + + if len(existing_raw) > 1: + raise RuntimeError("Multiple records present; resurrect behavior is undefined in this helper") + + diff = datetime.datetime.utcnow() - datetime.datetime(1601, 1, 1) + tstime = int(diff.total_seconds() * 10000000) + ts_bytes = build_ts_record_bytes(tstime) + + put_client = ADWSConnect.put_client(ip, domain, username, auth) + + for raw in existing_raw: + try: + put_client.put( + object_ref=node_dn, + operation="delete", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(raw), + ) + except Exception: + pass + + put_client.put( + object_ref=node_dn, + operation="add", + attribute="addata:dnsRecord", + data_type="base64Binary", + value=_b64(ts_bytes), + ) + + # Leave dNSTombstoned change to caller if they need it + print(f"[+] Resurrect helper wrote TS record for {target}.{zone} (caller's responsibility to set dNSTombstoned=False if needed).") + return True \ No newline at end of file diff --git a/src/soa.py b/src/soa.py index e091db8..564b0d0 100644 --- a/src/soa.py +++ b/src/soa.py @@ -1,7 +1,36 @@ +#!/usr/bin/env python3 +""" +soa.py + +Main CLI entrypoint for SOAPy ADWS operations, extended to support AD-integrated DNS +management (add/modify/remove/tombstone/resurrect) via ADWS in addition to computer +management functions (create/delete/disable/spn/asrep/rbcd) already present. + +This file consolidates the functionality from ad_computer_management.py and wires in +the ADWS-based DNS helpers implemented in src/ad_dns_manager_adws.py. + +Usage: same as before, with new DNS options: + --dns-add FQDN Add an A record (requires --dns-ip) + --dns-modify FQDN Replace A record (requires --dns-ip) + --dns-remove FQDN Remove A record (requires --dns-ip unless --ldapdelete) + --dns-tombstone FQDN Tombstone a dnsNode (replace with TS record + set dNSTombstoned=true) + --dns-resurrect FQDN Resurrect a tombstoned dnsNode (replace TS and set dNSTombstoned=false) + --dns-ip IP IP used for add/modify/remove operations + --ldapdelete When used with --dns-remove, delete the dnsNode object instead of removing a record + --allow-multiple Allow multiple A records when adding + --ttl N TTL for A records (default 180) + --tcp Use DNS over TCP for serial lookup +""" + import argparse import logging import sys from base64 import b64decode, b64encode +import base64 +import string +import random +from uuid import uuid4 +from typing import Optional from impacket.examples import logger from impacket.examples.utils import parse_target @@ -15,8 +44,20 @@ ) from src.adws import ADWSConnect, NTLMAuth -from src.soap_templates import NAMESPACES +from src.soap_templates import NAMESPACES, LDAP_CREATE_FOR_RESOURCEFACTORY, LDAP_DELETE_FOR_RESOURCE, LDAP_PUT_FSTRING + +# DNS ADWS helpers (module you added) +from src.ad_dns_manager_adws import ( + add_dns_record_adws, + modify_dns_record_adws, + remove_dns_record_adws, + tombstone_dns_record_adws, + resurrect_dns_record_adws, +) +# --------------------------------------------------------------------------- +# Utility helpers (copied / adapted from ad_computer_management.py) +# --------------------------------------------------------------------------- # https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/rbcd.py#L180 def _create_empty_sd(): @@ -50,39 +91,302 @@ def _create_allow_ace(sid: LDAP_SID): nace["Ace"] = acedata return nace -def getAccountDN( + +def getAccountDN(target: str, username: str, ip: str, domain: str, auth: NTLMAuth): + """Get the distinguishedName of a user or computer in AD using ADWS Pull""" + 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, basedn=None, attributes=attributes) + + distinguishedName_elem = None + + # Look for user first, then computer (same order used in other scripts) + for tag in [".//addata:user", ".//addata:computer"]: + for item in pull_et.findall(tag, namespaces=NAMESPACES): + distinguishedName_elem = item.find( + ".//addata:distinguishedName/ad:value", namespaces=NAMESPACES + ) + if distinguishedName_elem is not None: + break + if distinguishedName_elem is not None: + break + + if distinguishedName_elem is None or distinguishedName_elem.text is None: + raise RuntimeError(f"Unable to locate DN for target '{target}'") + + return distinguishedName_elem.text + + +from xml.etree import ElementTree as ET +from uuid import uuid4 +from src.adws import ADWSConnect, ADWSError +from src.soap_templates import LDAP_DELETE_FOR_RESOURCE + +def delete_computer( + machine_name: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth +) -> bool: + """ + Delete an AD computer object using ADWS WS-Transfer Delete. + + Improved error handling: catches ADWS faults and prints a concise English + message for common cases (insufficient rights, validation errors, ...). + """ + print(f"[*] Attempting to delete computer: {machine_name}") + + # Normalize SAM + sam = machine_name if machine_name.endswith("$") else machine_name + "$" + + # ---- Locate DN of the computer ---- + print("[*] Locating computer in AD...") + try: + dn = getAccountDN( + target=sam, + username=username, + ip=ip, + domain=domain, + auth=auth + ) + except Exception as e: + print(f"[-] Failed to locate machine {sam}: {e}") + return False + + if not dn: + print(f"[-] Could not find DN for computer {sam}") + return False + + print(f"[+] Found DN: {dn}") + + # ---- Build WS-Transfer Delete request ---- + msg_id = f"urn:uuid:{uuid4()}" + + delete_payload = LDAP_DELETE_FOR_RESOURCE.format( + object_dn=dn, + fqdn=ip, + uuid=msg_id + ) + + # ---- Send request ---- + print("[*] Connecting to ADWS Resource endpoint to delete object...") + + client = ADWSConnect(ip, domain, username, auth, "Resource") + try: + client._nmf.send(delete_payload) + response = client._nmf.recv() + except Exception as e: + print(f"[-] Transport error when sending Delete request: {e}") + return False + + # Try to parse the response safely and produce a concise English message on failure + try: + et = client._handle_str_to_xml(response) + except ADWSError: + # Extract useful info from raw SOAP Fault and show a short message + s = response if isinstance(response, str) else response.decode(errors="ignore") + start = s.find('<') + if start != -1: + s = s[start:] + try: + root = ET.fromstring(s) + ns = {'ad': 'http://schemas.microsoft.com/2008/1/ActiveDirectory'} + win32_elem = root.find('.//ad:Win32ErrorCode', namespaces=ns) + errcode_elem = root.find('.//ad:ErrorCode', namespaces=ns) + msg_elem = root.find('.//ad:Message', namespaces=ns) + ext_elem = root.find('.//ad:ExtendedErrorMessage', namespaces=ns) + + win32 = win32_elem.text.strip() if win32_elem is not None and win32_elem.text else None + errcode = errcode_elem.text.strip() if errcode_elem is not None and errcode_elem.text else None + msg = msg_elem.text.strip() if msg_elem is not None and msg_elem.text else None + ext = ext_elem.text.strip() if ext_elem is not None and ext_elem.text else None + + # Map to short, user-friendly messages + if win32 == '5' or errcode == '50' or (msg and 'insufficient access' in msg.lower()): + print("! Insufficient access rights to perform the operation.") + elif msg: + short = msg.splitlines()[0] + print(f"! AD error: {short}") + if ext: + print(f" Details: {ext}") + else: + print("! ADWS operation failed (see server response for details).") + except Exception: + # If parsing fails, fallback to a single-line message + print("! ADWS operation failed and the fault could not be parsed.") + return False + + # If parsing succeeded but returned no XML object + if et is None: + print("[-] Empty or malformed DeleteResponse (AD may still have removed the object).") + return False + + # Check for explicit SOAP Fault even when _handle_str_to_xml did not raise + fault = et.find(".//{http://www.w3.org/2003/05/soap-envelope}Fault") + if fault is not None: + # try the same concise extraction as above + try: + ns = {'ad': 'http://schemas.microsoft.com/2008/1/ActiveDirectory'} + win32_elem = et.find('.//ad:Win32ErrorCode', namespaces=ns) + errcode_elem = et.find('.//ad:ErrorCode', namespaces=ns) + msg_elem = et.find('.//ad:Message', namespaces=ns) + ext_elem = et.find('.//ad:ExtendedErrorMessage', namespaces=ns) + + win32 = win32_elem.text.strip() if win32_elem is not None and win32_elem.text else None + errcode = errcode_elem.text.strip() if errcode_elem is not None and errcode_elem.text else None + msg = msg_elem.text.strip() if msg_elem is not None and msg_elem.text else None + ext = ext_elem.text.strip() if ext_elem is not None and ext_elem.text else None + + if win32 == '5' or errcode == '50' or (msg and 'insufficient access' in msg.lower()): + print("! Insufficient access rights to perform the operation.") + elif msg: + short = msg.splitlines()[0] + print(f"! AD error: {short}") + if ext: + print(f" Details: {ext}") + else: + print("! ADWS operation failed (server returned a SOAP Fault).") + except Exception: + print("! ADWS operation failed (SOAP Fault present).") + return False + + # Success + print(f"[+] Computer {sam} successfully deleted.") + return True + + +def encode_unicode_pwd(password: str) -> str: + # AD requires: password in quotes, UTF-16LE encoded, base64 encoded + quoted = f'"{password}"' + pwd_utf16 = quoted.encode('utf-16-le') + return base64.b64encode(pwd_utf16).decode() + + +def add_computer( target: str, + machine_name: str, + ou_dn: 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 + remove: bool = False, + computer_pass: str = None, + spn_list: list = None, +) -> bool: + """ + Create a computer object in AD via ADWS ResourceFactory (WS-Transfer Create) + and optionally set unicodePwd and SPNs via Put operations. """ + if remove: + raise NotImplementedError("Removal logic is not implemented.") + + # If no machine_name given by user, generate a secure name + import secrets - get_account_query = f"(samAccountName={target})" - pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + if machine_name is None: + machine_name = 'DESKTOP-' + (''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))) - attributes: list = [ - "distinguishedname", + print(f"[+] Using machine ame: {machine_name}") + + # Normalize names + sam = machine_name if machine_name.endswith("$") else machine_name + "$" + cn = machine_name + host = cn.rstrip("$") + + # Find DN container + if ou_dn: + container_dn = ou_dn + else: + domain_parts = [f"DC={p}" for p in domain.split(".") if p] + domain_dn = ",".join(domain_parts) + container_dn = f"CN=Computers,{domain_dn}" + + logging.info(f"[+] Creating computer account {sam} in {container_dn} via ADWS ResourceFactory") + + # ---- Build AttributeTypeAndValue XML blocks ---- + # If no password given by user, generate a secure 16-character password + import secrets + + if computer_pass is None: + alphabet = string.ascii_letters + string.digits + "!@#$%^&*?" + computer_pass = ''.join(secrets.choice(alphabet) for _ in range(16)) + + print(f"[+] Using computer password: {computer_pass}") + + encoded_pass = encode_unicode_pwd(computer_pass) + + # Default SPNs like Powermad / Impacket + default_spns = [ + f"HOST/{host}", + f"HOST/{host}.{domain}", + f"RestrictedKrbHost/{host}", + f"RestrictedKrbHost/{host}.{domain}", ] - pull_et = pull_client.pull(query=get_account_query, basedn=None, attributes=attributes) + spns = spn_list if spn_list else default_spns + + attrs = { + "addata:objectClass": ["computer"], + "ad:container-hierarchy-parent": [container_dn], + "ad:relativeDistinguishedName": [f"CN={cn}"], + "addata:sAMAccountName": [sam], + "addata:userAccountControl": ["4096"], # WORKSTATION_TRUST_ACCOUNT (0x1000) + "addata:dnsHostName": [f"{host}.{domain}"], + "addata:servicePrincipalName": spns, + "addata:unicodePwd": [encoded_pass], + } - for item in pull_et.findall(".//addata:user", namespaces=NAMESPACES): - distinguishedName_elem = item.find( - ".//addata:distinguishedName/ad:value", namespaces=NAMESPACES + atav_xml = "" + for attr_type, values in attrs.items(): + values_xml = "" + for v in values: + if attr_type == "addata:unicodePwd": + # unicodePwd must be sent as base64Binary in ADWS SOAP + values_xml += f'{v}' + else: + # SPNs and dnsHostName are strings; multiple SPNs create multiple entries + values_xml += f'{v}' + + atav_xml += ( + " \n" + f" {attr_type}\n" + f" \n {values_xml}\n \n" + " \n" ) - dn = distinguishedName_elem.text - return dn + # ---- Build SOAP Envelope ---- + msg_id = f"urn:uuid:{uuid4()}" + + addrequest_payload = LDAP_CREATE_FOR_RESOURCEFACTORY.format( + uuid=msg_id, + fqdn=ip, + atav_xml=atav_xml + ) + + # ---- Send AddRequest ---- + client = ADWSConnect(ip, domain, username, auth, "ResourceFactory") + client._nmf.send(addrequest_payload) + response = client._nmf.recv() + + et = client._handle_str_to_xml(response) + if et is None: + raise RuntimeError("AddRequest response empty or malformed.") + + logging.info("[+] AddRequest successful. Locating newly created object...") + + dn = getAccountDN(target=sam, username=username, ip=ip, domain=domain, auth=auth) + if not dn: + raise RuntimeError("Failed to locate DN of the newly created computer.") + + logging.info(f"[+] Created object DN: {dn}") + + print(f"[+] Computer {sam} successfully created in {dn}") + return True def set_spn( @@ -94,20 +398,9 @@ def set_spn( 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) - + """Set a value in servicePrincipalName. Appends value to the attribute rather than replacing.""" + dn = getAccountDN(target=target, username=username, ip=ip, domain=domain, auth=auth) + put_client = ADWSConnect.put_client(ip, domain, username, auth) put_client.put( @@ -118,9 +411,8 @@ def set_spn( value=value, ) - print( - f"[+] servicePrincipalName {value} {'removed' if remove else 'written'} successfully on {target}!" - ) + print(f"[+] servicePrincipalName {value} {'removed' if remove else 'written'} successfully on {target}!") + def set_asrep( target: str, @@ -130,18 +422,7 @@ def set_asrep( 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""" + """Set or clear the DONT_REQ_PREAUTH flag on userAccountControl via ADWS Put (replace).""" get_accounts_queries = f"(sAMAccountName={target})" pull_client = ADWSConnect.pull_client(ip, domain, username, auth) @@ -151,22 +432,20 @@ def set_asrep( ] pull_et = pull_client.pull(query=get_accounts_queries, basedn=None, attributes=attributes) + uac = None + distinguishedName_elem = None + 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 - ) + uac = item.find(".//addata:userAccountControl/ad:value", namespaces=NAMESPACES) + distinguishedName_elem = item.find(".//addata:distinguishedName/ad:value", namespaces=NAMESPACES) + if distinguishedName_elem is None or distinguishedName_elem.text is None: + raise RuntimeError("Unable to locate target DN for asrep operation") 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", @@ -174,7 +453,6 @@ def set_asrep( data_type="string", value=newUac, ) - else: newUac = int(uac.text) & ~0x400000 put_client.put( @@ -185,9 +463,8 @@ def set_asrep( value=newUac, ) - print( - f"[+] DONT_REQ_PREAUTH {'removed' if remove else 'written'} successfully!" - ) + print(f"[+] DONT_REQ_PREAUTH {'removed' if remove else 'written'} successfully!") + def set_rbcd( target: str, @@ -198,25 +475,10 @@ def set_rbcd( 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 - """ - + """Write or remove RBCD (msDS-AllowedToActOnBehalfOfOtherIdentity) using ADWS Put operations.""" 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", @@ -231,17 +493,10 @@ def set_rbcd( 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, - ) + 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 - ) + 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 "" @@ -256,9 +511,7 @@ def set_rbcd( target_sd = SR_SECURITY_DESCRIPTOR(data=b64decode(sd)) if not account_sid: - logging.critical( - f"Unable to find {target} or {account}." - ) + logging.critical(f"Unable to find {target} or {account}.") raise SystemExit() # collect a clean list. remove the account sid if its present @@ -279,7 +532,6 @@ def set_rbcd( 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, @@ -289,12 +541,102 @@ def set_rbcd( value=b64encode(target_sd.getData()).decode("utf-8"), ) - print( - f"[+] msDS-AllowedToActOnBehalfOfIdentity {'removed' if remove else 'written'} successfully!" - ) + print(f"[+] msDS-AllowedToActOnBehalfOfIdentity {'removed' if remove else 'written'} successfully!") print(f"[+] {account} {'can not' if remove else 'can'} delegate to {target}") +def disable_machine_account( + machine_name: str, + username: str, + ip: str, + domain: str, + auth: NTLMAuth +) -> bool: + """ + Disable a computer account (set the ACCOUNTDISABLE flag in userAccountControl) + using ADWS WS-Transfer Put (replace userAccountControl). + """ + print(f"[*] Attempting to disable computer: {machine_name}") + + # Normalize SAM + sam = machine_name if machine_name.endswith("$") else machine_name + "$" + + # ---- Locate current userAccountControl and DN ---- + get_accounts_queries = f"(sAMAccountName={sam})" + pull_client = ADWSConnect.pull_client(ip, domain, username, auth) + + attributes: list = [ + "userAccountControl", + "distinguishedName", + ] + + try: + pull_et = pull_client.pull(query=get_accounts_queries, basedn=None, attributes=attributes) + except Exception as e: + print(f"[-] Failed LDAP pull for {sam}: {e}") + return False + + uac_elem = None + distinguishedName_elem = None + + # Try computer first, then user + for tag in [".//addata:computer", ".//addata:user"]: + for item in pull_et.findall(tag, namespaces=NAMESPACES): + if uac_elem is None: + uac_elem = item.find(".//addata:userAccountControl/ad:value", namespaces=NAMESPACES) + if distinguishedName_elem is None: + distinguishedName_elem = item.find(".//addata:distinguishedName/ad:value", namespaces=NAMESPACES) + if uac_elem is not None and distinguishedName_elem is not None: + break + if uac_elem is not None and distinguishedName_elem is not None: + break + + if distinguishedName_elem is None or distinguishedName_elem.text is None: + print(f"[-] Unable to locate DN for {sam}") + return False + + dn = distinguishedName_elem.text + + if uac_elem is None or uac_elem.text is None: + print(f"[-] Unable to locate userAccountControl for {sam}") + return False + + try: + current_uac = int(uac_elem.text) + except Exception as e: + print(f"[-] Failed to parse userAccountControl value: {e}") + return False + + ACCOUNTDISABLE_FLAG = 0x2 + + if (current_uac & ACCOUNTDISABLE_FLAG) != 0: + print(f"[-] Computer {sam} is already disabled (userAccountControl={current_uac}).") + return True + + new_uac = current_uac | ACCOUNTDISABLE_FLAG + + # ---- Perform Put (replace userAccountControl) ---- + try: + put_client = ADWSConnect.put_client(ip, domain, username, auth) + put_client.put( + object_ref=dn, + operation="replace", + attribute="addata:userAccountControl", + data_type="string", + value=new_uac, + ) + except Exception as e: + print(f"[-] Failed to write new userAccountControl for {sam}: {e}") + return False + + print(f"[+] Computer {sam} successfully disabled (userAccountControl set to {new_uac}).") + return True + + +# --------------------------------------------------------------------------- +# CLI entrypoint: run_cli() +# --------------------------------------------------------------------------- + def run_cli(): print(""" ███████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗ @@ -302,15 +644,15 @@ def run_cli(): ███████╗██║ ██║███████║██████╔╝ ╚████╔╝ ╚════██║██║ ██║██╔══██║██╔═══╝ ╚██╔╝ ███████║╚██████╔╝██║ ██║██║ ██║ -╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ +╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ @_logangoins -github.com/jlevere - """) +github.com/jlevere +""") parser = argparse.ArgumentParser( add_help=True, - description="Perform AD reconnaisance and post-exploitation through ADWS from Linux ", + description="Perform AD reconnaissance and post-exploitation through ADWS from Linux", ) parser.add_argument( "connection", @@ -318,13 +660,13 @@ def run_cli(): help="domain/username[:password]@", ) parser.add_argument( - "--debug", - action="store_true", + "--debug", + action="store_true", help="Turn DEBUG output ON" ) parser.add_argument( - "--ts", - action="store_true", + "--ts", + action="store_true", help="Adds timestamp to every logging output." ) parser.add_argument( @@ -335,105 +677,45 @@ def run_cli(): ) 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( - "-f", "--filter", - action="store", - metavar="attr,attr,...", - help="Attributes to select from the objects returned, in a comma seperated list", - ) - enum.add_argument( - "-dn", "--distinguishedname", - action="store", - metavar="distinguishedname", - help="The root objects distinguishedName for the query", - ) - enum.add_argument( - "-p", "--parse", - action="store_true", - help="Parse attributes to human readable format", - ) + 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 msds-allowedtodelegateto") + enum.add_argument("--unconstrained", action="store_true", help="Enumerate objects with TRUSTED_FOR_DELEGATION") + enum.add_argument("--spns", action="store_true", help="Enumerate accounts with servicePrincipalName set") + enum.add_argument("--asreproastable", action="store_true", help="Enumerate accounts with DONT_REQ_PREAUTH 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("-f", "--filter", action="store", metavar="attr,attr,...", help="Attributes to select, comma separated") + enum.add_argument("-dn", "--distinguishedname", action="store", metavar="distinguishedname", help="The root object's distinguishedName for the query") + enum.add_argument("-p", "--parse", action="store_true", help="Parse attributes to human readable format") 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", - ) + writing.add_argument("--rbcd", action="store", metavar="source", help="Write/remove RBCD (source computer)") + writing.add_argument("--spn", action="store", metavar="value", help='Write servicePrincipalName value (use --remove to delete)') + writing.add_argument("--asrep", action="store_true", help="Write DONT_REQ_PREAUTH flag (asrep roastable)") + writing.add_argument("--account", action="store", metavar="account", help="Account to perform operations on") + writing.add_argument("--remove", action="store_true", help="Remove attribute value based on operation") + + # Computer management (create/delete/disable) + writing.add_argument("--addcomputer", nargs='?', const='', action="store", metavar="MACHINE", help="Create a computer account in AD (optional MACHINE name)") + writing.add_argument("--computer-pass", action="store", metavar="pass", help="Password for the new computer account (optional).") + writing.add_argument("--ou", action="store", metavar="ou", help="DN of the OU where to create the computer (optional).") + writing.add_argument("--delete-computer", action="store", metavar="MACHINE", help="Delete an existing computer account") + writing.add_argument("--disable-account", action="store", metavar="MACHINE", help="Disable a computer account (set AccountDisabled)") + + # DNS management options + writing.add_argument("--dns-add", action="store", metavar="FQDN", help="Add A record (FQDN). Requires --dns-ip") + writing.add_argument("--dns-modify", action="store", metavar="FQDN", help="Modify/replace A record (FQDN). Requires --dns-ip") + writing.add_argument("--dns-remove", action="store", metavar="FQDN", help="Remove A record (FQDN). Requires --dns-ip unless --ldapdelete") + writing.add_argument("--dns-tombstone", action="store", metavar="FQDN", help="Tombstone a dnsNode (replace with TS record + set dNSTombstoned=true)") + writing.add_argument("--dns-resurrect", action="store", metavar="FQDN", help="Resurrect a tombstoned dnsNode") + writing.add_argument("--dns-ip", action="store", metavar="IP", help="IP used with dns add/modify/remove") + writing.add_argument("--ldapdelete", action="store_true", help="Use delete on dnsNode object (when used with --dns-remove)") + writing.add_argument("--allow-multiple", action="store_true", help="Allow multiple A records when adding") + writing.add_argument("--ttl", type=int, default=180, help="TTL for new A record (default 180)") + writing.add_argument("--tcp", action="store_true", help="Use DNS over TCP when fetching SOA serial") if len(sys.argv) == 1: parser.print_help() @@ -452,10 +734,9 @@ def run_cli(): if domain is None: domain = "" - # if there are no supplied auth information, ask for a password interactivly + # Ask for password if missing and username present if password == "" and username != "" and options.hash is None: from getpass import getpass - password = getpass("Password:") queries: dict[str, str] = { @@ -470,12 +751,11 @@ def run_cli(): "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 getattr(options, flag): + ldap_query.append(this_query) if not domain: logging.critical('"domain" must be specified') @@ -486,86 +766,214 @@ def run_cli(): 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"' + + # ----------------------- + # Writing operations + # ----------------------- + + try: + # RBCD + if options.rbcd is not 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, ) - 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"' + # SPN write/remove + elif options.spn is not 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 ) - 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"' + + # ASREP + 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 + ) + + # Add computer + elif getattr(options, "addcomputer", None) is not None: + if not username: + logging.critical('Please specify a username with the connection string') + raise SystemExit() + + machine_name = None if options.addcomputer == "" else options.addcomputer + try: + add_computer( + target=options.account if options.account else None, + machine_name=machine_name, + ou_dn=options.ou, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + remove=options.remove, + computer_pass=options.computer_pass, + ) + display_name = machine_name if machine_name else "(generated)" + print(f"[+] Computer {display_name} {'removed' if options.remove else 'created'} successfully.") + except NotImplementedError as e: + logging.error("Feature not implemented: %s", e) + raise SystemExit(2) + except Exception as e: + logging.exception("Error during add_computer operation: %s", e) + raise SystemExit(1) + + # Disable account + elif options.disable_account: + if not username: + logging.critical('Please specify a username with the connection string') + raise SystemExit() + try: + success = disable_machine_account( + machine_name=options.disable_account, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + ) + if not success: + raise SystemExit(1) + print(f"[+] Computer {options.disable_account} disabled successfully (requested).") + except Exception as e: + logging.exception("Error during disable_account operation: %s", e) + raise SystemExit(1) + + # Delete computer + elif options.delete_computer: + delete_computer( + machine_name=options.delete_computer, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + ) + return + + # ----------------------- + # DNS operations + # ----------------------- + elif options.dns_add: + if not options.dns_ip: + logging.critical("--dns-add requires --dns-ip") + raise SystemExit(1) + add_dns_record_adws( + fqdn_record=options.dns_add, + ip_addr=options.dns_ip, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + allow_multiple=options.allow_multiple, + ttl=options.ttl, + tcp=options.tcp, + ) + + elif options.dns_modify: + if not options.dns_ip: + logging.critical("--dns-modify requires --dns-ip") + raise SystemExit(1) + modify_dns_record_adws( + fqdn_record=options.dns_modify, + new_ip=options.dns_ip, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + ttl=options.ttl, + tcp=options.tcp, + ) + + elif options.dns_remove: + if not options.ldapdelete and not options.dns_ip: + logging.critical("--dns-remove requires --dns-ip unless --ldapdelete is specified") + raise SystemExit(1) + remove_dns_record_adws( + fqdn_record=options.dns_remove, + ip_to_remove=options.dns_ip if options.dns_ip else "", + username=username, + ip=remoteName, + domain=domain, + auth=auth, + tcp=options.tcp, + ldapdelete=options.ldapdelete, + ) + + elif options.dns_tombstone: + tombstone_dns_record_adws( + fqdn_record=options.dns_tombstone, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + tcp=options.tcp, + ) + + elif options.dns_resurrect: + resurrect_dns_record_adws( + fqdn_record=options.dns_resurrect, + username=username, + ip=remoteName, + domain=domain, + auth=auth, + tcp=options.tcp, ) - 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: + # ----------------------- + # Enumeration / Pull operations (default) + # ----------------------- + else: + if not ldap_query: + logging.critical("Query can not be None") + raise SystemExit() - 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 = None - - client.pull(current_query, options.distinguishedname, attributes, print_incrementally=True, parse_values=options.parse) + for current_query in ldap_query: + if not current_query: + continue + + if options.filter is not None: + attributes: list = [x.strip() for x in options.filter.split(",")] + else: + attributes = None + + client.pull(current_query, options.distinguishedname, attributes, print_incrementally=True, parse_values=options.parse) + + except Exception as e: + logging.exception("Operation failed: %s", e) + raise SystemExit(1) if __name__ == "__main__": - run_cli() + run_cli() \ No newline at end of file diff --git a/src/soap_templates.py b/src/soap_templates.py index eda1b0d..a69db60 100644 --- a/src/soap_templates.py +++ b/src/soap_templates.py @@ -105,4 +105,95 @@ - """ \ No newline at end of file + """ + +LDAP_CREATE_FOR_RESOURCEFACTORY: str = """ + + http://schemas.microsoft.com/2008/1/ActiveDirectory/ResourceFactory/Create + ldap:389 + urn:uuid:{uuid} + + http://www.w3.org/2005/08/addressing/anonymous + + net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/ResourceFactory + + + + {container_dn} + {object_class} + + {attributes_xml} + + + +""" + +LDAP_ROOT_DSE_FSTRING: str = """ + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Get + ldap:389 + 11111111-1111-1111-1111-111111111111 + urn:uuid:{uuid} + + http://www.w3.org/2005/08/addressing/anonymous + + net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Resource + + +""" + +LDAP_CREATE_FOR_RESOURCEFACTORY: str = """ + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Create + + + ldap:389 + {uuid} + + http://www.w3.org/2005/08/addressing/anonymous + + + net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/ResourceFactory + + + + +{atav_xml} + + + +""" + +LDAP_DELETE_FOR_RESOURCE: str = """ + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete + ldap:389 + {object_dn} + urn:uuid:{uuid} + + http://www.w3.org/2005/08/addressing/anonymous + + net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Resource + + +"""